From 9527a2fb851c250cc7d9e2931917c9aab17d9cb8 Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Sun, 10 May 2026 00:52:03 +0800 Subject: [PATCH 01/15] feat(sdk): add @codegraff/sdk N-API package + cross-platform CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces sdk/typescript as a workspace member: Rust crate with napi-rs bindings (lib.rs, wire.rs), TypeScript wrappers (lib.js, lib.d.ts), example consumers (agent-demo, compare, benchmark, smoke), and the per-triple npm subpackages so a future publish workflow can ship the addon to npm without re-deriving the matrix. The new sdk-typescript GitHub Actions workflow builds .node binaries for darwin-{arm64,x64} + linux-{arm64,x64}-gnu + win32-x64-msvc on every push touching sdk/typescript/, crates/, or Cargo.{toml,lock}. Publishing is intentionally NOT wired up yet — that's a separate release workflow gated on a tag. Also: gitignore .worktrees/ and codedb.snapshot so the daemon's session state and local worktree metadata stop showing up as pending changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/sdk-typescript.yml | 153 ++ .gitignore | 3 + Cargo.lock | 98 + Cargo.toml | 2 +- sdk/typescript/.gitignore | 18 + sdk/typescript/Cargo.toml | 28 + sdk/typescript/README.md | 205 ++ sdk/typescript/build.rs | 3 + sdk/typescript/examples/agent-demo.ts | 108 + sdk/typescript/examples/benchmark.ts | 347 +++ sdk/typescript/examples/compare.ts | 153 ++ sdk/typescript/examples/session.mjs | 27 + sdk/typescript/examples/smoke.mjs | 35 + sdk/typescript/npm/darwin-arm64/README.md | 3 + sdk/typescript/npm/darwin-arm64/package.json | 19 + sdk/typescript/npm/darwin-x64/README.md | 3 + sdk/typescript/npm/darwin-x64/package.json | 19 + sdk/typescript/npm/linux-arm64-gnu/README.md | 3 + .../npm/linux-arm64-gnu/package.json | 22 + sdk/typescript/npm/linux-x64-gnu/README.md | 3 + sdk/typescript/npm/linux-x64-gnu/package.json | 22 + sdk/typescript/npm/win32-x64-msvc/README.md | 3 + .../npm/win32-x64-msvc/package.json | 19 + sdk/typescript/package-lock.json | 2342 +++++++++++++++++ sdk/typescript/package.json | 58 + sdk/typescript/src/lib.rs | 292 ++ sdk/typescript/src/wire.rs | 154 ++ 27 files changed, 4141 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sdk-typescript.yml create mode 100644 sdk/typescript/.gitignore create mode 100644 sdk/typescript/Cargo.toml create mode 100644 sdk/typescript/README.md create mode 100644 sdk/typescript/build.rs create mode 100644 sdk/typescript/examples/agent-demo.ts create mode 100644 sdk/typescript/examples/benchmark.ts create mode 100644 sdk/typescript/examples/compare.ts create mode 100644 sdk/typescript/examples/session.mjs create mode 100644 sdk/typescript/examples/smoke.mjs create mode 100644 sdk/typescript/npm/darwin-arm64/README.md create mode 100644 sdk/typescript/npm/darwin-arm64/package.json create mode 100644 sdk/typescript/npm/darwin-x64/README.md create mode 100644 sdk/typescript/npm/darwin-x64/package.json create mode 100644 sdk/typescript/npm/linux-arm64-gnu/README.md create mode 100644 sdk/typescript/npm/linux-arm64-gnu/package.json create mode 100644 sdk/typescript/npm/linux-x64-gnu/README.md create mode 100644 sdk/typescript/npm/linux-x64-gnu/package.json create mode 100644 sdk/typescript/npm/win32-x64-msvc/README.md create mode 100644 sdk/typescript/npm/win32-x64-msvc/package.json create mode 100644 sdk/typescript/package-lock.json create mode 100644 sdk/typescript/package.json create mode 100644 sdk/typescript/src/lib.rs create mode 100644 sdk/typescript/src/wire.rs diff --git a/.github/workflows/sdk-typescript.yml b/.github/workflows/sdk-typescript.yml new file mode 100644 index 00000000..d8a46d79 --- /dev/null +++ b/.github/workflows/sdk-typescript.yml @@ -0,0 +1,153 @@ +# Cross-platform build for `@codegraff/sdk` (sdk/typescript). +# +# Builds the napi-rs native addon on each target triple, uploads each .node +# binary as a CI artifact, and then assembles them into the per-triple npm +# subpackages under `sdk/typescript/npm//`. The assembled tree is +# uploaded as a single artifact so a maintainer can sanity-check the layout +# before any future publish workflow gets wired up. +# +# Publishing to npm is intentionally NOT done here. When we're ready to ship +# release 0.2.0 we'll add a separate workflow gated on a `sdk/typescript-vX.Y.Z` +# tag that downloads the assembled artifact and runs `napi prepublish`. + +name: sdk-typescript + +on: + push: + branches: [main] + paths: + - 'sdk/typescript/**' + - 'crates/**' + - 'Cargo.toml' + - 'Cargo.lock' + - '.github/workflows/sdk-typescript.yml' + pull_request: + paths: + - 'sdk/typescript/**' + - 'crates/**' + - 'Cargo.toml' + - 'Cargo.lock' + - '.github/workflows/sdk-typescript.yml' + workflow_dispatch: + +env: + RUSTFLAGS: '-Dwarnings' + +defaults: + run: + # All steps are SDK-scoped — the Cargo workspace is at the repo root but + # `napi build` reads the package.json next to the crate. + working-directory: sdk/typescript + +concurrency: + group: sdk-typescript-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: build (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + strategy: + # Don't let one platform failure mask issues on the others. + fail-fast: false + matrix: + include: + - target: aarch64-apple-darwin + os: macos-latest # Apple Silicon runner + - target: x86_64-apple-darwin + os: macos-13 # Intel runner + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + # Note: aarch64-unknown-linux-gnu and the musl variants need + # cross-compilation (cross / zig). Tracked as a follow-up; for + # now those targets are listed in package.json's + # optionalDependencies but the addon must be built from source. + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: sdk/typescript/package-lock.json + + - name: Setup protoc + # Several forge crates pull in prost-build / tonic-prost-build. + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + + - name: Cache Cargo build + uses: Swatinem/rust-cache@v2 + with: + workspaces: . -> target + shared-key: sdk-typescript-${{ matrix.target }} + + - name: Install npm dependencies + run: npm ci + + - name: Build native addon + run: npm run build -- --target ${{ matrix.target }} + + - name: Upload .node artifact + uses: actions/upload-artifact@v4 + with: + name: bindings-${{ matrix.target }} + # napi build emits codegraff-sdk..node next to the package.json. + path: sdk/typescript/codegraff-sdk.*.node + if-no-files-found: error + retention-days: 7 + + assemble: + name: assemble npm packages + needs: build + runs-on: ubuntu-latest + # Assemble the per-triple subpackages so a maintainer can inspect the + # final shipping layout without us actually publishing anything to npm. + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: sdk/typescript/package-lock.json + + - name: Install npm dependencies + run: npm ci + + - name: Download all build artifacts + uses: actions/download-artifact@v4 + with: + path: sdk/typescript/artifacts + pattern: bindings-* + merge-multiple: false + + - name: Move .node files into npm// + run: npm run artifacts + + - name: List assembled tree + run: | + echo "::group::sdk/typescript/npm tree" + find npm -type f -printf '%p (%s bytes)\n' | sort + echo "::endgroup::" + + - name: Upload assembled npm packages + uses: actions/upload-artifact@v4 + with: + name: sdk-typescript-npm-packages + path: sdk/typescript/npm/ + if-no-files-found: error + retention-days: 14 diff --git a/.gitignore b/.gitignore index d9e9b17c..8207970e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Local git worktrees and codedb daemon snapshots. +.worktrees/ +codedb.snapshot # Generated by Cargo # will have compiled files and executables debug/ diff --git a/Cargo.lock b/Cargo.lock index 752635d6..6704fec5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1438,6 +1438,16 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -2706,6 +2716,26 @@ dependencies = [ "url", ] +[[package]] +name = "forge_sdk_node" +version = "0.1.5" +dependencies = [ + "anyhow", + "forge_api", + "forge_app", + "forge_config", + "forge_domain", + "forge_stream", + "futures", + "napi", + "napi-build", + "napi-derive", + "rustls 0.23.40", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "forge_select" version = "0.1.5" @@ -5270,6 +5300,16 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libredox" version = "0.1.16" @@ -5661,6 +5701,64 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "napi" +version = "2.16.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" +dependencies = [ + "bitflags 2.11.0", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", + "tokio", +] + +[[package]] +name = "napi-build" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" + +[[package]] +name = "napi-derive" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +dependencies = [ + "cfg-if", + "convert_case 0.6.0", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +dependencies = [ + "convert_case 0.6.0", + "once_cell", + "proc-macro2", + "quote", + "regex", + "semver", + "syn 2.0.117", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading", +] + [[package]] name = "native-tls" version = "0.2.18" diff --git a/Cargo.toml b/Cargo.toml index 71394097..ca2a6c74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/*"] +members = ["crates/*", "sdk/typescript"] resolver = "2" diff --git a/sdk/typescript/.gitignore b/sdk/typescript/.gitignore new file mode 100644 index 00000000..0d851ab3 --- /dev/null +++ b/sdk/typescript/.gitignore @@ -0,0 +1,18 @@ +# Build artifacts (regenerated by `napi build` and CI). +target/ +node_modules/ +artifacts/ +*.node + +# codedb daemon snapshot (volatile session state, not source). +codedb.snapshot + +# napi-rs auto-generated loader and types — produced by `napi build`. +index.js +index.d.ts + +# Per-triple npm subpackages: track package.json and README.md, but never +# the platform binary (built fresh in CI per target). The `*.node` rule +# above already covers this; restated here so it's obvious why npm// +# directories are committed but the binaries inside aren't. +npm/*/codegraff-sdk.*.node diff --git a/sdk/typescript/Cargo.toml b/sdk/typescript/Cargo.toml new file mode 100644 index 00000000..a84f0dcb --- /dev/null +++ b/sdk/typescript/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "forge_sdk_node" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +publish = false +description = "N-API bindings exposing the codegraff agent to Node.js / TypeScript." + +[lib] +crate-type = ["cdylib"] + +[dependencies] +napi = { version = "2.16", default-features = false, features = ["napi6", "async", "tokio_rt"] } +napi-derive = "2.16" +forge_api.workspace = true +forge_app.workspace = true +forge_config.workspace = true +forge_domain.workspace = true +forge_stream.workspace = true +serde.workspace = true +serde_json.workspace = true +futures.workspace = true +tokio.workspace = true +anyhow.workspace = true +rustls.workspace = true + +[build-dependencies] +napi-build = "2" diff --git a/sdk/typescript/README.md b/sdk/typescript/README.md new file mode 100644 index 00000000..09b3c9dd --- /dev/null +++ b/sdk/typescript/README.md @@ -0,0 +1,205 @@ +# @codegraff/sdk + +TypeScript / Node SDK for the [codegraff](https://github.com/justrach/codegraff) agent. + +The SDK ships as an N-API native addon (built with [napi-rs](https://napi.rs)) that +embeds the Rust `forge_api::ForgeAPI` directly into your Node process — no +subprocess, no daemon. You drive the agent programmatically and consume its +events as a typed async iterable. + +## Status + +**Phase 4 — cross-platform prebuilds infrastructure (no publish yet).** The +package is wired up so that, once a maintainer is ready, a single tag push can +publish prebuilt binaries for every supported target. Until then, install +falls back to building from source. + +Available surface: + +- `Graff.init(cwd?)` — long-lived instance with the conversation / agent / trajectory surface. +- `runAgent({ prompt, cwd?, conversationId?, model? })` — one-shot async iterator. +- `new GraffSession({ cwd?, conversationId?, model? })` — multi-turn class that retains `conversationId` between `.send()` calls. +- Cancellation: `handle.cancel()` aborts the in-flight chat and `for await ... break` does the same automatically. +- Low-level passthroughs: `GraffApi`, `ChatStreamHandle`, `newConversationId()`, `version()`. + +## Build from source + +```bash +cd sdk/typescript +npm install +npm run build # produces codegraff-sdk..node + index.{js,d.ts} +node -e "console.log(require('./lib.js').version())" +``` + +Requirements: Rust toolchain (1.92+), Node.js 18+, and a C toolchain +appropriate for your platform. + +## Cross-platform builds (CI) + +`.github/workflows/sdk-typescript.yml` defines a build matrix that produces a +`.node` binary for each supported target. Today CI builds: + +| Triple | Runner | Notes | +|---|---|---| +| `aarch64-apple-darwin` | `macos-latest` | native — Apple Silicon | +| `x86_64-apple-darwin` | `macos-13` | native — Intel Mac | +| `x86_64-unknown-linux-gnu` | `ubuntu-latest` | native | +| `x86_64-pc-windows-msvc` | `windows-latest` | native | + +After the matrix completes, an `assemble` job downloads each `bindings-*` +artifact, runs `napi artifacts` to move binaries into the matching +`sdk/typescript/npm//` subpackage, and uploads the consolidated tree +as a single `sdk-typescript-npm-packages` artifact for inspection. + +**Publish is intentionally not wired up.** The assemble job stops after +producing inspectable artifacts. When we're ready for `0.2.0`, a separate +release workflow will download the assembled artifact, run `napi prepublish`, +and call `npm publish` for each subpackage and the root. + +### Triples not yet built in CI + +`aarch64-unknown-linux-gnu`, `x86_64-unknown-linux-musl`, and +`aarch64-unknown-linux-musl` are listed in `package.json`'s +`optionalDependencies` for forward compatibility, but require cross-compile +infrastructure (cross / zig / Alpine docker images). For now, users on those +platforms must build from source. Adding them is a follow-up task that +extends the matrix in `.github/workflows/sdk-typescript.yml`. + +## Layout + +``` +sdk/typescript/ +├── Cargo.toml # forge_sdk_node — cdylib napi-rs crate +├── src/lib.rs # #[napi] bindings → GraffApi, ChatStreamHandle, ... +├── src/wire.rs # ChatResponse → JSON wire format (auto-fires Notify) +├── lib.js / lib.d.ts # public TS surface — Graff, GraffSession, runAgent +├── index.js / index.d.ts # napi-rs auto-generated loader + raw type defs +├── package.json # @codegraff/sdk — main + optionalDependencies +└── npm/ + ├── darwin-arm64/ # @codegraff/sdk-darwin-arm64 + │ ├── package.json # os/cpu/libc filters + │ └── README.md + ├── darwin-x64/ + ├── linux-x64-gnu/ + ├── linux-arm64-gnu/ + └── win32-x64-msvc/ +``` + +The `npm//package.json` files are committed; the `.node` binaries +inside them are not (CI builds them fresh per platform). + +## Usage + +### One-shot + +```ts +import { runAgent } from "@codegraff/sdk"; + +for await (const ev of runAgent({ prompt: "summarise this repo" })) { + switch (ev.type) { + case "TaskMessage": + if (ev.content.kind === "Markdown") process.stdout.write(ev.content.text); + break; + case "ToolCallStart": console.log("→ tool:", ev.tool_call.name); break; + case "TaskComplete": console.log("\n[done]"); break; + } +} +``` + +### Multi-turn session + +```ts +import { GraffSession } from "@codegraff/sdk"; + +const session = new GraffSession({ model: "claude-opus-4-7" }); +for await (const _ of session.send("add a logout button")) { /* render */ } +for await (const _ of session.send("now write a test for it")) { /* render */ } +console.log("session id:", session.conversationId); +``` + +### Long-lived Graff instance + +```ts +import { Graff } from "@codegraff/sdk"; + +const graff = await Graff.init(); + +// Browse history +const recent = await graff.listConversations(20); +const last = await graff.lastConversation(); +console.log("most recent:", last?.id); + +// Manage agents +console.log("active:", await graff.getActiveAgent()); +await graff.setActiveAgent("muse"); +const agents = await graff.getAgentInfos(); // [{ id: "forge", ... }, ...] + +// Run a chat using this Graff's underlying GraffApi +for await (const ev of graff.chat({ prompt: "write a haiku about rust" })) { + if (ev.type === "TaskMessage" && ev.content.kind === "Markdown") { + process.stdout.write(ev.content.text); + } +} + +// Build a session that shares this Graff +const sess = graff.session(); +for await (const _ of sess.send("now make it about typescript")) { /* render */ } + +// Compact / rename / delete history +await graff.renameConversation(last.id, "haiku experiments"); +const compaction = await graff.compactConversation(last.id); +console.log("token reduction:", compaction.original_tokens, "→", compaction.compacted_tokens); +await graff.deleteConversation(last.id); + +// Inspect tool-call trajectory (used by /trace in the TUI) +const events = await graff.listTrajectory(sess.conversationId!); +``` + +### Cancellation + +```ts +for await (const ev of graff.chat({ prompt: "long task..." })) { + if (somethingHappened) break; // calls handle.cancel() automatically via the async generator's finally +} + +// Or explicitly via the low-level handle: +const api = await GraffApi.init(process.cwd()); +const handle = await api.chat({ prompt: "..." }); +setTimeout(() => handle.cancel(), 5000); +for (let raw = await handle.next(); raw != null; raw = await handle.next()) { + console.log(JSON.parse(raw)); +} +``` + +## Event shape + +```ts +type AgentEvent = + | { type: "ConversationStarted"; conversationId: string } // synthetic, surfaced once at start + | { type: "TaskMessage"; content: { kind: "Markdown" | "ToolInput" | "ToolOutput"; ... } } + | { type: "TaskReasoning"; content: string } + | { type: "ToolCallStart"; tool_call: ToolCallFull } + | { type: "ToolCallEnd"; result: ToolResult } + | { type: "RetryAttempt"; cause: string; duration_ms: number } + | { type: "Interrupt"; reason: { kind: "MaxToolFailurePerTurnLimitReached" | "MaxRequestPerTurnLimitReached"; limit: number } } + | { type: "TaskComplete" }; +``` + +See `lib.d.ts` for the full typed surface. + +## Examples + +- **`examples/agent-demo.ts`** *(recommended)* — TypeScript multi-turn session that drives the agent through a real exploration of this repo. Run with `npm run demo`. Tool calls fire automatically (auto-approved); the second turn intentionally relies on memory from the first to prove session context retention. +- `examples/smoke.mjs` — single prompt, prints rendered markdown to stdout. +- `examples/session.mjs` — minimal two-turn conversation. + +All three require a configured codegraff provider (run `graff provider login` +once in this directory or globally). + +## Roadmap + +- ✅ **Phase 1** — scaffold, version export, workspace wiring. +- ✅ **Phase 2** — `runAgent()` async iterator, `GraffSession` class, `WireEvent` JSON. +- ✅ **Phase 3** — `Graff` class with conversation / agent / trajectory management; cancellation. +- ✅ **Phase 4** — Cross-platform build matrix in CI, per-triple npm subpackages, `optionalDependencies` wired up. +- **Future** — Publish workflow (gated on a release tag), Linux ARM64 + musl matrix entries via cross-compile, MCP / workspace / commit / suggest surfaces. diff --git a/sdk/typescript/build.rs b/sdk/typescript/build.rs new file mode 100644 index 00000000..0f1b0100 --- /dev/null +++ b/sdk/typescript/build.rs @@ -0,0 +1,3 @@ +fn main() { + napi_build::setup(); +} diff --git a/sdk/typescript/examples/agent-demo.ts b/sdk/typescript/examples/agent-demo.ts new file mode 100644 index 00000000..e4ceca36 --- /dev/null +++ b/sdk/typescript/examples/agent-demo.ts @@ -0,0 +1,108 @@ +/** + * Multi-turn agent demo for `@codegraff/sdk`. + * + * Drives the local codegraff agent through a series of related prompts + * against this repo, exercising: + * + * - Streaming `AgentEvent`s from the native N-API addon. + * - Real tool execution (read / search / etc.) inside the agent — the SDK + * auto-fires `Notify` so tools run without a TUI to confirm. + * - Conversation memory carried across `.send()` calls. + * - The `Graff` long-lived instance + `session()` factory from Phase 3. + * + * Run from the SDK root: + * + * npm run demo + * + * The demo points at this repo's checkout by default. Override with + * + * GRAFF_CWD=/some/other/workspace npm run demo + */ + +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { Graff, type AgentEvent } from "../lib.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +// examples/ → sdk/typescript/ → sdk/ → repo root +const REPO_ROOT = process.env.GRAFF_CWD ?? path.resolve(__dirname, "../../.."); + +const TURNS: readonly string[] = [ + "List the .rs files under sdk/typescript/src and describe each in ONE sentence. Be concise.", + "Based on what you just saw (don't open any new files), which file would I edit to add a new SDK method that returns workspace info? Reply with just the filename and one short reason.", + "Now confirm by reading that file and quoting the existing method whose structure I should mimic.", +]; + +function ts(): string { + return new Date().toISOString().slice(11, 19); +} + +function renderEvent(ev: AgentEvent, turnIdx: number): void { + const tag = `[turn ${turnIdx + 1} ${ts()}]`; + switch (ev.type) { + case "ConversationStarted": + process.stderr.write(`${tag} ⏵ conversationId=${ev.conversationId.slice(0, 8)}…\n`); + break; + case "TaskMessage": + if (ev.content.kind === "Markdown") { + // Stream the visible answer to stdout so it can be piped/captured. + process.stdout.write(ev.content.text); + } else if (ev.content.kind === "ToolInput") { + process.stderr.write(`${tag} ▸ ${ev.content.title}\n`); + } + break; + case "TaskReasoning": + // Collapse silent reasoning to a single dot per chunk so the demo + // doesn't flood the terminal but you can still see progress. + process.stderr.write("."); + break; + case "ToolCallStart": { + const args = JSON.stringify(ev.tool_call.arguments); + const argPreview = args.length > 80 ? args.slice(0, 77) + "..." : args; + process.stderr.write(`${tag} → ${ev.tool_call.name}(${argPreview})\n`); + break; + } + case "ToolCallEnd": { + const ok = ev.result.output?.is_error ? "✗" : "✓"; + process.stderr.write(`${tag} ${ok} ${ev.result.name}\n`); + break; + } + case "RetryAttempt": + process.stderr.write(`${tag} ⟳ retry: ${ev.cause} (after ${ev.duration_ms}ms)\n`); + break; + case "Interrupt": + process.stderr.write(`${tag} ⚠ interrupt: ${ev.reason.kind}\n`); + break; + case "TaskComplete": + process.stderr.write(`\n${tag} ✓ turn complete\n`); + break; + } +} + +async function main(): Promise { + const graff = await Graff.init(REPO_ROOT); + const active = (await graff.getActiveAgent()) ?? "(default)"; + process.stderr.write( + `@codegraff/sdk ${graff.version()} — cwd=${REPO_ROOT} agent=${active}\n`, + ); + + const session = graff.session(); + + for (let i = 0; i < TURNS.length; i++) { + const prompt = TURNS[i]!; + process.stderr.write(`\n[turn ${i + 1}] >>> ${prompt}\n---\n`); + for await (const ev of session.send(prompt)) { + renderEvent(ev, i); + } + } + + process.stderr.write(`\nfinal conversationId: ${session.conversationId}\n`); + await session.close(); +} + +main().catch((err) => { + process.stderr.write(`\nFATAL: ${err?.stack ?? err}\n`); + process.exit(1); +}); diff --git a/sdk/typescript/examples/benchmark.ts b/sdk/typescript/examples/benchmark.ts new file mode 100644 index 00000000..ae43c60d --- /dev/null +++ b/sdk/typescript/examples/benchmark.ts @@ -0,0 +1,347 @@ +/** + * Defensible side-by-side benchmark: `@codegraff/sdk` vs `@cursor/sdk`. + * + * Improves on `compare.ts` in three ways: + * 1. Creates each agent ONCE up front (amortises cold start). + * 2. Runs a 5-prompt suite of varied complexity on each. + * 3. Reports time-to-first-event (TTFE) and time-to-first-content (TTFC) + * separately from total turn time, so SDK overhead is decomposed from + * model think time. + * + * For codegraff TTFE counts the synthetic `ConversationStarted` event + * (always near-zero); TTFC counts the first real model output + * (TaskMessage / TaskReasoning / ToolCallStart). For cursor TTFE = TTFC + * since the SDK does not emit a synthetic kickoff event. + * + * Run: + * CURSOR_API_KEY= npm run benchmark + * # or, if `cursor-agent login` has stored creds, just: + * npm run benchmark + * + * GRAFF_CWD=/some/path npm run benchmark # different workspace + */ + +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { performance } from "node:perf_hooks"; + +import { Agent, type SDKMessage } from "@cursor/sdk"; + +import { Graff, type AgentEvent } from "../lib.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const REPO_ROOT = process.env.GRAFF_CWD ?? path.resolve(__dirname, "../../.."); + +interface Prompt { + id: string; + text: string; + expectsTools: boolean; +} + +const PROMPTS: Prompt[] = [ + { + id: "p1-trivial", + text: "Reply with just the word PONG. No commentary, no punctuation.", + expectsTools: false, + }, + { + id: "p2-listing", + text: + "List every .rs file directly inside sdk/typescript/src — one filename " + + "per line, no path, no commentary.", + expectsTools: true, + }, + { + id: "p3-read", + text: + "Read sdk/typescript/lib.d.ts and reply with the number of variants in " + + "the AgentEvent discriminated-union type. Just the integer.", + expectsTools: true, + }, + { + id: "p4-search", + text: + "Find the file that defines the `Graff` class (with `static init`). " + + "Reply with just the relative path.", + expectsTools: true, + }, + { + id: "p5-count", + text: + "How many TypeScript files (.ts) live anywhere under sdk/typescript/ " + + "excluding node_modules? Reply with just the integer.", + expectsTools: true, + }, +]; + +interface TurnResult { + ok: boolean; + ttfeMs: number; // first event of any kind + ttfcMs: number; // first content event (model output) + totalMs: number; + toolCalls: number; + finalText: string; + error?: string; +} + +interface SuiteResult { + label: string; + coldStartMs: number; + turns: Map; +} + +function nowMs(): number { + return performance.now(); +} + +async function runCodegraff(): Promise { + const turns = new Map(); + const t0 = nowMs(); + const graff = await Graff.init(REPO_ROOT); + const coldStartMs = nowMs() - t0; + + for (const prompt of PROMPTS) { + const ts = nowMs(); + let ttfe = -1; + let ttfc = -1; + let toolCalls = 0; + let finalText = ""; + try { + for await (const ev of graff.chat({ prompt: prompt.text })) { + const elapsed = nowMs() - ts; + if (ttfe < 0) ttfe = elapsed; + if ( + ttfc < 0 && + ev.type !== "ConversationStarted" && + ev.type !== "TaskComplete" + ) { + ttfc = elapsed; + } + if (ev.type === "ToolCallStart") toolCalls++; + if (ev.type === "TaskMessage" && ev.content.kind === "Markdown") { + finalText += ev.content.text; + } + } + turns.set(prompt.id, { + ok: true, + ttfeMs: Math.max(ttfe, 0), + ttfcMs: Math.max(ttfc, 0), + totalMs: nowMs() - ts, + toolCalls, + finalText, + }); + } catch (err) { + turns.set(prompt.id, { + ok: false, + ttfeMs: Math.max(ttfe, 0), + ttfcMs: Math.max(ttfc, 0), + totalMs: nowMs() - ts, + toolCalls, + finalText, + error: err instanceof Error ? err.message : String(err), + }); + } + } + + return { label: "codegraff", coldStartMs, turns }; +} + +async function runCursor(): Promise { + const turns = new Map(); + const t0 = nowMs(); + let agent: Awaited>; + try { + agent = await Agent.create({ + apiKey: process.env.CURSOR_API_KEY, + model: { id: "gpt-5.5" }, + local: { cwd: REPO_ROOT }, + }); + } catch (err) { + const coldStartMs = nowMs() - t0; + const msg = err instanceof Error ? err.message : String(err); + for (const p of PROMPTS) { + turns.set(p.id, { + ok: false, + ttfeMs: 0, + ttfcMs: 0, + totalMs: 0, + toolCalls: 0, + finalText: "", + error: `init failed: ${msg.split("\n")[0]}`, + }); + } + return { label: "cursor", coldStartMs, turns }; + } + const coldStartMs = nowMs() - t0; + + for (const prompt of PROMPTS) { + const ts = nowMs(); + let ttfe = -1; + let toolCalls = 0; + let finalText = ""; + try { + const run = await agent.send(prompt.text); + for await (const msg of run.stream() as AsyncIterable) { + const elapsed = nowMs() - ts; + if (ttfe < 0) ttfe = elapsed; + if (msg.type === "tool_call" && msg.status === "running") { + toolCalls++; + } + if (msg.type === "assistant") { + for (const block of msg.message.content) { + if (block.type === "text") finalText += block.text; + } + } + } + turns.set(prompt.id, { + ok: true, + ttfeMs: Math.max(ttfe, 0), + ttfcMs: Math.max(ttfe, 0), // cursor: no synthetic kickoff event + totalMs: nowMs() - ts, + toolCalls, + finalText, + }); + } catch (err) { + turns.set(prompt.id, { + ok: false, + ttfeMs: Math.max(ttfe, 0), + ttfcMs: Math.max(ttfe, 0), + totalMs: nowMs() - ts, + toolCalls, + finalText, + error: err instanceof Error ? err.message : String(err), + }); + } + } + + await agent.close?.(); + return { label: "cursor", coldStartMs, turns }; +} + +function fmtMs(ms: number): string { + if (ms < 1000) return `${ms.toFixed(0)}ms`; + return `${(ms / 1000).toFixed(2)}s`; +} + +function pad(s: string, w: number, right = false): string { + if (s.length >= w) return s.slice(0, w); + const fill = " ".repeat(w - s.length); + return right ? fill + s : s + fill; +} + +function avg(ns: number[]): number { + if (ns.length === 0) return 0; + return ns.reduce((a, b) => a + b, 0) / ns.length; +} + +function summary(label: string, suite: SuiteResult): void { + console.log(`\n=== ${label.toUpperCase()} ===`); + console.log(`cold start (init): ${fmtMs(suite.coldStartMs)}`); + console.log(""); + console.log( + pad("prompt", 14) + + pad("status", 8) + + pad("ttfe", 10, true) + + pad("ttfc", 10, true) + + pad("total", 10, true) + + pad("tools", 7, true) + + " preview", + ); + console.log("-".repeat(80)); + const oks: TurnResult[] = []; + for (const p of PROMPTS) { + const t = suite.turns.get(p.id); + if (!t) continue; + if (t.ok) oks.push(t); + const status = t.ok ? "ok" : "FAIL"; + const preview = (t.finalText || t.error || "").replace(/\s+/g, " ").trim().slice(0, 40); + console.log( + pad(p.id, 14) + + pad(status, 8) + + pad(t.ok ? fmtMs(t.ttfeMs) : "—", 10, true) + + pad(t.ok ? fmtMs(t.ttfcMs) : "—", 10, true) + + pad(fmtMs(t.totalMs), 10, true) + + pad(String(t.toolCalls), 7, true) + + " " + + preview, + ); + } + console.log("-".repeat(80)); + if (oks.length > 0) { + console.log( + `means (n=${oks.length}): ttfe=${fmtMs(avg(oks.map((t) => t.ttfeMs)))} ` + + `ttfc=${fmtMs(avg(oks.map((t) => t.ttfcMs)))} ` + + `total=${fmtMs(avg(oks.map((t) => t.totalMs)))} ` + + `tools/turn=${avg(oks.map((t) => t.toolCalls)).toFixed(1)}`, + ); + const sumTotal = oks.reduce((a, t) => a + t.totalMs, 0); + console.log(`wall-clock for ${oks.length} prompts: ${fmtMs(sumTotal)}`); + } +} + +function comparison(a: SuiteResult, b: SuiteResult): void { + console.log(`\n=== HEAD-TO-HEAD ===`); + console.log( + pad("prompt", 14) + + pad(`${a.label} ttfc`, 14, true) + + pad(`${b.label} ttfc`, 14, true) + + pad("ratio", 10, true) + + pad(`${a.label} total`, 14, true) + + pad(`${b.label} total`, 14, true) + + pad("ratio", 10, true), + ); + console.log("-".repeat(90)); + for (const p of PROMPTS) { + const ta = a.turns.get(p.id); + const tb = b.turns.get(p.id); + if (!ta?.ok || !tb?.ok) { + console.log(pad(p.id, 14) + " (skipped — at least one side failed)"); + continue; + } + const ttfcRatio = tb.ttfcMs > 0 ? (tb.ttfcMs / Math.max(ta.ttfcMs, 1)).toFixed(1) + "x" : "—"; + const totalRatio = + tb.totalMs > 0 ? (tb.totalMs / Math.max(ta.totalMs, 1)).toFixed(1) + "x" : "—"; + console.log( + pad(p.id, 14) + + pad(fmtMs(ta.ttfcMs), 14, true) + + pad(fmtMs(tb.ttfcMs), 14, true) + + pad(ttfcRatio, 10, true) + + pad(fmtMs(ta.totalMs), 14, true) + + pad(fmtMs(tb.totalMs), 14, true) + + pad(totalRatio, 10, true), + ); + } + console.log("-".repeat(90)); + console.log( + `cold-start tax: ${a.label}=${fmtMs(a.coldStartMs)} ` + + `${b.label}=${fmtMs(b.coldStartMs)} ` + + `(${b.label} pays ${ + a.coldStartMs > 0 ? (b.coldStartMs / a.coldStartMs).toFixed(1) + "x" : "?" + } of ${a.label})`, + ); +} + +async function main(): Promise { + console.log(`Workspace: ${REPO_ROOT}`); + console.log(`Suite: ${PROMPTS.length} prompts, both agents init'd ONCE.`); + console.log(`Running codegraff first (in-process), then cursor (cloud-orchestrated).\n`); + + console.log("--- codegraff (init + 5 prompts) ---"); + const codegraff = await runCodegraff(); + + console.log("--- cursor (init + 5 prompts) ---"); + const cursor = await runCursor(); + + summary("codegraff", codegraff); + summary("cursor", cursor); + comparison(codegraff, cursor); + + console.log("\nNote: ratios are descriptive, not statistically rigorous (n=1 per prompt)."); + console.log("Run with `npm run benchmark` repeatedly to characterise variance."); +} + +main().catch((err) => { + console.error("\nFATAL:", err?.stack ?? err); + process.exit(1); +}); diff --git a/sdk/typescript/examples/compare.ts b/sdk/typescript/examples/compare.ts new file mode 100644 index 00000000..06d3d229 --- /dev/null +++ b/sdk/typescript/examples/compare.ts @@ -0,0 +1,153 @@ +/** + * Side-by-side comparison demo: `@codegraff/sdk` vs `@cursor/sdk`. + * + * Both agents are pointed at this codegraff checkout and given the same + * prompt. Each runs in parallel; we capture per-agent wall-clock duration, + * tool-call count, and final assistant text, then print a comparison table. + * + * Auth: + * - codegraff inherits the local `graff` config (provider/model already + * selected via `graff provider login`). + * - cursor uses `CURSOR_API_KEY` if set, otherwise falls back to the + * credentials stored by `cursor-agent login`. If neither is available + * the cursor side surfaces a clear error in the comparison row. + * + * Run: + * npm run compare + * + * GRAFF_CWD=/some/path npm run compare # point at a different workspace + */ + +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { Agent, type SDKMessage } from "@cursor/sdk"; + +import { Graff, type AgentEvent } from "../lib.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +// examples/ → sdk/typescript/ → sdk/ → repo root +const REPO_ROOT = process.env.GRAFF_CWD ?? path.resolve(__dirname, "../../.."); + +const PROMPT = + "Read sdk/typescript/lib.d.ts and list every variant of the `AgentEvent` " + + "discriminated-union type. Reply with ONE variant name per line, no " + + "commentary, no code fence."; + +interface RunSummary { + label: string; + ok: boolean; + durationMs: number; + toolCalls: number; + finalText: string; + error?: string; +} + +async function runCodegraff(): Promise { + const t0 = Date.now(); + let toolCalls = 0; + let finalText = ""; + try { + const graff = await Graff.init(REPO_ROOT); + for await (const ev of graff.chat({ prompt: PROMPT })) { + if (ev.type === "ToolCallStart") toolCalls++; + if (ev.type === "TaskMessage" && ev.content.kind === "Markdown") { + finalText += ev.content.text; + } + } + return { label: "codegraff", ok: true, durationMs: Date.now() - t0, toolCalls, finalText }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return { label: "codegraff", ok: false, durationMs: Date.now() - t0, toolCalls, finalText, error: msg }; + } +} + +async function runCursor(): Promise { + const t0 = Date.now(); + let toolCalls = 0; + let finalText = ""; + try { + // Per @cursor/sdk types: `apiKey` is optional. When unset, the SDK uses + // whatever credentials `cursor-agent login` stored. We forward an env-var + // override if present so CI / scripted use can authenticate explicitly. + const agent = await Agent.create({ + apiKey: process.env.CURSOR_API_KEY, + model: { id: "gpt-5.5" }, + local: { cwd: REPO_ROOT }, + }); + const run = await agent.send(PROMPT); + + for await (const msg of run.stream() as AsyncIterable) { + if (msg.type === "tool_call" && msg.status === "running") { + toolCalls++; + } + if (msg.type === "assistant") { + for (const block of msg.message.content) { + if (block.type === "text") finalText += block.text; + } + } + } + + await agent.close?.(); + return { label: "cursor", ok: true, durationMs: Date.now() - t0, toolCalls, finalText }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return { label: "cursor", ok: false, durationMs: Date.now() - t0, toolCalls, finalText, error: msg }; + } +} + +function pad(s: string, w: number): string { + return s.length >= w ? s.slice(0, w) : s + " ".repeat(w - s.length); +} + +function printTable(rows: RunSummary[]): void { + const colW = 38; + const sep = "+" + "-".repeat(15) + "+" + rows.map(() => "-".repeat(colW)).join("+") + "+"; + console.log(""); + console.log(sep); + process.stdout.write("| " + pad("metric", 13) + " "); + for (const r of rows) process.stdout.write("| " + pad(r.label.toUpperCase(), colW - 2) + " "); + console.log("|"); + console.log(sep); + const fields: Array<[string, (r: RunSummary) => string]> = [ + ["status", (r) => (r.ok ? "✓ ok" : "✗ " + (r.error?.split("\n")[0] ?? "failed").slice(0, colW - 6))], + ["duration", (r) => `${(r.durationMs / 1000).toFixed(1)}s`], + ["tool calls", (r) => String(r.toolCalls)], + ["answer chars", (r) => String(r.finalText.length)], + ["preview", (r) => r.finalText.replace(/\s+/g, " ").trim().slice(0, colW - 5)], + ]; + for (const [name, fn] of fields) { + process.stdout.write("| " + pad(name, 13) + " "); + for (const r of rows) process.stdout.write("| " + pad(fn(r), colW - 2) + " "); + console.log("|"); + } + console.log(sep); +} + +function printAnswers(rows: RunSummary[]): void { + for (const r of rows) { + console.log(`\n=== ${r.label.toUpperCase()} final answer ===`); + if (r.ok) { + console.log(r.finalText.trim() || "(empty)"); + } else { + console.log(`(error: ${r.error})`); + } + } +} + +async function main(): Promise { + console.log(`Prompt:\n ${PROMPT}\n`); + console.log(`Workspace: ${REPO_ROOT}`); + console.log(`Running codegraff + cursor in parallel...`); + + const results = await Promise.all([runCodegraff(), runCursor()]); + + printTable(results); + printAnswers(results); +} + +main().catch((err) => { + console.error("\nFATAL:", err?.stack ?? err); + process.exit(1); +}); diff --git a/sdk/typescript/examples/session.mjs b/sdk/typescript/examples/session.mjs new file mode 100644 index 00000000..66368fff --- /dev/null +++ b/sdk/typescript/examples/session.mjs @@ -0,0 +1,27 @@ +// Multi-turn session example. +// +// Run: +// node examples/session.mjs +// +// Demonstrates that a single GraffSession reuses one conversationId across +// `.send()` calls so the agent retains memory of the prior turn. + +import { GraffSession } from "../lib.js"; + +const session = new GraffSession(); + +async function turn(prompt) { + console.log(`\n>>> ${prompt}`); + for await (const ev of session.send(prompt)) { + if (ev.type === "TaskMessage" && ev.content.kind === "Markdown") { + process.stdout.write(ev.content.text); + } else if (ev.type === "TaskComplete") { + process.stdout.write("\n"); + } + } + console.log(`(conversationId = ${session.conversationId})`); +} + +await turn("my name is rach. remember it."); +await turn("what name did i tell you?"); +await session.close(); diff --git a/sdk/typescript/examples/smoke.mjs b/sdk/typescript/examples/smoke.mjs new file mode 100644 index 00000000..981d7811 --- /dev/null +++ b/sdk/typescript/examples/smoke.mjs @@ -0,0 +1,35 @@ +// Manual smoke test for @codegraff/sdk. +// +// Requires: +// - `npm run build` has been run in this directory. +// - A working .forge.toml in the cwd or globally configured codegraff +// credentials (since chat hits a real provider). +// +// Run: +// node examples/smoke.mjs "hello, who are you?" +// +// On success this prints a stream of decoded AgentEvent objects ending in a +// `TaskComplete`. Use it to sanity-check the binding against your local setup. + +import { runAgent, version } from "../lib.js"; + +const prompt = process.argv.slice(2).join(" ") || "reply with the single word OK"; + +console.log(`@codegraff/sdk version: ${version()}`); +console.log(`prompt: ${prompt}`); +console.log("---"); + +try { + for await (const ev of runAgent({ prompt })) { + if (ev.type === "TaskMessage" && ev.content.kind === "Markdown") { + process.stdout.write(ev.content.text); + } else if (ev.type === "TaskComplete") { + process.stdout.write("\n--- TaskComplete ---\n"); + } else { + console.log(JSON.stringify(ev)); + } + } +} catch (e) { + console.error("agent error:", e); + process.exit(1); +} diff --git a/sdk/typescript/npm/darwin-arm64/README.md b/sdk/typescript/npm/darwin-arm64/README.md new file mode 100644 index 00000000..504a5c70 --- /dev/null +++ b/sdk/typescript/npm/darwin-arm64/README.md @@ -0,0 +1,3 @@ +# `@codegraff/sdk-darwin-arm64` + +This is the **aarch64-apple-darwin** binary for `@codegraff/sdk` diff --git a/sdk/typescript/npm/darwin-arm64/package.json b/sdk/typescript/npm/darwin-arm64/package.json new file mode 100644 index 00000000..9911ba2a --- /dev/null +++ b/sdk/typescript/npm/darwin-arm64/package.json @@ -0,0 +1,19 @@ +{ + "name": "@codegraff/sdk-darwin-arm64", + "version": "0.1.5", + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "main": "codegraff-sdk.darwin-arm64.node", + "files": [ + "codegraff-sdk.darwin-arm64.node" + ], + "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", + "license": "MIT", + "engines": { + "node": ">= 18" + } +} \ No newline at end of file diff --git a/sdk/typescript/npm/darwin-x64/README.md b/sdk/typescript/npm/darwin-x64/README.md new file mode 100644 index 00000000..e296bf5c --- /dev/null +++ b/sdk/typescript/npm/darwin-x64/README.md @@ -0,0 +1,3 @@ +# `@codegraff/sdk-darwin-x64` + +This is the **x86_64-apple-darwin** binary for `@codegraff/sdk` diff --git a/sdk/typescript/npm/darwin-x64/package.json b/sdk/typescript/npm/darwin-x64/package.json new file mode 100644 index 00000000..41ab8646 --- /dev/null +++ b/sdk/typescript/npm/darwin-x64/package.json @@ -0,0 +1,19 @@ +{ + "name": "@codegraff/sdk-darwin-x64", + "version": "0.1.5", + "os": [ + "darwin" + ], + "cpu": [ + "x64" + ], + "main": "codegraff-sdk.darwin-x64.node", + "files": [ + "codegraff-sdk.darwin-x64.node" + ], + "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", + "license": "MIT", + "engines": { + "node": ">= 18" + } +} \ No newline at end of file diff --git a/sdk/typescript/npm/linux-arm64-gnu/README.md b/sdk/typescript/npm/linux-arm64-gnu/README.md new file mode 100644 index 00000000..50c6a281 --- /dev/null +++ b/sdk/typescript/npm/linux-arm64-gnu/README.md @@ -0,0 +1,3 @@ +# `@codegraff/sdk-linux-arm64-gnu` + +This is the **aarch64-unknown-linux-gnu** binary for `@codegraff/sdk` diff --git a/sdk/typescript/npm/linux-arm64-gnu/package.json b/sdk/typescript/npm/linux-arm64-gnu/package.json new file mode 100644 index 00000000..f4be87d1 --- /dev/null +++ b/sdk/typescript/npm/linux-arm64-gnu/package.json @@ -0,0 +1,22 @@ +{ + "name": "@codegraff/sdk-linux-arm64-gnu", + "version": "0.1.5", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "main": "codegraff-sdk.linux-arm64-gnu.node", + "files": [ + "codegraff-sdk.linux-arm64-gnu.node" + ], + "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "libc": [ + "glibc" + ] +} \ No newline at end of file diff --git a/sdk/typescript/npm/linux-x64-gnu/README.md b/sdk/typescript/npm/linux-x64-gnu/README.md new file mode 100644 index 00000000..4c747775 --- /dev/null +++ b/sdk/typescript/npm/linux-x64-gnu/README.md @@ -0,0 +1,3 @@ +# `@codegraff/sdk-linux-x64-gnu` + +This is the **x86_64-unknown-linux-gnu** binary for `@codegraff/sdk` diff --git a/sdk/typescript/npm/linux-x64-gnu/package.json b/sdk/typescript/npm/linux-x64-gnu/package.json new file mode 100644 index 00000000..dbc741e9 --- /dev/null +++ b/sdk/typescript/npm/linux-x64-gnu/package.json @@ -0,0 +1,22 @@ +{ + "name": "@codegraff/sdk-linux-x64-gnu", + "version": "0.1.5", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "main": "codegraff-sdk.linux-x64-gnu.node", + "files": [ + "codegraff-sdk.linux-x64-gnu.node" + ], + "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "libc": [ + "glibc" + ] +} \ No newline at end of file diff --git a/sdk/typescript/npm/win32-x64-msvc/README.md b/sdk/typescript/npm/win32-x64-msvc/README.md new file mode 100644 index 00000000..7514f7f9 --- /dev/null +++ b/sdk/typescript/npm/win32-x64-msvc/README.md @@ -0,0 +1,3 @@ +# `@codegraff/sdk-win32-x64-msvc` + +This is the **x86_64-pc-windows-msvc** binary for `@codegraff/sdk` diff --git a/sdk/typescript/npm/win32-x64-msvc/package.json b/sdk/typescript/npm/win32-x64-msvc/package.json new file mode 100644 index 00000000..fa8bafe4 --- /dev/null +++ b/sdk/typescript/npm/win32-x64-msvc/package.json @@ -0,0 +1,19 @@ +{ + "name": "@codegraff/sdk-win32-x64-msvc", + "version": "0.1.5", + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "main": "codegraff-sdk.win32-x64-msvc.node", + "files": [ + "codegraff-sdk.win32-x64-msvc.node" + ], + "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", + "license": "MIT", + "engines": { + "node": ">= 18" + } +} \ No newline at end of file diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json new file mode 100644 index 00000000..9ed498d3 --- /dev/null +++ b/sdk/typescript/package-lock.json @@ -0,0 +1,2342 @@ +{ + "name": "@codegraff/sdk", + "version": "0.1.5", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@codegraff/sdk", + "version": "0.1.5", + "license": "MIT", + "devDependencies": { + "@cursor/sdk": "^1.0.12", + "@napi-rs/cli": "^2.18.4", + "tsx": "^4.20.0" + }, + "engines": { + "node": ">= 18" + }, + "optionalDependencies": { + "@codegraff/sdk-darwin-arm64": "0.1.5", + "@codegraff/sdk-darwin-x64": "0.1.5", + "@codegraff/sdk-linux-arm64-gnu": "0.1.5", + "@codegraff/sdk-linux-x64-gnu": "0.1.5", + "@codegraff/sdk-win32-x64-msvc": "0.1.5" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==", + "dev": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@codegraff/sdk-darwin-arm64": { + "optional": true + }, + "node_modules/@codegraff/sdk-darwin-x64": { + "optional": true + }, + "node_modules/@codegraff/sdk-linux-arm64-gnu": { + "optional": true + }, + "node_modules/@codegraff/sdk-linux-x64-gnu": { + "optional": true + }, + "node_modules/@codegraff/sdk-win32-x64-msvc": { + "optional": true + }, + "node_modules/@connectrpc/connect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.7.0.tgz", + "integrity": "sha512-iNKdJRi69YP3mq6AePRT8F/HrxWCewrhxnLMNm0vpqXAR8biwzRtO6Hjx80C6UvtKJ5sFmffQT7I4Baecz389w==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0" + } + }, + "node_modules/@connectrpc/connect-node": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-1.7.0.tgz", + "integrity": "sha512-6vaPIkG/NyhxlYgytLoR9KYbPhczEboFB2OYWkA9qvUz1K7efXfeGrlRxoLtpa+r8VxyIOw73w5ktNe743nD+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "undici": "^5.28.4" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "1.7.0" + } + }, + "node_modules/@cursor/sdk": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk/-/sdk-1.0.12.tgz", + "integrity": "sha512-jGx0wFY1N9uIdIKr303CfM6m/dLXmRCUnU/0yNP/oiOpkBXqgqaThGbgYbcOeVrYonMZc/DZJ9EydXOEPJLcbg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@bufbuild/protobuf": "1.10.0", + "@connectrpc/connect": "^1.6.1", + "@connectrpc/connect-node": "^1.6.1", + "@statsig/js-client": "3.31.0", + "sqlite3": "^5.1.7", + "zod": "^3.25.0" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@cursor/sdk-darwin-arm64": "1.0.12", + "@cursor/sdk-darwin-x64": "1.0.12", + "@cursor/sdk-linux-arm64": "1.0.12", + "@cursor/sdk-linux-x64": "1.0.12", + "@cursor/sdk-win32-x64": "1.0.12" + } + }, + "node_modules/@cursor/sdk-darwin-arm64": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-arm64/-/sdk-darwin-arm64-1.0.12.tgz", + "integrity": "sha512-AOFx+aX+4SntAeC66YncHACXk5duxp+HzDrxxF4Tl93N6nLjHaHEKSAXbt87ivL34MCHop4v/3c70QzBhamB2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cursor/sdk-darwin-x64": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-darwin-x64/-/sdk-darwin-x64-1.0.12.tgz", + "integrity": "sha512-/ZDAYFUrnPd8hAGRky9ZGcROqZSZ2b5W+aEjTdINzLhJ8x5ZNXtjaz0ZYSHabOn2BeErjXgTcq+4bX2/To4C1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cursor/sdk-linux-arm64": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-linux-arm64/-/sdk-linux-arm64-1.0.12.tgz", + "integrity": "sha512-kAxNqiB3dPtlW9fVjjIZEdbIGEGLA9moOM3zYwsXh8J1Qw942nJYMGDGR4o8x0zglwZ24a1JpovvZamrCaC3Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cursor/sdk-linux-x64": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-linux-x64/-/sdk-linux-x64-1.0.12.tgz", + "integrity": "sha512-RmBiBCPKMZC5McDerGk2Rk4P47xz2A+uzRoRgH6sMoOjklc33ry11iAZC0D5F5xH85chgY878086A/Q8+XrAuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cursor/sdk-win32-x64": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@cursor/sdk-win32-x64/-/sdk-win32-x64-1.0.12.tgz", + "integrity": "sha512-uH4shdHrKOdtNLapy1uuScJ9lL2Pc8zc9I9ZKC6b6bx+0UX6xLAqjPP7dqVPfO6D9u61yLq1Hs86XOLs5ZVkPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@napi-rs/cli": { + "version": "2.18.4", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.4.tgz", + "integrity": "sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==", + "dev": true, + "license": "MIT", + "bin": { + "napi": "scripts/index.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@statsig/client-core": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@statsig/client-core/-/client-core-3.31.0.tgz", + "integrity": "sha512-SuxQD6TmVszPG7FoMKwTk/uyBuVFk7XnxI3T/E0uyb7PL7GNjONtfsoh+NqBBVUJVse0CUeSFfgJPoZy1ZOslQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@statsig/js-client": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/@statsig/js-client/-/js-client-3.31.0.tgz", + "integrity": "sha512-LFa5E0LjT6sTfZv3sNGoyRLSZ1078+agdgOA+Vm1ecjG+KbSOfBLTW7hMwimrJ29slRwbYDzbtKaPJo/R37N2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@statsig/client-core": "3.31.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json new file mode 100644 index 00000000..14a07baa --- /dev/null +++ b/sdk/typescript/package.json @@ -0,0 +1,58 @@ +{ + "name": "@codegraff/sdk", + "version": "0.1.5", + "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", + "main": "lib.js", + "types": "lib.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/justrach/codegraff.git", + "directory": "sdk/typescript" + }, + "napi": { + "name": "codegraff-sdk", + "triples": { + "defaults": false, + "additional": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-pc-windows-msvc" + ] + } + }, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "scripts": { + "build": "napi build --platform --release", + "build:debug": "napi build --platform", + "artifacts": "napi artifacts", + "create-npm-dirs": "napi create-npm-dir -t .", + "version": "napi version", + "smoke": "node examples/smoke.mjs", + "demo": "tsx examples/agent-demo.ts", + "compare": "tsx examples/compare.ts", + "benchmark": "tsx examples/benchmark.ts" + }, + "devDependencies": { + "@cursor/sdk": "^1.0.12", + "@napi-rs/cli": "^2.18.4", + "tsx": "^4.20.0" + }, + "optionalDependencies": { + "@codegraff/sdk-darwin-arm64": "0.1.5", + "@codegraff/sdk-darwin-x64": "0.1.5", + "@codegraff/sdk-linux-arm64-gnu": "0.1.5", + "@codegraff/sdk-linux-x64-gnu": "0.1.5", + "@codegraff/sdk-win32-x64-msvc": "0.1.5" + }, + "files": [ + "lib.js", + "lib.d.ts", + "index.js", + "index.d.ts" + ] +} diff --git a/sdk/typescript/src/lib.rs b/sdk/typescript/src/lib.rs new file mode 100644 index 00000000..fbb7148a --- /dev/null +++ b/sdk/typescript/src/lib.rs @@ -0,0 +1,292 @@ +//! N-API bindings for the codegraff agent. +//! +//! See `sdk/typescript/lib.js` and `sdk/typescript/lib.d.ts` for the +//! ergonomic public surface; this module exposes the raw building blocks. +//! Methods that return data structures emit JSON strings; the `Graff` JS +//! wrapper parses them so callers get typed objects. + +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::{Arc, OnceLock}; + +use forge_api::{API, ChatRequest, ForgeAPI}; +use forge_config::ForgeConfig; +use forge_domain::{AgentId, ChatResponse, Conversation, ConversationId, Event, ModelId}; +use forge_stream::MpscStream; +use futures::StreamExt; +use napi::bindgen_prelude::*; +use napi_derive::napi; +use tokio::sync::Mutex as AsyncMutex; + +mod wire; +use wire::WireEvent; + +static CRYPTO_INIT: OnceLock<()> = OnceLock::new(); + +fn ensure_crypto() { + CRYPTO_INIT.get_or_init(|| { + // Required by rustls 0.23+ when multiple crypto providers are linked. + let _ = rustls::crypto::ring::default_provider().install_default(); + }); +} + +fn err(msg: impl Into) -> napi::Error { + napi::Error::from_reason(msg.into()) +} + +fn map_err(e: E) -> napi::Error { + err(format!("{e:?}")) +} + +fn parse_conv_id(s: &str) -> Result { + ConversationId::from_str(s).map_err(|e| err(format!("invalid conversation id `{s}`: {e:?}"))) +} + +fn to_json(v: &T, what: &str) -> Result { + serde_json::to_string(v).map_err(|e| err(format!("serialize {what}: {e}"))) +} + +/// JS-friendly mirror of `forge_domain::ChatRequest`. +#[napi(object)] +pub struct ChatRequestJs { + /// User prompt to send to the agent. + pub prompt: String, + /// Optional UUID of an existing conversation to resume. When omitted a + /// fresh conversation is created. + pub conversation_id: Option, + /// Optional per-request model override (e.g. `"claude-opus-4-7"`). Must + /// belong to the agent's authenticated provider or chat will fail. + pub model: Option, +} + +/// Handle to an embedded ForgeAPI instance. +#[napi] +pub struct GraffApi { + inner: Arc, +} + +#[napi] +impl GraffApi { + /// Initialise a ForgeAPI rooted at `cwd`. Reads global config from + /// `~/.forge/forge.toml` (and merges env overrides). The cwd determines + /// workspace-scoped state (conversation history, .forge/ folder, etc). + #[napi(factory)] + pub async fn init(cwd: String) -> Result { + ensure_crypto(); + let cwd = PathBuf::from(cwd); + let config = ForgeConfig::read().map_err(|e| err(format!("ForgeConfig::read: {e:?}")))?; + let api: Arc = Arc::new(ForgeAPI::init(cwd, config)); + Ok(GraffApi { inner: api }) + } + + /// Send a chat request and return a streaming handle. + /// + /// If `conversation_id` is omitted a fresh `Conversation` is created and + /// upserted before the chat begins. Pull events off the returned handle + /// via `next()` until it yields `null` (end of stream), or `cancel()` to + /// abort early. + #[napi] + pub async fn chat(&self, req: ChatRequestJs) -> Result { + let conversation_id = match req.conversation_id { + Some(s) => parse_conv_id(&s)?, + None => { + let conv = Conversation::generate(); + let id = conv.id; + self.inner + .upsert_conversation(conv) + .await + .map_err(map_err)?; + id + } + }; + + let event = Event::new(req.prompt); + let mut chat_req = ChatRequest::new(event, conversation_id); + if let Some(model_str) = req.model { + chat_req.model_override = Some(ModelId::new(model_str)); + } + + let stream = self.inner.chat(chat_req).await.map_err(map_err)?; + + Ok(ChatStreamHandle { + inner: Arc::new(AsyncMutex::new(Some(stream))), + conversation_id: conversation_id.into_string(), + }) + } + + /// List conversations for the active workspace as a JSON array. The TS + /// wrapper parses this into `Conversation[]`. + #[napi] + pub async fn list_conversations(&self, limit: Option) -> Result { + let convs = self + .inner + .get_conversations(limit.map(|n| n as usize)) + .await + .map_err(map_err)?; + to_json(&convs, "conversations") + } + + /// Fetch a single conversation by id, or `null` when absent. + #[napi] + pub async fn get_conversation(&self, id: String) -> Result> { + let conv_id = parse_conv_id(&id)?; + let conv = self.inner.conversation(&conv_id).await.map_err(map_err)?; + match conv { + None => Ok(None), + Some(c) => Ok(Some(to_json(&c, "conversation")?)), + } + } + + /// Most recent conversation for the workspace, or `null` if none yet. + #[napi] + pub async fn last_conversation(&self) -> Result> { + let conv = self.inner.last_conversation().await.map_err(map_err)?; + match conv { + None => Ok(None), + Some(c) => Ok(Some(to_json(&c, "conversation")?)), + } + } + + /// Permanently delete a conversation by id. + #[napi] + pub async fn delete_conversation(&self, id: String) -> Result<()> { + let conv_id = parse_conv_id(&id)?; + self.inner + .delete_conversation(&conv_id) + .await + .map_err(map_err) + } + + /// Set a conversation's title. + #[napi] + pub async fn rename_conversation(&self, id: String, title: String) -> Result<()> { + let conv_id = parse_conv_id(&id)?; + self.inner + .rename_conversation(&conv_id, title) + .await + .map_err(map_err) + } + + /// Compact (summarise) the agent's context for a conversation. Returns a + /// JSON-encoded `CompactionResult`. + #[napi] + pub async fn compact_conversation(&self, id: String) -> Result { + let conv_id = parse_conv_id(&id)?; + let result = self + .inner + .compact_conversation(&conv_id) + .await + .map_err(map_err)?; + to_json(&result, "compaction") + } + + /// Currently-active agent id, or `null` if none has been set. + #[napi] + pub async fn get_active_agent(&self) -> Option { + self.inner + .get_active_agent() + .await + .map(|a| a.as_str().to_string()) + } + + /// Set the active agent for subsequent chat requests. + #[napi] + pub async fn set_active_agent(&self, agent_id: String) -> Result<()> { + self.inner + .set_active_agent(AgentId::new(agent_id)) + .await + .map_err(map_err) + } + + /// Lightweight metadata for all available agents (does not require a + /// configured provider). Returns JSON-encoded `AgentInfo[]`. + #[napi] + pub async fn get_agent_infos(&self) -> Result { + let agents = self.inner.get_agent_infos().await.map_err(map_err)?; + to_json(&agents, "agents") + } + + /// List trajectory events recorded for a conversation. JSON-encoded + /// `TrajectoryEvent[]` (one row per tool call across every agent in the + /// conversation tree). + #[napi] + pub async fn list_trajectory(&self, conversation_id: String) -> Result { + let conv_id = parse_conv_id(&conversation_id)?; + let events = self + .inner + .list_trajectory(&conv_id) + .await + .map_err(map_err)?; + to_json(&events, "trajectory") + } + + /// SDK + crate version string. + #[napi] + pub fn version(&self) -> String { + env!("CARGO_PKG_VERSION").to_string() + } +} + +/// Pull-based handle over the chat event stream. +/// +/// Each call to [`ChatStreamHandle::next`] returns the next event JSON-encoded, +/// or `null` when the stream ends. The TS wrapper turns this into an +/// `AsyncIterable`. Call [`ChatStreamHandle::cancel`] to abort +/// the in-flight chat and free the underlying tokio task. +#[napi] +pub struct ChatStreamHandle { + // Wrapped in Option so cancel() can drop the stream — MpscStream's Drop + // impl closes the receiver and aborts the spawned join handle. + inner: Arc>>>>, + conversation_id: String, +} + +#[napi] +impl ChatStreamHandle { + /// Returns the next event as a JSON string, or `null` when the stream ends + /// (either naturally or because `cancel()` was called). + #[napi] + pub async fn next(&self) -> Result> { + let mut guard = self.inner.lock().await; + let stream = match guard.as_mut() { + None => return Ok(None), + Some(s) => s, + }; + match stream.next().await { + None => { + *guard = None; + Ok(None) + } + Some(Err(e)) => Err(err(format!("agent error: {e:?}"))), + Some(Ok(resp)) => { + let wire = WireEvent::from(resp); + Ok(Some(to_json(&wire, "event")?)) + } + } + } + + /// Cancel the in-flight chat. Drops the underlying stream which aborts + /// the spawned tokio task. Subsequent calls to `next()` return `null`. + #[napi] + pub async fn cancel(&self) { + let mut guard = self.inner.lock().await; + guard.take(); + } + + #[napi(getter)] + pub fn conversation_id(&self) -> String { + self.conversation_id.clone() + } +} + +/// SDK version (top-level convenience). +#[napi] +pub fn version() -> String { + env!("CARGO_PKG_VERSION").to_string() +} + +/// Generate a fresh conversation id (UUID v4 string). +#[napi] +pub fn new_conversation_id() -> String { + ConversationId::generate().into_string() +} diff --git a/sdk/typescript/src/wire.rs b/sdk/typescript/src/wire.rs new file mode 100644 index 00000000..423fa97f --- /dev/null +++ b/sdk/typescript/src/wire.rs @@ -0,0 +1,154 @@ +//! JSON wire format mirroring `forge_domain::ChatResponse`. +//! +//! `ChatResponse` itself is not `Serialize` — and one of its variants holds an +//! `Arc` which can't cross the FFI boundary anyway — so we project it +//! into a tagged enum we control. The `From` impl also fires the +//! tool-execution notify so the SDK behaves like `--print` mode (auto-approve). + +use forge_domain::{ + Category, Cause, ChatResponse, ChatResponseContent, InterruptionReason, TitleFormat, +}; +use serde::Serialize; + +#[derive(Serialize)] +#[serde(tag = "type")] +pub enum WireEvent { + TaskMessage { + content: WireContent, + }, + TaskReasoning { + content: String, + }, + TaskComplete, + ToolCallStart { + tool_call: serde_json::Value, + }, + ToolCallEnd { + result: serde_json::Value, + }, + RetryAttempt { + cause: String, + duration_ms: u64, + }, + Interrupt { + reason: WireInterrupt, + }, +} + +#[derive(Serialize)] +#[serde(tag = "kind")] +pub enum WireContent { + ToolInput { + title: String, + sub_title: Option, + category: WireCategory, + }, + ToolOutput { + text: String, + }, + Markdown { + text: String, + partial: bool, + }, +} + +#[derive(Serialize)] +#[serde(rename_all = "lowercase")] +pub enum WireCategory { + Action, + Info, + Debug, + Error, + Completion, + Warning, +} + +#[derive(Serialize)] +#[serde(tag = "kind")] +pub enum WireInterrupt { + MaxToolFailurePerTurnLimitReached { limit: u64 }, + MaxRequestPerTurnLimitReached { limit: u64 }, +} + +impl From for WireEvent { + fn from(resp: ChatResponse) -> Self { + match resp { + ChatResponse::TaskMessage { content } => { + WireEvent::TaskMessage { content: content.into() } + } + ChatResponse::TaskReasoning { content } => WireEvent::TaskReasoning { content }, + ChatResponse::TaskComplete => WireEvent::TaskComplete, + ChatResponse::ToolCallStart { tool_call, notifier } => { + // The TUI uses `notifier` to gate tool execution behind user + // confirmation. The SDK has no UI to confirm, so we auto-fire + // it before forwarding the event — same behaviour as one-shot + // `graff -p` mode. + notifier.notify_one(); + WireEvent::ToolCallStart { + tool_call: serde_json::to_value(&tool_call) + .unwrap_or(serde_json::Value::Null), + } + } + ChatResponse::ToolCallEnd(result) => WireEvent::ToolCallEnd { + result: serde_json::to_value(&result).unwrap_or(serde_json::Value::Null), + }, + ChatResponse::RetryAttempt { cause, duration } => WireEvent::RetryAttempt { + cause: cause_to_string(&cause), + duration_ms: u64::try_from(duration.as_millis()).unwrap_or(u64::MAX), + }, + ChatResponse::Interrupt { reason } => WireEvent::Interrupt { reason: reason.into() }, + } + } +} + +impl From for WireContent { + fn from(content: ChatResponseContent) -> Self { + match content { + ChatResponseContent::ToolInput(title) => title.into(), + ChatResponseContent::ToolOutput(text) => WireContent::ToolOutput { text }, + ChatResponseContent::Markdown { text, partial } => { + WireContent::Markdown { text, partial } + } + } + } +} + +impl From for WireContent { + fn from(t: TitleFormat) -> Self { + WireContent::ToolInput { + title: t.title, + sub_title: t.sub_title, + category: t.category.into(), + } + } +} + +impl From for WireCategory { + fn from(c: Category) -> Self { + match c { + Category::Action => WireCategory::Action, + Category::Info => WireCategory::Info, + Category::Debug => WireCategory::Debug, + Category::Error => WireCategory::Error, + Category::Completion => WireCategory::Completion, + Category::Warning => WireCategory::Warning, + } + } +} + +impl From for WireInterrupt { + fn from(r: InterruptionReason) -> Self { + match r { + InterruptionReason::MaxToolFailurePerTurnLimitReached { limit, .. } => { + WireInterrupt::MaxToolFailurePerTurnLimitReached { limit } + } + InterruptionReason::MaxRequestPerTurnLimitReached { limit } => { + WireInterrupt::MaxRequestPerTurnLimitReached { limit } + } + } + } +} + +fn cause_to_string(cause: &Cause) -> String { + cause.as_str().to_string() +} From e27000e8407702799f3598ea26a3047bb0e1b37c Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Sun, 10 May 2026 01:05:17 +0800 Subject: [PATCH 02/15] feat(sdk): BYOK auth + ship hand-written JS wrappers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the BYOK entrypoint third-party SDK consumers need: await graff.upsertCredential("openai", "sk-..."); await graff.chat({ prompt: "...", model: "gpt-5.5" }); Routes through the same init_provider_auth + complete_provider_auth flow `graff provider login` uses, so the credential persists in the configured auth store and subsequent chats authenticate without further setup. Optional extra_params support providers that need URL parameters alongside the key (Vertex AI project + location, etc). Also ships removeCredential() — mirrors `graff provider logout`. Fixes a packaging miss: the previous commit's hand-written JS wrappers (lib.js, lib.d.ts) were caught by the top-level *.js / *.d.ts gitignore rules and never made it into git. Adds explicit negating rules so they're tracked, while keeping the napi auto-generated index.js / index.d.ts ignored. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 6 ++ sdk/typescript/lib.d.ts | 159 +++++++++++++++++++++++++++++ sdk/typescript/lib.js | 206 ++++++++++++++++++++++++++++++++++++++ sdk/typescript/src/lib.rs | 67 ++++++++++++- 4 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 sdk/typescript/lib.d.ts create mode 100644 sdk/typescript/lib.js diff --git a/.gitignore b/.gitignore index 8207970e..e36e884b 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,12 @@ jobs/** .mcp.json *.d.ts *.js + +# SDK hand-written JS wrappers — exception to the blanket *.js / *.d.ts rules above. +# (index.js / index.d.ts inside sdk/typescript stay ignored; they're auto-generated +# by napi build, see sdk/typescript/.gitignore.) +!sdk/typescript/lib.js +!sdk/typescript/lib.d.ts *.map **/.forge/request.body.json node_modules/ diff --git a/sdk/typescript/lib.d.ts b/sdk/typescript/lib.d.ts new file mode 100644 index 00000000..51d5048d --- /dev/null +++ b/sdk/typescript/lib.d.ts @@ -0,0 +1,159 @@ +// Public TypeScript types for @codegraff/sdk. +// The native bindings live in ./index.d.ts (auto-generated by napi-rs). + +import { GraffApi, ChatStreamHandle, newConversationId, version } from "./index.js"; + +export { GraffApi, ChatStreamHandle, newConversationId, version }; + +// ── Options ────────────────────────────────────────────────────────────── + +export interface RunAgentOptions { + /** User prompt to send to the agent. */ + prompt: string; + /** Working directory rooted to the codegraff workspace. Defaults to `process.cwd()`. */ + cwd?: string; + /** Optional UUID of an existing conversation to resume. */ + conversationId?: string; + /** Optional per-request model override. Must belong to the agent's authenticated provider. */ + model?: string; +} + +export interface GraffSessionOptions { + cwd?: string; + conversationId?: string; + model?: string; +} + +export interface ChatOptions { + prompt: string; + conversationId?: string; + model?: string; +} + +// ── Event stream ───────────────────────────────────────────────────────── + +export type AgentEventCategory = + | "action" + | "info" + | "debug" + | "error" + | "completion" + | "warning"; + +export type AgentEventContent = + | { kind: "ToolInput"; title: string; sub_title?: string | null; category: AgentEventCategory } + | { kind: "ToolOutput"; text: string } + | { kind: "Markdown"; text: string; partial: boolean }; + +export type AgentEventInterrupt = + | { kind: "MaxToolFailurePerTurnLimitReached"; limit: number } + | { kind: "MaxRequestPerTurnLimitReached"; limit: number }; + +export type AgentEvent = + /** Synthetic event emitted by the SDK (not the Rust core) at the start of + * `runAgent` / `Graff.chat` / `GraffSession.send` so callers can capture + * the conversation id without inspecting the underlying handle. */ + | { type: "ConversationStarted"; conversationId: string } + | { type: "TaskMessage"; content: AgentEventContent } + | { type: "TaskReasoning"; content: string } + | { type: "TaskComplete" } + | { type: "ToolCallStart"; tool_call: ToolCallFull } + | { type: "ToolCallEnd"; result: ToolResult } + | { type: "RetryAttempt"; cause: string; duration_ms: number } + | { type: "Interrupt"; reason: AgentEventInterrupt }; + +// ── Domain types (mirror forge_domain) ─────────────────────────────────── +// +// These shapes are loose on purpose — the underlying Rust structs evolve +// quickly. The fields explicitly listed here are stable; the index signature +// allows access to less-stable fields without a TS error. + +export interface Conversation { + id: string; + title?: string | null; + metadata: { created_at: string; updated_at?: string | null }; + metrics: Record; + context?: Record | null; + [k: string]: unknown; +} + +export interface AgentInfo { + id: string; + [k: string]: unknown; +} + +export interface CompactionResult { + /** Total tokens before compaction. */ + original_tokens?: number; + /** Total tokens after compaction. */ + compacted_tokens?: number; + [k: string]: unknown; +} + +export interface ToolCallFull { + name: string; + call_id?: string | null; + arguments: Record | unknown; + thought_signature?: string | null; +} + +export interface ToolResult { + name: string; + call_id?: string | null; + output: { is_error?: boolean; [k: string]: unknown }; +} + +export interface TrajectoryEvent { + seq: number; + agent_id: string; + conversation_id: string; + [k: string]: unknown; +} + +// ── Top-level API ──────────────────────────────────────────────────────── + +export function runAgent(opts: RunAgentOptions): AsyncGenerator; + +export class GraffSession { + constructor(opts?: GraffSessionOptions); + readonly conversationId: string | undefined; + send(prompt: string): AsyncGenerator; + close(): Promise; +} + +export class Graff { + /** Initialise a long-lived Graff rooted at `cwd`. */ + static init(cwd?: string): Promise; + + /** Run a single chat turn. Same event stream as `runAgent` but reuses this Graff's GraffApi. */ + chat(opts: ChatOptions): AsyncGenerator; + + /** Create a multi-turn session that shares this Graff's underlying GraffApi. */ + session(opts?: GraffSessionOptions): GraffSession; + + // Auth (BYOK) + upsertCredential( + providerId: string, + apiKey: string, + extraParams?: Array<[string, string]> | null, + ): Promise; + removeCredential(providerId: string): Promise; + + // Conversation management + listConversations(limit?: number): Promise; + getConversation(id: string): Promise; + lastConversation(): Promise; + deleteConversation(id: string): Promise; + renameConversation(id: string, title: string): Promise; + compactConversation(id: string): Promise; + + // Agents + getActiveAgent(): Promise; + setActiveAgent(agentId: string): Promise; + getAgentInfos(): Promise; + + // Observability + listTrajectory(conversationId: string): Promise; + + version(): string; +} diff --git a/sdk/typescript/lib.js b/sdk/typescript/lib.js new file mode 100644 index 00000000..cd113cd1 --- /dev/null +++ b/sdk/typescript/lib.js @@ -0,0 +1,206 @@ +// Public surface for @codegraff/sdk. +// +// `index.js` and the platform .node addon are produced by napi-rs at build +// time and expose the raw building blocks (GraffApi, ChatStreamHandle, +// version(), newConversationId()). This module wraps them in three ergonomic +// shapes: +// +// - runAgent(opts) => AsyncIterable (one-shot) +// - new GraffSession(opts) => session.send() returns AsyncIterable +// (multi-turn, persists conversationId) +// - Graff.init(cwd) => long-lived instance with the conversation +// management surface (list / get / delete / +// rename / compact / agents / trajectory). + +const native = require("./index.js"); + +const { GraffApi, ChatStreamHandle, version, newConversationId } = native; + +/** Pull events off a ChatStreamHandle and yield decoded objects. Calls + * cancel() on `return()` (e.g. when the caller breaks out of `for await`) + * so the underlying tokio task is aborted. */ +async function* iterateHandle(handle) { + try { + while (true) { + const raw = await handle.next(); + if (raw == null) return; + yield JSON.parse(raw); + } + } finally { + await handle.cancel(); + } +} + +/** Run a single chat turn against the codegraff agent. Spins up a fresh + * GraffApi rooted at `opts.cwd` per call. To preserve state across calls, + * reuse a Graff or GraffSession instance instead. */ +async function* runAgent(opts) { + if (!opts || typeof opts.prompt !== "string") { + throw new TypeError("runAgent: { prompt: string } is required"); + } + const api = await GraffApi.init(opts.cwd ?? process.cwd()); + const handle = await api.chat({ + prompt: opts.prompt, + conversationId: opts.conversationId, + model: opts.model, + }); + yield { type: "ConversationStarted", conversationId: handle.conversationId }; + yield* iterateHandle(handle); +} + +/** Multi-turn session. Reuses one GraffApi instance and the same + * conversationId across `.send()` calls so the agent retains memory. */ +class GraffSession { + constructor(opts = {}) { + this._opts = opts; + this._cwd = opts.cwd ?? process.cwd(); + this._conversationId = opts.conversationId; + // Internal: when the session is created via `Graff.session()`, the + // existing GraffApi is passed in to avoid double-initialising the + // workspace. Treat as private; not part of the public TS type. + this._apiPromise = opts._api ? Promise.resolve(opts._api) : null; + } + + get conversationId() { + return this._conversationId; + } + + _api() { + if (!this._apiPromise) { + this._apiPromise = GraffApi.init(this._cwd); + } + return this._apiPromise; + } + + async *send(prompt) { + if (typeof prompt !== "string") { + throw new TypeError("GraffSession.send: prompt must be a string"); + } + const api = await this._api(); + const handle = await api.chat({ + prompt, + conversationId: this._conversationId, + model: this._opts.model, + }); + if (!this._conversationId) { + this._conversationId = handle.conversationId; + } + yield* iterateHandle(handle); + } + + async close() { + this._apiPromise = null; + } +} + +/** Long-lived Graff instance. Wraps a single GraffApi and exposes the + * conversation / agent management surface with parsed return values. */ +class Graff { + constructor(api) { + this._api = api; + } + + static async init(cwd = process.cwd()) { + return new Graff(await GraffApi.init(cwd)); + } + + /** Run a chat turn. Mirrors `runAgent` but reuses this Graff's GraffApi. */ + async *chat(opts) { + if (!opts || typeof opts.prompt !== "string") { + throw new TypeError("Graff.chat: { prompt: string } is required"); + } + const handle = await this._api.chat({ + prompt: opts.prompt, + conversationId: opts.conversationId, + model: opts.model, + }); + yield { type: "ConversationStarted", conversationId: handle.conversationId }; + yield* iterateHandle(handle); + } + + /** Build a multi-turn session that shares this Graff's underlying GraffApi. */ + session(opts = {}) { + return new GraffSession({ ...opts, _api: this._api }); + } + + // ── Auth (BYOK) ────────────────────────────────────────────────────────── + + /** Upsert an API key credential for a provider. After this returns, any + * subsequent `chat()` whose model belongs to this provider authenticates + * with the supplied key. `extraParams` is an optional list of [name, + * value] pairs for providers that need URL parameters alongside the + * key (e.g. Vertex AI's project + location). */ + upsertCredential(providerId, apiKey, extraParams) { + return this._api.upsertCredential(providerId, apiKey, extraParams); + } + + /** Remove a provider's credential. Mirrors `graff provider logout`. */ + removeCredential(providerId) { + return this._api.removeCredential(providerId); + } + + // ── Conversation management ───────────────────────────────────────────── + + async listConversations(limit) { + return JSON.parse(await this._api.listConversations(limit)); + } + + async getConversation(id) { + const j = await this._api.getConversation(id); + return j == null ? null : JSON.parse(j); + } + + async lastConversation() { + const j = await this._api.lastConversation(); + return j == null ? null : JSON.parse(j); + } + + deleteConversation(id) { + return this._api.deleteConversation(id); + } + + renameConversation(id, title) { + return this._api.renameConversation(id, title); + } + + async compactConversation(id) { + return JSON.parse(await this._api.compactConversation(id)); + } + + // ── Agents ────────────────────────────────────────────────────────────── + + getActiveAgent() { + return this._api.getActiveAgent(); + } + + setActiveAgent(agentId) { + return this._api.setActiveAgent(agentId); + } + + async getAgentInfos() { + return JSON.parse(await this._api.getAgentInfos()); + } + + // ── Trajectory ────────────────────────────────────────────────────────── + + async listTrajectory(conversationId) { + return JSON.parse(await this._api.listTrajectory(conversationId)); + } + + version() { + return this._api.version(); + } +} + +module.exports = { + // High-level + Graff, + GraffSession, + runAgent, + // Helpers + newConversationId, + version, + // Low-level passthroughs (rarely needed) + GraffApi, + ChatStreamHandle, +}; diff --git a/sdk/typescript/src/lib.rs b/sdk/typescript/src/lib.rs index fbb7148a..2eb6323f 100644 --- a/sdk/typescript/src/lib.rs +++ b/sdk/typescript/src/lib.rs @@ -5,13 +5,18 @@ //! Methods that return data structures emit JSON strings; the `Graff` JS //! wrapper parses them so callers get typed objects. +use std::collections::HashMap; use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, OnceLock}; use forge_api::{API, ChatRequest, ForgeAPI}; use forge_config::ForgeConfig; -use forge_domain::{AgentId, ChatResponse, Conversation, ConversationId, Event, ModelId}; +use forge_domain::{ + AgentId, ApiKey, ApiKeyResponse, AuthContext, AuthContextRequest, AuthContextResponse, + AuthMethod, ChatResponse, Conversation, ConversationId, Event, ModelId, ProviderId, URLParam, + URLParamValue, +}; use forge_stream::MpscStream; use futures::StreamExt; use napi::bindgen_prelude::*; @@ -79,6 +84,66 @@ impl GraffApi { Ok(GraffApi { inner: api }) } + /// Set or replace the API key credential for a provider — the BYOK entrypoint. + /// + /// `provider_id` is the snake_case provider name (e.g. "openai", + /// "anthropic", "open_router", "xai", "cerebras", "github_copilot"). + /// `extra_params` is an optional list of `[name, value]` pairs for + /// providers that need URL parameters alongside the key (e.g. + /// Vertex AI's project + location). Most providers can pass `None`. + /// + /// Routes through the same auth flow `graff provider login` uses, + /// so credentials persist in the configured auth store and the next + /// `chat()` whose `model` belongs to this provider authenticates + /// without further setup. + #[napi] + pub async fn upsert_credential( + &self, + provider_id: String, + api_key: String, + extra_params: Option>>, + ) -> Result<()> { + let id: ProviderId = provider_id.into(); + let init = self + .inner + .init_provider_auth(id.clone(), AuthMethod::ApiKey) + .await + .map_err(map_err)?; + let request = match init { + AuthContextRequest::ApiKey(req) => req, + _ => return Err(err(format!( + "provider {id} does not use ApiKey auth" + ))), + }; + let mut url_params: HashMap = HashMap::new(); + if let Some(pairs) = extra_params { + for pair in pairs { + if pair.len() != 2 { + return Err(err(format!( + "extra_params entries must be [name, value] pairs; got {} fields", + pair.len() + ))); + } + url_params.insert(URLParam::from(pair[0].clone()), URLParamValue::from(pair[1].clone())); + } + } + let response = ApiKeyResponse { api_key: ApiKey::from(api_key), url_params }; + let context = AuthContextResponse::ApiKey(AuthContext { request, response }); + self.inner + .complete_provider_auth(id, context, std::time::Duration::from_secs(30)) + .await + .map_err(map_err)?; + Ok(()) + } + + /// Remove a provider's credential. Mirrors `graff provider logout`. + #[napi] + pub async fn remove_credential(&self, provider_id: String) -> Result<()> { + let id: ProviderId = provider_id.into(); + self.inner.remove_provider(&id).await.map_err(map_err)?; + Ok(()) + } + /// Send a chat request and return a streaming handle. /// /// If `conversation_id` is omitted a fresh `Conversation` is created and From 65bbc02bed42687829a4ac87190da74c07e2b6c4 Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Sun, 10 May 2026 01:48:13 +0800 Subject: [PATCH 03/15] feat(sdk): one-call BYOK via Graff.init({ provider, apiKey, model, maxTokens }) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folds the three sharp edges from the previous BYOK commit into a single init call. Today's working setup goes from this: process.env.FORGE_SESSION__PROVIDER_ID = "openai"; process.env.FORGE_SESSION__MODEL_ID = "gpt-4o-mini"; process.env.FORGE_MAX_TOKENS = "8000"; const g = await Graff.init(cwd); await g.upsertCredential("openai", key); To this: const g = await Graff.init({ cwd, provider: "openai", apiKey: key, model: "gpt-4o-mini", maxTokens: 8000, }); Implementation is JS-only — the new options translate to FORGE_* env vars before the Rust init reads ForgeConfig, then upsertCredential is called automatically when apiKey + provider are both supplied. No Rust patch needed. The legacy `Graff.init(cwd)` string signature still works (overload), so existing call sites are untouched. When neither provider nor apiKey is supplied, the SDK falls back to `~/.forge/forge.toml` exactly as before — graff CLI users see no behavior change. Verified end-to-end against api.openai.com with a real key: - Graff.init({...BYOK...}) → 3ms - chat({prompt: "PONG"}) → 1237ms, answer "PONG" Co-Authored-By: Claude Opus 4.7 (1M context) --- sdk/typescript/lib.d.ts | 25 ++++++++++++++++++++++++- sdk/typescript/lib.js | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/sdk/typescript/lib.d.ts b/sdk/typescript/lib.d.ts index 51d5048d..80af2675 100644 --- a/sdk/typescript/lib.d.ts +++ b/sdk/typescript/lib.d.ts @@ -30,6 +30,22 @@ export interface ChatOptions { model?: string; } +export interface GraffInitOptions { + /** Workspace root. Defaults to `process.cwd()`. */ + cwd?: string; + /** Provider id (snake_case) — e.g. "openai", "anthropic", "open_router". */ + provider?: string; + /** API key. Requires `provider`. The SDK calls `upsertCredential` after init. */ + apiKey?: string; + /** Optional URL parameters for providers that need them (Vertex AI etc). */ + extraParams?: Array<[string, string]>; + /** Pin the session model. Equivalent to `FORGE_SESSION__MODEL_ID`. */ + model?: string; + /** Override the default 20480 max-tokens cap. Required for providers + * with smaller completion limits (gpt-4o-mini caps at 16384). */ + maxTokens?: number; +} + // ── Event stream ───────────────────────────────────────────────────────── export type AgentEventCategory = @@ -123,7 +139,14 @@ export class GraffSession { export class Graff { /** Initialise a long-lived Graff rooted at `cwd`. */ - static init(cwd?: string): Promise; + /** Initialise a long-lived Graff. Pass a `cwd` string for legacy behavior, + * or an options object to also register a BYOK credential and pin the + * session provider/model in one call. + * + * When neither `provider` nor `apiKey` is set, the SDK falls back to + * `~/.forge/forge.toml` (or the bundled defaults). Existing call sites + * passing just a string still work. */ + static init(arg?: string | GraffInitOptions): Promise; /** Run a single chat turn. Same event stream as `runAgent` but reuses this Graff's GraffApi. */ chat(opts: ChatOptions): AsyncGenerator; diff --git a/sdk/typescript/lib.js b/sdk/typescript/lib.js index cd113cd1..07770ecb 100644 --- a/sdk/typescript/lib.js +++ b/sdk/typescript/lib.js @@ -100,8 +100,43 @@ class Graff { this._api = api; } - static async init(cwd = process.cwd()) { - return new Graff(await GraffApi.init(cwd)); + /** Initialise a long-lived Graff. Accepts either a string `cwd` (legacy) + * or an options object: + * + * Graff.init({ + * cwd: "/path/to/workspace", // default: process.cwd() + * provider: "openai", // optional — sets session provider + * apiKey: process.env.OPENAI_API_KEY, // optional — calls upsertCredential + * model: "gpt-4o-mini", // optional — sets session model + * maxTokens: 8000, // optional — overrides 20480 default + * }) + * + * When `apiKey` is supplied without `provider`, init throws — there's no way + * to know which provider to bind the key to. When neither is supplied, the + * SDK falls back to whatever `~/.forge/forge.toml` provides (or the bundled + * defaults), so existing call sites keep working. + * + * Each option that's set translates to the equivalent FORGE_* env var BEFORE + * the Rust init reads config, so they win over file-level config without + * needing a Rust patch. */ + static async init(arg) { + const opts = typeof arg === "string" ? { cwd: arg } : (arg || {}); + if (opts.apiKey && !opts.provider) { + throw new TypeError("Graff.init: `provider` is required when `apiKey` is supplied"); + } + // Translate options → env-var overrides. These are consumed by ForgeConfig + // when GraffApi.init runs below. Setting them on process.env is safe: they + // describe this Graff's session config, and any later Graff.init in the + // same process can override them again. + if (opts.provider) process.env.FORGE_SESSION__PROVIDER_ID = opts.provider; + if (opts.model) process.env.FORGE_SESSION__MODEL_ID = opts.model; + if (opts.maxTokens != null) process.env.FORGE_MAX_TOKENS = String(opts.maxTokens); + const cwd = opts.cwd ?? process.cwd(); + const graff = new Graff(await GraffApi.init(cwd)); + if (opts.apiKey) { + await graff.upsertCredential(opts.provider, opts.apiKey, opts.extraParams); + } + return graff; } /** Run a chat turn. Mirrors `runAgent` but reuses this Graff's GraffApi. */ From 8c1530e29968ec382b4ef105d5cdf99043a007c7 Mon Sep 17 00:00:00 2001 From: Rach <54503978+justrach@users.noreply.github.com> Date: Sun, 10 May 2026 13:33:56 +0800 Subject: [PATCH 04/15] fix(ci): backport pixo Linux coverage build fix to release/0.1.53 Backport the pixo Linux coverage build fix and generated snapshot updates to release/0.1.53 without pulling main-only cleanup changes into the release branch.\n\nCo-Authored-By: blackfloofie-a codegraff agent <265516171+blackfloofie@users.noreply.github.com> --- Cargo.toml | 2 +- ...l_registry__all_rendered_tool_descriptions.snap | 1 + crates/forge_app/src/tool_registry.rs | 2 +- crates/forge_app/src/utils.rs | 8 ++++---- ...ools__definition__usage__tests__tool_usage.snap | 2 +- ..._tests__openai_responses_all_catalog_tools.snap | 14 +++++++++++++- 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ca2a6c74..737473ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ mockito = "1.7.2" nom = "8.0.0" nu-ansi-term = "0.50.1" posthog-rs = "0.5.3" -pixo = { version = "0.4.1", default-features = false } +pixo = { version = "0.4.1", default-features = false, features = ["simd"] } pretty_assertions = "1.4.1" proc-macro2 = "1.0" quote = "1.0" diff --git a/crates/forge_app/src/snapshots/forge_app__tool_registry__all_rendered_tool_descriptions.snap b/crates/forge_app/src/snapshots/forge_app__tool_registry__all_rendered_tool_descriptions.snap index c3bebe41..9ff97b35 100644 --- a/crates/forge_app/src/snapshots/forge_app__tool_registry__all_rendered_tool_descriptions.snap +++ b/crates/forge_app/src/snapshots/forge_app__tool_registry__all_rendered_tool_descriptions.snap @@ -475,6 +475,7 @@ Usage notes: - Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent - If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement. - If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple task tool use content blocks. For example, if you need to launch both a build-validator agent and a test-runner agent in parallel, send a single message with both tool calls. +- Optional `model` field: when the user explicitly asks for a specific model for the subagent (e.g. "spawn a subagent with gpt-5.4-medium" or "use sonnet-4.6 for this delegation"), set `model` to the requested model id. Otherwise OMIT it and let the agent use its default. The override is rejected if the model isn't on the agent's currently-authenticated provider. Example usage: diff --git a/crates/forge_app/src/tool_registry.rs b/crates/forge_app/src/tool_registry.rs index 0b1b42f3..4a9e0d8a 100644 --- a/crates/forge_app/src/tool_registry.rs +++ b/crates/forge_app/src/tool_registry.rs @@ -113,7 +113,7 @@ impl> ToolReg let model_override = task_input .model .as_deref() - .map(|m| forge_domain::ModelId::new(m)); + .map(forge_domain::ModelId::new); // Parse session_id into ConversationId if present let conversation_id = session_id .map(|id| forge_domain::ConversationId::parse(&id)) diff --git a/crates/forge_app/src/utils.rs b/crates/forge_app/src/utils.rs index c52482f5..0cc8fed9 100644 --- a/crates/forge_app/src/utils.rs +++ b/crates/forge_app/src/utils.rs @@ -358,10 +358,10 @@ fn normalize_additional_properties( pub fn rewrite_one_of_to_any_of(value: &mut serde_json::Value) { match value { serde_json::Value::Object(map) => { - if let Some(branches) = map.remove("oneOf") { - if !map.contains_key("anyOf") { - map.insert("anyOf".to_string(), branches); - } + if let Some(branches) = map.remove("oneOf") + && !map.contains_key("anyOf") + { + map.insert("anyOf".to_string(), branches); } for v in map.values_mut() { rewrite_one_of_to_any_of(v); diff --git a/crates/forge_domain/src/tools/definition/snapshots/forge_domain__tools__definition__usage__tests__tool_usage.snap b/crates/forge_domain/src/tools/definition/snapshots/forge_domain__tools__definition__usage__tests__tool_usage.snap index 9192079f..5c20b9c8 100644 --- a/crates/forge_domain/src/tools/definition/snapshots/forge_domain__tools__definition__usage__tests__tool_usage.snap +++ b/crates/forge_domain/src/tools/definition/snapshots/forge_domain__tools__definition__usage__tests__tool_usage.snap @@ -17,4 +17,4 @@ expression: prompt {"name":"skill","description":"Fetches detailed information about a specific skill. Use this tool to load skill content and instructions when you need to understand how to perform a specialized task. Skills provide domain-specific knowledge, workflows, and best practices. Only invoke skills that are listed in the available skills section. Do not invoke a skill that is already active.","arguments":{"name":{"description":"The name of the skill to fetch (e.g., \"pdf\", \"code_review\")","type":"string","is_required":true}}} {"name":"todo_write","description":"Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.\nIt also helps the user understand the progress of the task and overall progress of their requests.\n\n## How It Works\n\nEach call sends only the items that changed — you do not need to repeat the whole list.\n\nEach item has two required fields:\n- `content`: The task description. This is the **unique key** — the server matches on content to decide whether to add or update.\n- `status`: One of `pending`, `in_progress`, `completed`, or `cancelled`.\n\n**Rules:**\n- Item with this `content` does **not** exist yet → **added** as a new task.\n- Item with this `content` already exists → its `status` is **updated**.\n- `status: cancelled` → the item is **removed** from the list entirely.\n- Items you do not mention are **left unchanged**.\n\nIDs are managed internally by the system and are never exposed to you.\n\n## When to Use This Tool\nUse this tool proactively in these scenarios:\n\n1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions\n2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations\n3. User explicitly requests todo list - When the user directly asks you to use the todo list\n4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)\n5. After receiving new instructions - Immediately capture user requirements as todos\n6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time\n7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation\n\n## When NOT to Use This Tool\n\nSkip using this tool when:\n1. There is only a single, straightforward task\n2. The task is trivial and tracking it provides no organizational benefit\n3. The task can be completed in less than 3 trivial steps\n4. The task is purely conversational or informational\n\nNOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.\n\n## Examples of When to Use the Todo List\n\n\nUser: I want to add a dark mode toggle to the application settings. Make sure you run the tests and build when you're done!\nAssistant: I'll help add a dark mode toggle to your application settings. Let me create a todo list to track this implementation.\n*Creates todo list with the following items:*\n1. Creating dark mode toggle component in Settings page\n2. Adding dark mode state management (context/store)\n3. Implementing CSS-in-JS styles for dark theme\n4. Updating existing components to support theme switching\n5. Running tests and build process, addressing any failures or errors that occur\n*Begins working on the first task*\n\n\nThe assistant used the todo list because:\n1. Adding dark mode is a multi-step feature requiring UI, state management, and styling changes\n2. The user explicitly requested tests and build be run afterward\n3. The assistant inferred that tests and build need to pass by adding \"Ensure tests and build succeed\" as the final task\n\n\n\n\nUser: Help me rename the function getCwd to getCurrentWorkingDirectory across my project\nAssistant: Let me first search through your codebase to find all occurrences of 'getCwd'.\n*Uses grep or search tools to locate all instances of getCwd in the codebase*\nAssistant: I've found 15 instances of 'getCwd' across 8 different files. Let me create a todo list to track these changes.\n*Creates todo list with specific items for each file that needs updating*\n\n\nThe assistant used the todo list because:\n1. First, the assistant searched to understand the scope of the task\n2. Upon finding multiple occurrences across different files, it determined this was a complex task with multiple steps\n3. The todo list helps ensure every instance is tracked and updated systematically\n4. This approach prevents missing any occurrences and maintains code consistency\n\n\n\n\n\nUser: I need to implement these features for my e-commerce site: user registration, product catalog, shopping cart, and checkout flow.\nAssistant: I'll help implement these features. First, let's add all the features to the todo list.\n*Creates a todo list breaking down each feature into specific tasks based on the project architecture*\nAssistant: Let's start with user registration. This will involve creating database models, API endpoints, and frontend forms that integrate with your existing authentication system.\n\n\nThe assistant used the todo list because:\n1. The user provided multiple complex features to implement in a comma separated list\n2. The todo list helps organize these large features into manageable tasks\n3. This approach allows for tracking progress across the entire implementation\n\n\n\n\nUser: Can you help optimize my React application? It's rendering slowly and has performance issues.\nAssistant: I'll help optimize your React application. First, let me examine your codebase to identify potential performance bottlenecks.\n*Reviews component structure, render patterns, state management, and data fetching*\nAssistant: After analyzing your codebase, I've identified several performance issues. Let me create a todo list to track our optimization efforts.\n*Creates todo list with items like: 1) Implementing memoization for expensive calculations in ProductList, 2) Adding virtualization for long lists in Dashboard, 3) Optimizing image loading in Gallery component, 4) Fixing state update loops in ShoppingCart, 5) Reviewing bundle size and implementing code splitting*\nLet's start by implementing memoization for the expensive calculations in your ProductList component.\n\n\nThe assistant used the todo list because:\n1. First, the assistant examined the codebase to identify specific performance issues\n2. Based on this analysis, it identified multiple optimization opportunities\n3. Performance optimization is a non-trivial task requiring multiple steps\n4. The todo list helps methodically track improvements across different components\n5. This systematic approach ensures all performance bottlenecks are addressed\n\n\n\n## Examples of When NOT to Use the Todo List\n\n\nUser: How do I print 'Hello World' in Python?\nAssistant: In Python, you can print \"Hello World\" with this simple code:\n\npython\nprint(\"Hello World\")\n\nThis will output the text \"Hello World\" to the console when executed.\n\n\nThe assistant did not use the todo list because this is a single, trivial task that can be completed in one step. There's no need to track multiple tasks or steps for such a straightforward request.\n\n\n\n\nUser: What does the git status command do?\nAssistant: The git status command shows the current state of your working directory and staging area. It displays which changes have been staged, which haven't, and which files aren't being tracked by Git. This is a fundamental command for understanding the current state of your repository before making commits.\n\n\nThe assistant did not use the todo list because this is an informational request with no actual coding task to complete. The user is simply asking for an explanation, not for the assistant to perform multiple steps or tasks.\n\n\n\n\nUser: Can you add a comment to the calculateTotal function to explain what it does?\nAssistant: Sure, let me add a comment to the calculateTotal function to explain what it does.\n* Uses the patch tool to add a comment to the calculateTotal function *\n\n\nThe assistant did not use the todo list because this is a single, straightforward task confined to one location in the code. Adding a comment doesn't require tracking multiple steps or systematic organization.\n\n\n\n\nUser: Run npm install for me and tell me what happens.\nAssistant: I'll run the npm install command for you.\n\n*Executes: npm install*\n\nThe command completed successfully. Here's the output:\n[Output of npm install command]\n\nAll dependencies have been installed according to your package.json file.\n\n\nThe assistant did not use the todo list because this is a single command execution with immediate results. There are no multiple steps to track or organize, making the todo list unnecessary for this straightforward task.\n\n\n\n## Task States and Management\n\n1. **Task States**: Use these states to track progress:\n - `pending`: Task not yet started\n - `in_progress`: Currently working on (limit to ONE task at a time)\n - `completed`: Task finished successfully\n - `cancelled`: Task is no longer relevant — this removes it from the list\n\n2. **Task Management**:\n - Only send the items that changed — do not repeat unchanged items\n - Mark tasks `in_progress` BEFORE beginning work\n - Mark tasks `completed` IMMEDIATELY after finishing (don't batch completions)\n - Exactly ONE task must be `in_progress` at any time\n - Use `cancelled` to remove tasks that are no longer relevant\n - Complete current tasks before starting new ones\n\n3. **Task Completion Requirements**:\n - ONLY mark a task as `completed` when you have FULLY accomplished it\n - If you encounter errors, blockers, or cannot finish, keep the task as `in_progress`\n - When blocked, create a new task describing what needs to be resolved\n - Never mark a task as `completed` if:\n - Tests are failing\n - Implementation is partial\n - You encountered unresolved errors\n - You couldn't find necessary files or dependencies\n\n4. **Task Breakdown**:\n - Create specific, actionable items\n - Break complex tasks into smaller, manageable steps\n - Use clear, descriptive task names\n\nWhen in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.","arguments":{"todos":{"description":"List of todo items to create or update. Each item must have `content`\nand `status`. The server matches on `content` — if an item with the\nsame content exists it is updated; otherwise a new item is added.\nSet `status` to `cancelled` to remove an item.","type":"array","is_required":true}}} {"name":"todo_read","description":"Retrieves the current todo list for this coding session. Use this tool to check existing todos before making updates, or to review the current state of tasks at any point during the session.\n\n## When to Use This Tool\n\n- Before calling `todo_write`, to understand which tasks already exist and avoid duplicates\n- When you need to know what tasks are pending, in progress, or completed\n- To resume work after a break and understand the current state of tasks\n- When the user asks about the current task list or progress\n\n## Output\n\nReturns all current todos with their IDs, content, and status (`pending`, `in_progress`, `completed`). If no todos exist yet, returns an empty list.","arguments":{}} -{"name":"task","description":"Launch a new agent to handle complex, multi-step tasks autonomously. \n\nThe {{tool_names.task}} tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.\n\nAvailable agent types and the tools they have access to:\n{{#each agents}}\n- **{{id}}**{{#if description}}: {{description}}{{/if}}{{#if tools}}\n - Tools: {{#each tools}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}{{/if}}\n{{/each}}\n\nWhen using the {{tool_names.task}} tool, you must specify a agent_id parameter to select which agent type to use.\n\nWhen NOT to use the {{tool_names.task}} tool:\n- If you want to read a specific file path, use the {{tool_names.read}} or {{tool_names.fs_search}} tool instead of the {{tool_names.task}} tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the {{tool_names.fs_search}} tool instead, to find the match more quickly\n- If you are searching for code within a specific file or set of 2-3 files, use the {{tool_names.read}} tool instead of the {{tool_names.task}} tool, to find the match more quickly\n- Other tasks that are not related to the agent descriptions above\n\n\nUsage notes:\n- Always include a short description (3-5 words) summarizing what the agent will do\n- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n- Agents can be resumed using the \\`session_id\\` parameter by passing the agent ID from a previous invocation. When resumed, the agent continues with its full previous context preserved. When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.\n- When the agent is done, it will return a single message back to you along with its agent ID. You can use this ID to resume the agent later if needed for follow-up work.\n- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.\n- Agents with \"access to current context\" can see the full conversation history before the tool call. When using these agents, you can write concise prompts that reference earlier context (e.g., \"investigate the error discussed above\") instead of repeating information. The agent will receive all prior messages and understand the context.\n- The agent's outputs should generally be trusted\n- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent\n- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.\n- If the user specifies that they want you to run agents \"in parallel\", you MUST send a single message with multiple {{tool_names.task}} tool use content blocks. For example, if you need to launch both a build-validator agent and a test-runner agent in parallel, send a single message with both tool calls.\n\nExample usage:\n\n\n\"test-runner\": use this agent after you are done writing code to run tests\n\"greeting-responder\": use this agent when to respond to user greetings with a friendly joke\n\n\n\nuser: \"Please write a function that checks if a number is prime\"\nassistant: Sure let me write a function that checks if a number is prime\nassistant: First let me use the {{tool_names.write}} tool to write a function that checks if a number is prime\nassistant: I'm going to use the {{tool_names.write}} tool to write the following code:\n\nfunction isPrime(n) {\n if (n <= 1) return false\n for (let i = 2; i * i <= n; i++) {\n if (n % i === 0) return false\n }\n return true\n}\n\n\nSince a significant piece of code was written and the task was completed, now use the test-runner agent to run the tests\n\nassistant: Now let me use the test-runner agent to run the tests\nassistant: Uses the {{tool_names.task}} tool to launch the test-runner agent\n\n\n\nuser: \"Hello\"\n\nSince the user is greeting, use the greeting-responder agent to respond with a friendly joke\n\nassistant: \"I'm going to use the {{tool_names.task}} tool to launch the greeting-responder agent\"\n","arguments":{"agent_id":{"description":"The ID of the specialized agent to delegate to (e.g., \"forge\", \"muse\",\n\"sage\")","type":"string","is_required":true},"session_id":{"description":"Optional session ID to continue an existing agent session. If not\nprovided, a new stateless session will be created. Use this to\nmaintain context across multiple task invocations with the same\nagent.","type":"string","is_required":false},"tasks":{"description":"A list of clear and detailed descriptions of the tasks to be performed\nby the agent in parallel. Provide sufficient context and specific\nrequirements to enable the agent to understand and execute the work\naccurately.","type":"array","is_required":true}}} +{"name":"task","description":"Launch a new agent to handle complex, multi-step tasks autonomously. \n\nThe {{tool_names.task}} tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.\n\nAvailable agent types and the tools they have access to:\n{{#each agents}}\n- **{{id}}**{{#if description}}: {{description}}{{/if}}{{#if tools}}\n - Tools: {{#each tools}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}{{/if}}\n{{/each}}\n\nWhen using the {{tool_names.task}} tool, you must specify a agent_id parameter to select which agent type to use.\n\nWhen NOT to use the {{tool_names.task}} tool:\n- If you want to read a specific file path, use the {{tool_names.read}} or {{tool_names.fs_search}} tool instead of the {{tool_names.task}} tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the {{tool_names.fs_search}} tool instead, to find the match more quickly\n- If you are searching for code within a specific file or set of 2-3 files, use the {{tool_names.read}} tool instead of the {{tool_names.task}} tool, to find the match more quickly\n- Other tasks that are not related to the agent descriptions above\n\n\nUsage notes:\n- Always include a short description (3-5 words) summarizing what the agent will do\n- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n- Agents can be resumed using the \\`session_id\\` parameter by passing the agent ID from a previous invocation. When resumed, the agent continues with its full previous context preserved. When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.\n- When the agent is done, it will return a single message back to you along with its agent ID. You can use this ID to resume the agent later if needed for follow-up work.\n- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.\n- Agents with \"access to current context\" can see the full conversation history before the tool call. When using these agents, you can write concise prompts that reference earlier context (e.g., \"investigate the error discussed above\") instead of repeating information. The agent will receive all prior messages and understand the context.\n- The agent's outputs should generally be trusted\n- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent\n- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.\n- If the user specifies that they want you to run agents \"in parallel\", you MUST send a single message with multiple {{tool_names.task}} tool use content blocks. For example, if you need to launch both a build-validator agent and a test-runner agent in parallel, send a single message with both tool calls.\n- Optional `model` field: when the user explicitly asks for a specific model for the subagent (e.g. \"spawn a subagent with gpt-5.4-medium\" or \"use sonnet-4.6 for this delegation\"), set `model` to the requested model id. Otherwise OMIT it and let the agent use its default. The override is rejected if the model isn't on the agent's currently-authenticated provider.\n\nExample usage:\n\n\n\"test-runner\": use this agent after you are done writing code to run tests\n\"greeting-responder\": use this agent when to respond to user greetings with a friendly joke\n\n\n\nuser: \"Please write a function that checks if a number is prime\"\nassistant: Sure let me write a function that checks if a number is prime\nassistant: First let me use the {{tool_names.write}} tool to write a function that checks if a number is prime\nassistant: I'm going to use the {{tool_names.write}} tool to write the following code:\n\nfunction isPrime(n) {\n if (n <= 1) return false\n for (let i = 2; i * i <= n; i++) {\n if (n % i === 0) return false\n }\n return true\n}\n\n\nSince a significant piece of code was written and the task was completed, now use the test-runner agent to run the tests\n\nassistant: Now let me use the test-runner agent to run the tests\nassistant: Uses the {{tool_names.task}} tool to launch the test-runner agent\n\n\n\nuser: \"Hello\"\n\nSince the user is greeting, use the greeting-responder agent to respond with a friendly joke\n\nassistant: \"I'm going to use the {{tool_names.task}} tool to launch the greeting-responder agent\"\n","arguments":{"agent_id":{"description":"The ID of the specialized agent to delegate to (e.g., \"forge\", \"muse\",\n\"sage\")","type":"string","is_required":true},"model":{"description":"Optional model override for this subagent run. When set, the spawned\nagent will use this model id instead of its configured default. The\nmodel must belong to a provider the user has already authenticated\nwith — unauthenticated models are rejected. Use this when the user\nexplicitly requests a specific model for a subagent (e.g. \"spawn a\nsubagent with gpt-5.4-medium to do X\").","type":"string","is_required":false},"session_id":{"description":"Optional session ID to continue an existing agent session. If not\nprovided, a new stateless session will be created. Use this to\nmaintain context across multiple task invocations with the same\nagent.","type":"string","is_required":false},"tasks":{"description":"A list of clear and detailed descriptions of the tasks to be performed\nby the agent in parallel. Provide sufficient context and specific\nrequirements to enable the agent to understand and execute the work\naccurately.","type":"array","is_required":true}}} diff --git a/crates/forge_repo/src/provider/openai_responses/snapshots/forge_repo__provider__openai_responses__request__tests__openai_responses_all_catalog_tools.snap b/crates/forge_repo/src/provider/openai_responses/snapshots/forge_repo__provider__openai_responses__request__tests__openai_responses_all_catalog_tools.snap index 1cd286cd..cafb5f15 100644 --- a/crates/forge_repo/src/provider/openai_responses/snapshots/forge_repo__provider__openai_responses__request__tests__openai_responses_all_catalog_tools.snap +++ b/crates/forge_repo/src/provider/openai_responses/snapshots/forge_repo__provider__openai_responses__request__tests__openai_responses_all_catalog_tools.snap @@ -746,6 +746,7 @@ expression: actual.tools "additionalProperties": false, "required": [ "agent_id", + "model", "session_id", "tasks" ], @@ -771,10 +772,21 @@ expression: actual.tools "type": "null" } ] + }, + "model": { + "description": "Optional model override for this subagent run. When set, the spawned\nagent will use this model id instead of its configured default. The\nmodel must belong to a provider the user has already authenticated\nwith — unauthenticated models are rejected. Use this when the user\nexplicitly requests a specific model for a subagent (e.g. \"spawn a\nsubagent with gpt-5.4-medium to do X\").", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } } }, "strict": true, - "description": "Launch a new agent to handle complex, multi-step tasks autonomously. \n\nThe {{tool_names.task}} tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.\n\nAvailable agent types and the tools they have access to:\n{{#each agents}}\n- **{{id}}**{{#if description}}: {{description}}{{/if}}{{#if tools}}\n - Tools: {{#each tools}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}{{/if}}\n{{/each}}\n\nWhen using the {{tool_names.task}} tool, you must specify a agent_id parameter to select which agent type to use.\n\nWhen NOT to use the {{tool_names.task}} tool:\n- If you want to read a specific file path, use the {{tool_names.read}} or {{tool_names.fs_search}} tool instead of the {{tool_names.task}} tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the {{tool_names.fs_search}} tool instead, to find the match more quickly\n- If you are searching for code within a specific file or set of 2-3 files, use the {{tool_names.read}} tool instead of the {{tool_names.task}} tool, to find the match more quickly\n- Other tasks that are not related to the agent descriptions above\n\n\nUsage notes:\n- Always include a short description (3-5 words) summarizing what the agent will do\n- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n- Agents can be resumed using the \\`session_id\\` parameter by passing the agent ID from a previous invocation. When resumed, the agent continues with its full previous context preserved. When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.\n- When the agent is done, it will return a single message back to you along with its agent ID. You can use this ID to resume the agent later if needed for follow-up work.\n- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.\n- Agents with \"access to current context\" can see the full conversation history before the tool call. When using these agents, you can write concise prompts that reference earlier context (e.g., \"investigate the error discussed above\") instead of repeating information. The agent will receive all prior messages and understand the context.\n- The agent's outputs should generally be trusted\n- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent\n- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.\n- If the user specifies that they want you to run agents \"in parallel\", you MUST send a single message with multiple {{tool_names.task}} tool use content blocks. For example, if you need to launch both a build-validator agent and a test-runner agent in parallel, send a single message with both tool calls.\n\nExample usage:\n\n\n\"test-runner\": use this agent after you are done writing code to run tests\n\"greeting-responder\": use this agent when to respond to user greetings with a friendly joke\n\n\n\nuser: \"Please write a function that checks if a number is prime\"\nassistant: Sure let me write a function that checks if a number is prime\nassistant: First let me use the {{tool_names.write}} tool to write a function that checks if a number is prime\nassistant: I'm going to use the {{tool_names.write}} tool to write the following code:\n\nfunction isPrime(n) {\n if (n <= 1) return false\n for (let i = 2; i * i <= n; i++) {\n if (n % i === 0) return false\n }\n return true\n}\n\n\nSince a significant piece of code was written and the task was completed, now use the test-runner agent to run the tests\n\nassistant: Now let me use the test-runner agent to run the tests\nassistant: Uses the {{tool_names.task}} tool to launch the test-runner agent\n\n\n\nuser: \"Hello\"\n\nSince the user is greeting, use the greeting-responder agent to respond with a friendly joke\n\nassistant: \"I'm going to use the {{tool_names.task}} tool to launch the greeting-responder agent\"\n" + "description": "Launch a new agent to handle complex, multi-step tasks autonomously. \n\nThe {{tool_names.task}} tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.\n\nAvailable agent types and the tools they have access to:\n{{#each agents}}\n- **{{id}}**{{#if description}}: {{description}}{{/if}}{{#if tools}}\n - Tools: {{#each tools}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}{{/if}}\n{{/each}}\n\nWhen using the {{tool_names.task}} tool, you must specify a agent_id parameter to select which agent type to use.\n\nWhen NOT to use the {{tool_names.task}} tool:\n- If you want to read a specific file path, use the {{tool_names.read}} or {{tool_names.fs_search}} tool instead of the {{tool_names.task}} tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the {{tool_names.fs_search}} tool instead, to find the match more quickly\n- If you are searching for code within a specific file or set of 2-3 files, use the {{tool_names.read}} tool instead of the {{tool_names.task}} tool, to find the match more quickly\n- Other tasks that are not related to the agent descriptions above\n\n\nUsage notes:\n- Always include a short description (3-5 words) summarizing what the agent will do\n- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n- Agents can be resumed using the \\`session_id\\` parameter by passing the agent ID from a previous invocation. When resumed, the agent continues with its full previous context preserved. When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.\n- When the agent is done, it will return a single message back to you along with its agent ID. You can use this ID to resume the agent later if needed for follow-up work.\n- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.\n- Agents with \"access to current context\" can see the full conversation history before the tool call. When using these agents, you can write concise prompts that reference earlier context (e.g., \"investigate the error discussed above\") instead of repeating information. The agent will receive all prior messages and understand the context.\n- The agent's outputs should generally be trusted\n- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent\n- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.\n- If the user specifies that they want you to run agents \"in parallel\", you MUST send a single message with multiple {{tool_names.task}} tool use content blocks. For example, if you need to launch both a build-validator agent and a test-runner agent in parallel, send a single message with both tool calls.\n- Optional `model` field: when the user explicitly asks for a specific model for the subagent (e.g. \"spawn a subagent with gpt-5.4-medium\" or \"use sonnet-4.6 for this delegation\"), set `model` to the requested model id. Otherwise OMIT it and let the agent use its default. The override is rejected if the model isn't on the agent's currently-authenticated provider.\n\nExample usage:\n\n\n\"test-runner\": use this agent after you are done writing code to run tests\n\"greeting-responder\": use this agent when to respond to user greetings with a friendly joke\n\n\n\nuser: \"Please write a function that checks if a number is prime\"\nassistant: Sure let me write a function that checks if a number is prime\nassistant: First let me use the {{tool_names.write}} tool to write a function that checks if a number is prime\nassistant: I'm going to use the {{tool_names.write}} tool to write the following code:\n\nfunction isPrime(n) {\n if (n <= 1) return false\n for (let i = 2; i * i <= n; i++) {\n if (n % i === 0) return false\n }\n return true\n}\n\n\nSince a significant piece of code was written and the task was completed, now use the test-runner agent to run the tests\n\nassistant: Now let me use the test-runner agent to run the tests\nassistant: Uses the {{tool_names.task}} tool to launch the test-runner agent\n\n\n\nuser: \"Hello\"\n\nSince the user is greeting, use the greeting-responder agent to respond with a friendly joke\n\nassistant: \"I'm going to use the {{tool_names.task}} tool to launch the greeting-responder agent\"\n" } ] From 20ffdac0851ad269d22d9573d6e5971ab8701f8f Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Mon, 11 May 2026 12:19:41 +0800 Subject: [PATCH 05/15] fix(orch): break the pending-todos doom loop The orchestrator's End hook can re-arm the run loop by injecting a follow-up message (orch.rs ~548-553), and PendingTodosHandler does exactly that whenever there are still pending/in_progress todos. The existing fingerprint dedupe is content-keyed, so any todo reword changes the fingerprint and lets the reminder fire again. With a vague meta-todo the agent can never check off, this loops until max_requests_per_turn (default 100) cuts it off. Four bounded fixes: 1. PendingTodosHandler tracks total reminders in context, not just the last fingerprint. After max_reminders injections the handler stops re-arming regardless of how the agent rewords its todos. 2. Orchestrator counts End-hook re-arms per run and force-yields with a new InterruptionReason::EndHookRearmLimitReached when the configured cap is exceeded. Defense-in-depth against any End hook (current or future), not just pending-todos. 3. DoomLoopDetector now strips volatile keys (offset, limit, line_start/end, max_results, context_lines, ...) from a known set of exploration tools (read, grep, fs_search, codedb_*) before computing tool-call signatures. Catches "re-read same file with shifting offsets" and "re-grep with different max_results" loops that previously slipped through because each call had a unique argument tuple. 4. Both caps are wired to a single forge_config knob, max_end_hook_rearms (default 3), so one number bounds the loop from both ends. Tests: - 5 new unit tests in hooks::pending_todos / hooks::doom_loop - All 714 forge_app tests pass - cargo test --workspace green Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: blackfloofie-a codegraff agent <265516171+blackfloofie@users.noreply.github.com> --- crates/forge_app/src/app.rs | 11 +- crates/forge_app/src/hooks/doom_loop.rs | 163 +++++++++++++++++- crates/forge_app/src/hooks/pending_todos.rs | 159 +++++++++++++++-- crates/forge_app/src/orch.rs | 31 +++- crates/forge_app/src/orch_spec/orch_runner.rs | 17 +- crates/forge_app/src/orch_spec/orch_setup.rs | 9 + crates/forge_config/.forge.toml | 1 + crates/forge_config/src/config.rs | 15 ++ crates/forge_domain/src/chat_response.rs | 8 + crates/forge_main/src/ui.rs | 5 + forge.schema.json | 9 + sdk/typescript/src/wire.rs | 4 + 12 files changed, 408 insertions(+), 24 deletions(-) diff --git a/crates/forge_app/src/app.rs b/crates/forge_app/src/app.rs index 199abebc..e42510c5 100644 --- a/crates/forge_app/src/app.rs +++ b/crates/forge_app/src/app.rs @@ -224,12 +224,19 @@ impl> ForgeAp TitleGenerationHandler::with_enabled(services.clone(), forge_config.generate_titles); // Build the on_end hook, conditionally adding PendingTodosHandler based on - // config + // config. The reminder cap is wired to `max_end_hook_rearms` so a + // single knob bounds both the orchestrator's re-arm count and the + // number of pending-todo reminders the handler is willing to inject — + // they describe the same loop from two ends. let on_end_hook = if forge_config.verify_todos { + let pending_todos_handler = match forge_config.max_end_hook_rearms { + Some(cap) => PendingTodosHandler::with_max_reminders(cap), + None => PendingTodosHandler::new(), + }; tracing_handler .clone() .and(title_handler.clone()) - .and(PendingTodosHandler::new()) + .and(pending_todos_handler) } else { tracing_handler.clone().and(title_handler.clone()) }; diff --git a/crates/forge_app/src/hooks/doom_loop.rs b/crates/forge_app/src/hooks/doom_loop.rs index 3515b74e..92dbbb37 100644 --- a/crates/forge_app/src/hooks/doom_loop.rs +++ b/crates/forge_app/src/hooks/doom_loop.rs @@ -73,10 +73,85 @@ impl DoomLoopDetector { .iter() .filter_map(|msg| msg.tool_calls.as_ref()) .flat_map(|calls| calls.iter()) - .map(|call| (call.name.clone(), call.arguments.clone())) + .map(|call| { + ( + call.name.clone(), + Self::normalize_arguments(&call.name, &call.arguments), + ) + }) .collect() } + /// Normalizes tool-call arguments before doom-loop comparison so that + /// near-identical exploration patterns (same file/query, slightly + /// different offsets/limits) bucket together as a single signature. + /// + /// For the read/grep family we keep only the *intent* keys + /// (`file_path`, `path`, `pattern`, `query`, `glob_pattern`) and drop + /// pagination-style keys (`offset`, `limit`, `line_start`, `line_end`, + /// `max_results`, `context_lines`). This catches the common loop where + /// an agent re-reads the same file with shifting line ranges, or + /// re-greps the same pattern with different `max_results`, without + /// triggering before for genuinely new content. + /// + /// Tools outside this allow-list pass through untouched. + fn normalize_arguments(name: &ToolName, args: &ToolCallArguments) -> ToolCallArguments { + const NORMALIZED_TOOLS: &[&str] = &[ + "read", + "grep", + "fs_search", + "fs_read", + "find_file_by_name", + "search", + "codedb_read", + "codedb_search", + "codedb_word", + "codedb_outline", + ]; + // Volatile keys whose values typically change between + // exploration calls without changing the agent's intent. Dropped + // before signature comparison so [(read, file=A, offset=0)], + // [(read, file=A, offset=200)], [(read, file=A, offset=400)] + // all collapse to one signature. + const VOLATILE_KEYS: &[&str] = &[ + "offset", + "limit", + "line_start", + "line_end", + "start_line", + "end_line", + "max_results", + "max_result", + "context_lines", + "compact", + "if_hash", + "scope", + ]; + + if !NORMALIZED_TOOLS + .iter() + .any(|t| name.as_str().eq_ignore_ascii_case(t)) + { + return args.clone(); + } + + // Parse to a JSON Value; fall back to original on any error so we + // never drop signatures we couldn't normalize. + let Ok(value) = args.parse() else { + return args.clone(); + }; + + let serde_json::Value::Object(mut map) = value else { + return args.clone(); + }; + + for key in VOLATILE_KEYS { + map.remove(*key); + } + + ToolCallArguments::Parsed(serde_json::Value::Object(map)) + } + /// Checks for repeating patterns at the end of the sequence. fn check_repeating_pattern(&self, sequence: &[T]) -> Option<(usize, usize)> where @@ -324,6 +399,92 @@ mod tests { assert_eq!(actual, None); } + #[test] + fn test_doom_loop_detector_normalizes_read_offsets() { + // Regression: agent re-reads the same file with shifting line + // ranges (offset/limit, line_start/line_end). Before + // normalization these were three different signatures and the + // loop was missed; after normalization they collapse to one. + let detector = DoomLoopDetector::new(); + + let call_a = ToolCallFull::new("read").arguments(ToolCallArguments::from_json( + r#"{"path": "huge.ts", "offset": 0, "limit": 200}"#, + )); + let call_b = ToolCallFull::new("read").arguments(ToolCallArguments::from_json( + r#"{"path": "huge.ts", "offset": 200, "limit": 200}"#, + )); + let call_c = ToolCallFull::new("read").arguments(ToolCallArguments::from_json( + r#"{"path": "huge.ts", "offset": 400, "limit": 200}"#, + )); + + let messages = vec![ + create_assistant_message(&call_a), + create_assistant_message(&call_b), + create_assistant_message(&call_c), + ]; + let conversation = create_conversation_with_messages(messages); + + let actual = detector.detect_from_conversation(&conversation); + let expected = Some(3); + assert_eq!(actual, expected); + } + + #[test] + fn test_doom_loop_detector_normalizes_grep_max_results() { + // Same idea for grep: max_results / context_lines change between + // turns but the search intent is identical. + let detector = DoomLoopDetector::new(); + + let call_a = ToolCallFull::new("grep").arguments(ToolCallArguments::from_json( + r#"{"pattern": "TODO", "path": "src/", "max_results": 10}"#, + )); + let call_b = ToolCallFull::new("grep").arguments(ToolCallArguments::from_json( + r#"{"pattern": "TODO", "path": "src/", "max_results": 50}"#, + )); + let call_c = ToolCallFull::new("grep").arguments(ToolCallArguments::from_json( + r#"{"pattern": "TODO", "path": "src/", "max_results": 100, "context_lines": 3}"#, + )); + + let messages = vec![ + create_assistant_message(&call_a), + create_assistant_message(&call_b), + create_assistant_message(&call_c), + ]; + let conversation = create_conversation_with_messages(messages); + + let actual = detector.detect_from_conversation(&conversation); + let expected = Some(3); + assert_eq!(actual, expected); + } + + #[test] + fn test_doom_loop_detector_keeps_distinct_files_distinct() { + // Sanity: normalization must not collapse genuinely different + // file paths together. + let detector = DoomLoopDetector::new(); + + let call_a = ToolCallFull::new("read").arguments(ToolCallArguments::from_json( + r#"{"path": "a.ts", "offset": 0, "limit": 200}"#, + )); + let call_b = ToolCallFull::new("read").arguments(ToolCallArguments::from_json( + r#"{"path": "b.ts", "offset": 0, "limit": 200}"#, + )); + let call_c = ToolCallFull::new("read").arguments(ToolCallArguments::from_json( + r#"{"path": "c.ts", "offset": 0, "limit": 200}"#, + )); + + let messages = vec![ + create_assistant_message(&call_a), + create_assistant_message(&call_b), + create_assistant_message(&call_c), + ]; + let conversation = create_conversation_with_messages(messages); + + let actual = detector.detect_from_conversation(&conversation); + let expected = None; + assert_eq!(actual, expected); + } + #[test] fn test_doom_loop_detector_resets_on_different_arguments() { let detector = DoomLoopDetector::new(); diff --git a/crates/forge_app/src/hooks/pending_todos.rs b/crates/forge_app/src/hooks/pending_todos.rs index 14526174..5bf4585c 100644 --- a/crates/forge_app/src/hooks/pending_todos.rs +++ b/crates/forge_app/src/hooks/pending_todos.rs @@ -22,19 +22,74 @@ struct PendingTodosContext { todos: Vec, } +/// Default cap on the number of pending-todos reminders that may be +/// injected into a single conversation. Reached when the agent keeps +/// reshuffling its todo list (which changes the fingerprint and would +/// otherwise re-arm the orchestrator loop indefinitely). Mirrors the +/// `max_end_hook_rearms` config default. +const DEFAULT_MAX_REMINDERS: usize = 3; + /// Detects when the LLM signals task completion while there are still /// pending or in-progress todo items. /// /// When triggered, it injects a formatted reminder listing all /// outstanding todos into the conversation context, preventing the /// orchestrator from yielding prematurely. -#[derive(Debug, Clone, Default)] -pub struct PendingTodosHandler; +/// +/// Bounded by two checks to avoid runaway reminder loops: +/// +/// 1. **Same-fingerprint dedupe**: if the most-recent reminder in +/// context already covers the current set of pending todos, skip. +/// 2. **Total-reminder cap**: if the conversation already contains +/// `max_reminders` reminders (across all fingerprints), skip even if +/// the agent keeps rewording its todos. The cap is configurable via +/// `PendingTodosHandler::with_max_reminders` and defaults to +/// [`DEFAULT_MAX_REMINDERS`]. +#[derive(Debug, Clone)] +pub struct PendingTodosHandler { + max_reminders: usize, +} + +impl Default for PendingTodosHandler { + fn default() -> Self { + Self::new() + } +} impl PendingTodosHandler { - /// Creates a new pending-todos handler + /// Creates a new pending-todos handler with the default reminder cap. pub fn new() -> Self { - Self + Self { max_reminders: DEFAULT_MAX_REMINDERS } + } + + /// Overrides the maximum number of pending-todo reminders that may + /// be injected into a single conversation. Typically wired to + /// `forge_config.max_end_hook_rearms` so a single config knob + /// governs both the orchestrator's re-arm cap and the reminder cap. + pub fn with_max_reminders(max_reminders: usize) -> Self { + Self { max_reminders } + } + + /// Counts how many pending-todos reminders already exist in the + /// given conversation context. Used to enforce the total-reminder + /// cap regardless of whether the agent rewords its todos between + /// turns. + fn count_existing_reminders(context: &forge_domain::Context) -> usize { + context + .messages + .iter() + .filter(|entry| { + entry + .message + .content() + .map(|content| { + content + .lines() + .any(|line| line.trim_start().starts_with("todo_fingerprint=\"")) + }) + .unwrap_or(false) + }) + .count() } fn pending_todo_fingerprint(todos: &[Todo]) -> String { @@ -76,21 +131,30 @@ impl EventHandle> for PendingTodosHandler { let current_fingerprint = Self::pending_todo_fingerprint(&pending_todos); let should_add_reminder = if let Some(context) = &conversation.context { - let last_reminder_fingerprint = context.messages.iter().rev().find_map(|entry| { - let content = entry.message.content()?; - let fingerprint = content - .lines() - .find(|line| line.trim_start().starts_with("todo_fingerprint=\""))? - .split_once("\"")? - .1 - .split_once("\"")? - .0; - Some(fingerprint.to_string()) - }); - - match last_reminder_fingerprint { - Some(last_fingerprint) => last_fingerprint != current_fingerprint, - None => true, + // Cap total reminders regardless of fingerprint changes. If the + // agent has already been told `max_reminders` times that it has + // pending todos, further reminders won't help — let the + // orchestrator yield and surface control to the user. + if Self::count_existing_reminders(context) >= self.max_reminders { + false + } else { + let last_reminder_fingerprint = + context.messages.iter().rev().find_map(|entry| { + let content = entry.message.content()?; + let fingerprint = content + .lines() + .find(|line| line.trim_start().starts_with("todo_fingerprint=\""))? + .split_once("\"")? + .1 + .split_once("\"")? + .0; + Some(fingerprint.to_string()) + }); + + match last_reminder_fingerprint { + Some(last_fingerprint) => last_fingerprint != current_fingerprint, + None => true, + } } } else { true @@ -266,6 +330,63 @@ mod tests { assert_eq!(after_second, 1); } + #[tokio::test] + async fn test_reminder_capped_when_todos_keep_changing() { + // Regression: agent reshuffles its todo list each turn (changes + // fingerprint), historically that re-armed the orchestrator loop + // forever. The total-reminder cap forces yield after N reminders. + let handler = PendingTodosHandler::with_max_reminders(3); + let event = fixture_event(); + let mut conversation = + fixture_conversation(vec![Todo::new("v1").status(TodoStatus::Pending)]); + + // Reminder 1: fresh fingerprint + handler.handle(&event, &mut conversation).await.unwrap(); + assert_eq!(conversation.context.as_ref().unwrap().messages.len(), 1); + + // Reminder 2: change fingerprint by rewording + conversation.metrics = conversation + .metrics + .clone() + .todos(vec![Todo::new("v2").status(TodoStatus::Pending)]); + handler.handle(&event, &mut conversation).await.unwrap(); + assert_eq!(conversation.context.as_ref().unwrap().messages.len(), 2); + + // Reminder 3: different fingerprint again, this is the last allowed + conversation.metrics = conversation + .metrics + .clone() + .todos(vec![Todo::new("v3").status(TodoStatus::Pending)]); + handler.handle(&event, &mut conversation).await.unwrap(); + assert_eq!(conversation.context.as_ref().unwrap().messages.len(), 3); + + // Reminder 4: cap kicks in even though the fingerprint changed + conversation.metrics = conversation + .metrics + .clone() + .todos(vec![Todo::new("v4").status(TodoStatus::Pending)]); + handler.handle(&event, &mut conversation).await.unwrap(); + let actual = conversation.context.as_ref().unwrap().messages.len(); + let expected = 3; + assert_eq!(actual, expected); + } + + #[tokio::test] + async fn test_with_max_reminders_zero_disables_handler() { + // Setting the cap to 0 effectively turns the handler off; useful + // for tests / users who want the orchestrator to never re-arm on + // pending todos. + let handler = PendingTodosHandler::with_max_reminders(0); + let event = fixture_event(); + let mut conversation = + fixture_conversation(vec![Todo::new("Fix the build").status(TodoStatus::Pending)]); + + handler.handle(&event, &mut conversation).await.unwrap(); + let actual = conversation.context.as_ref().unwrap().messages.len(); + let expected = 0; + assert_eq!(actual, expected); + } + #[tokio::test] async fn test_reminder_added_when_todos_change() { let handler = PendingTodosHandler::new(); diff --git a/crates/forge_app/src/orch.rs b/crates/forge_app/src/orch.rs index 2adc8fd1..9931c44b 100644 --- a/crates/forge_app/src/orch.rs +++ b/crates/forge_app/src/orch.rs @@ -321,6 +321,14 @@ impl> Orc let mut is_complete = false; let mut request_count = 0; + // Counts how many times an `End`-lifecycle hook re-armed the loop + // by injecting a follow-up message (most often the pending-todos + // reminder). Bounded by `forge_config.max_end_hook_rearms` to stop + // self-perpetuating "reminder + reword" loops well before + // `max_requests_per_turn` would cap them. `None` in config disables + // the cap entirely (legacy behavior). + let mut end_hook_rearms: usize = 0; + let end_hook_rearm_cap: Option = self.config.max_end_hook_rearms; // Per-run fitness vector accumulators. Sums what's already computed // turn-by-turn (token usage on each assistant message, success/error // flags on each tool result) so an AgentRunEnd event at the bottom @@ -550,7 +558,28 @@ impl> Orc if let Some(updated_context) = &self.conversation.context { context = updated_context.clone(); } - should_yield = false; + end_hook_rearms = end_hook_rearms.saturating_add(1); + + // Force-yield once we've re-armed past the configured cap. + // Prevents the "pending-todos reminder fires, agent + // reshuffles todos, fingerprint changes, reminder fires + // again" loop from running until max_requests_per_turn. + if let Some(cap) = end_hook_rearm_cap + && end_hook_rearms > cap + { + interrupt_reason = Some(format!( + "max_end_hook_rearms_reached: {cap}" + )); + self.send(ChatResponse::Interrupt { + reason: InterruptionReason::EndHookRearmLimitReached { + limit: cap as u64, + }, + }) + .await?; + // leave should_yield = true so the outer loop exits + } else { + should_yield = false; + } } } } diff --git a/crates/forge_app/src/orch_spec/orch_runner.rs b/crates/forge_app/src/orch_spec/orch_runner.rs index c33c8349..1e02fbb0 100644 --- a/crates/forge_app/src/orch_spec/orch_runner.rs +++ b/crates/forge_app/src/orch_spec/orch_runner.rs @@ -129,13 +129,28 @@ impl Runner { ApplyTunableParameters::new(agent.clone(), system_tools.clone()).apply(conversation); let conversation = SetConversationId.apply(conversation); + // Mirror the production wiring in `app.rs`: the pending-todos + // handler's reminder cap follows `max_end_hook_rearms` so tests + // exercising the orchestrator-level re-arm cap can trip it cleanly + // by setting the config field rather than threading a handler + // override through the runner. Tests that need to exercise the + // orchestrator cap *without* the handler short-circuiting first + // can set `pending_todos_handler_cap_override` to a high value. + let pending_todos_handler = match setup + .pending_todos_handler_cap_override + .or(setup.config.max_end_hook_rearms) + { + Some(cap) => PendingTodosHandler::with_max_reminders(cap), + None => PendingTodosHandler::new(), + }; + let orch = Orchestrator::new(services.clone(), conversation, agent, setup.config.clone()) .error_tracker(ToolErrorTracker::new(3)) .tool_definitions(system_tools) .hook(Arc::new( Hook::default() .on_request(DoomLoopDetector::default()) - .on_end(PendingTodosHandler::new()), + .on_end(pending_todos_handler), )) .sender(tx); diff --git a/crates/forge_app/src/orch_spec/orch_setup.rs b/crates/forge_app/src/orch_spec/orch_setup.rs index 5a28d482..6659e3a7 100644 --- a/crates/forge_app/src/orch_spec/orch_setup.rs +++ b/crates/forge_app/src/orch_spec/orch_setup.rs @@ -43,6 +43,14 @@ pub struct TestContext { /// ForgeConfig used to populate TemplateConfig for /// system prompt rendering in tests. pub config: ForgeConfig, + + /// Optional override for the pending-todos handler reminder cap, used + /// only in tests that need to decouple the handler cap from the + /// orchestrator-level `max_end_hook_rearms` cap (e.g. exercising the + /// orch-level interrupt without the handler short-circuiting first). + /// `None` means "follow `config.max_end_hook_rearms`" — production + /// behavior. + pub pending_todos_handler_cap_override: Option, } impl Default for TestContext { @@ -68,6 +76,7 @@ impl Default for TestContext { config: ForgeConfig::default() .tool_supported(true) .max_extensions(15), + pending_todos_handler_cap_override: None, title: Some("test-conversation".into()), agent: Agent::new( AgentId::new("forge"), diff --git a/crates/forge_config/.forge.toml b/crates/forge_config/.forge.toml index 7df89c28..7a59a9bb 100644 --- a/crates/forge_config/.forge.toml +++ b/crates/forge_config/.forge.toml @@ -28,6 +28,7 @@ tool_timeout_secs = 300 top_k = 30 top_p = 0.8 verify_todos = true +max_end_hook_rearms = 3 research_subagent = false currency_symbol = "$" currency_conversion_rate = 1.0 diff --git a/crates/forge_config/src/config.rs b/crates/forge_config/src/config.rs index 9c29ef22..1172ab9a 100644 --- a/crates/forge_config/src/config.rs +++ b/crates/forge_config/src/config.rs @@ -294,6 +294,21 @@ pub struct ForgeConfig { #[serde(default)] pub verify_todos: bool, + /// Maximum number of times an `End`-lifecycle hook may re-arm the + /// orchestrator loop in a single run by injecting a follow-up message + /// (e.g. the pending-todos reminder). + /// + /// Once the cap is reached, the orchestrator force-yields with an + /// `EndHookRearmLimitReached` interrupt rather than continuing to ping + /// the model. This bounds the "reminder + reword + reminder" loop that + /// otherwise only terminated at `max_requests_per_turn`. + /// + /// Also used as the per-fingerprint cap in the pending-todos handler so + /// the same set of incomplete todos can't trigger more reminders than + /// the orchestrator is willing to honor anyway. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_end_hook_rearms: Option, + /// Whether the deep research agent is available. /// /// When set to `true`, the Sage agent is added to the agent list and diff --git a/crates/forge_domain/src/chat_response.rs b/crates/forge_domain/src/chat_response.rs index e24cd9d7..8ed438b5 100644 --- a/crates/forge_domain/src/chat_response.rs +++ b/crates/forge_domain/src/chat_response.rs @@ -102,6 +102,14 @@ pub enum InterruptionReason { MaxRequestPerTurnLimitReached { limit: u64, }, + /// The orchestrator's `End`-lifecycle hooks tried to re-arm the loop + /// (by injecting a follow-up message) more times than the configured + /// cap allows. Most commonly hit when the pending-todos reminder + /// keeps firing because the agent reshuffles its todo list each turn + /// instead of completing items. + EndHookRearmLimitReached { + limit: u64, + }, } #[derive(Clone)] diff --git a/crates/forge_main/src/ui.rs b/crates/forge_main/src/ui.rs index fc62b2b2..35a070a3 100644 --- a/crates/forge_main/src/ui.rs +++ b/crates/forge_main/src/ui.rs @@ -4296,6 +4296,11 @@ impl A + Send + Sync> UI InterruptionReason::MaxToolFailurePerTurnLimitReached { limit, .. } => { format!("Maximum tool failure limit ({limit}) reached for this turn") } + InterruptionReason::EndHookRearmLimitReached { limit } => { + format!( + "End-hook re-arm limit ({limit}) reached \u{2014} pending-todos reminder kept firing without progress" + ) + } }; self.writeln_title(TitleFormat::action(title))?; diff --git a/forge.schema.json b/forge.schema.json index e6add9e2..c051a698 100644 --- a/forge.schema.json +++ b/forge.schema.json @@ -362,6 +362,15 @@ "type": "boolean", "default": false }, + "max_end_hook_rearms": { + "description": "Maximum number of times an `End`-lifecycle hook may re-arm the\norchestrator loop in a single run by injecting a follow-up message\n(e.g. the pending-todos reminder).\n\nOnce the cap is reached, the orchestrator force-yields with an\n`EndHookRearmLimitReached` interrupt rather than continuing to ping\nthe model. This bounds the \"reminder + reword + reminder\" loop that\notherwise only terminated at `max_requests_per_turn`.\n\nAlso used as the per-fingerprint cap in the pending-todos handler so\nthe same set of incomplete todos can't trigger more reminders than\nthe orchestrator is willing to honor anyway.", + "type": [ + "integer", + "null" + ], + "format": "uint", + "minimum": 0 + }, "research_subagent": { "description": "Whether the deep research agent is available.\n\nWhen set to `true`, the Sage agent is added to the agent list and\nthe `:sage` app command is enabled. Defaults to `false`.", "type": "boolean", diff --git a/sdk/typescript/src/wire.rs b/sdk/typescript/src/wire.rs index 423fa97f..3f470475 100644 --- a/sdk/typescript/src/wire.rs +++ b/sdk/typescript/src/wire.rs @@ -68,6 +68,7 @@ pub enum WireCategory { pub enum WireInterrupt { MaxToolFailurePerTurnLimitReached { limit: u64 }, MaxRequestPerTurnLimitReached { limit: u64 }, + EndHookRearmLimitReached { limit: u64 }, } impl From for WireEvent { @@ -145,6 +146,9 @@ impl From for WireInterrupt { InterruptionReason::MaxRequestPerTurnLimitReached { limit } => { WireInterrupt::MaxRequestPerTurnLimitReached { limit } } + InterruptionReason::EndHookRearmLimitReached { limit } => { + WireInterrupt::EndHookRearmLimitReached { limit } + } } } } From 45e0d6291738c391ec2e8f9e230181bd20ba5c35 Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Wed, 20 May 2026 10:17:43 +0800 Subject: [PATCH 06/15] ci(sdk-typescript): add tag-gated npm publish workflow Triggered by sdk/typescript-vX.Y.Z tags. Builds native addons for all four supported triples, assembles per-triple subpackages, then publishes to npm with provenance via the GitHub OIDC token. Verifies the tag's version matches package.json before building so a misnamed tag fails fast instead of after the matrix completes. Auth path is dual: Trusted Publisher (OIDC, no secret) is preferred, NPM_TOKEN is the fallback if it is set as a repo secret. NPM_CONFIG_PROVENANCE is enabled either way so the published artifacts carry a signed attestation of which workflow + commit produced them. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/sdk-typescript-publish.yml | 187 +++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 .github/workflows/sdk-typescript-publish.yml diff --git a/.github/workflows/sdk-typescript-publish.yml b/.github/workflows/sdk-typescript-publish.yml new file mode 100644 index 00000000..029e7734 --- /dev/null +++ b/.github/workflows/sdk-typescript-publish.yml @@ -0,0 +1,187 @@ +# Tag-gated publish for @codegraff/sdk to npm. +# +# Triggered by pushing a tag like sdk/typescript-v0.1.6. Rebuilds every native +# addon, assembles the per-triple npm subpackages, then publishes the lot to +# npm with provenance (signed by the GitHub OIDC token). +# +# Pre-flight (one-time, on npmjs.com): +# 1. Repo must be public for provenance to work. +# 2. Configure "Trusted Publisher" on each of these packages: +# @codegraff/sdk +# @codegraff/sdk-darwin-arm64 +# @codegraff/sdk-darwin-x64 +# @codegraff/sdk-linux-arm64-gnu +# @codegraff/sdk-linux-x64-gnu +# @codegraff/sdk-win32-x64-msvc +# Workflow file: .github/workflows/sdk-typescript-publish.yml. +# 3. Or add NPM_TOKEN as a repo secret as a fallback. +# +# Release flow: +# cd sdk/typescript +# npm run version # bumps main + every subpackage in lockstep +# git commit -am "release: sdk/typescript v0.1.6" +# git tag sdk/typescript-v0.1.6 +# git push origin main --tags +# +# This workflow rejects the run if the tag version does not match package.json. + +name: sdk-typescript-publish + +on: + push: + tags: + - 'sdk/typescript-v*' + +env: + RUSTFLAGS: '-Dwarnings' + +defaults: + run: + working-directory: sdk/typescript + +# id-token: write is required to mint the OIDC token npm verifies for provenance. +permissions: + contents: read + id-token: write + +concurrency: + # Never let two publishes race on the same tag. + group: sdk-typescript-publish-${{ github.ref }} + cancel-in-progress: false + +jobs: + verify-tag: + name: verify tag matches package.json + runs-on: ubuntu-latest + outputs: + version: ${{ steps.extract.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Extract version from tag and verify against package.json + id: extract + run: | + tag="${GITHUB_REF#refs/tags/sdk/typescript-v}" + pkg="$(node -p "require('./package.json').version")" + if [ "$tag" != "$pkg" ]; then + echo "::error::Tag version ($tag) does not match package.json version ($pkg)" + exit 1 + fi + echo "version=$tag" >> "$GITHUB_OUTPUT" + echo "Publishing version $tag" + + build: + name: build (${{ matrix.target }}) + needs: verify-tag + runs-on: ${{ matrix.os }} + strategy: + # Fail the whole publish if any triple fails to build. We never want a + # partial release where only some platforms are on npm. + fail-fast: true + matrix: + include: + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-apple-darwin + os: macos-13 + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: sdk/typescript/package-lock.json + + - name: Setup protoc + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + + - name: Cache Cargo build + uses: Swatinem/rust-cache@v2 + with: + workspaces: . -> target + shared-key: sdk-typescript-${{ matrix.target }} + + - name: Install npm dependencies + run: npm ci + + - name: Build native addon + run: npm run build -- --target ${{ matrix.target }} + + - name: Upload .node artifact + uses: actions/upload-artifact@v4 + with: + name: bindings-${{ matrix.target }} + path: sdk/typescript/codegraff-sdk.*.node + if-no-files-found: error + retention-days: 7 + + publish: + name: publish to npm + needs: [verify-tag, build] + runs-on: ubuntu-latest + environment: + name: npm-publish + url: https://www.npmjs.com/package/@codegraff/sdk + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + # registry-url makes setup-node write an .npmrc that picks up + # NODE_AUTH_TOKEN. Still required when using Trusted Publisher (OIDC) + # so npm publish targets the right registry. + registry-url: 'https://registry.npmjs.org' + cache: 'npm' + cache-dependency-path: sdk/typescript/package-lock.json + + - name: Install npm dependencies + run: npm ci + + - name: Download all build artifacts + uses: actions/download-artifact@v4 + with: + path: sdk/typescript/artifacts + pattern: bindings-* + merge-multiple: false + + - name: Move .node files into npm// + run: npm run artifacts + + - name: List assembled tree + run: | + echo "::group::sdk/typescript/npm tree" + find npm -type f -printf '%p (%s bytes)\n' | sort + echo "::endgroup::" + + - name: Publish to npm (with provenance) + env: + # Provenance flag is read by both npm publish + napi prepublish. + NPM_CONFIG_PROVENANCE: 'true' + # Empty when using Trusted Publisher (OIDC handles auth). + # Populated when falling back to a classic automation token. + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npx napi prepublish -t npm --skip-gh-release + + - name: Summary + run: | + echo "Published @codegraff/sdk@${{ needs.verify-tag.outputs.version }}" >> "$GITHUB_STEP_SUMMARY" + echo "See: https://www.npmjs.com/package/@codegraff/sdk/v/${{ needs.verify-tag.outputs.version }}" >> "$GITHUB_STEP_SUMMARY" From 426a4f6988d65005a1fa179cb1415a43b451e0ef Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Wed, 20 May 2026 13:11:02 +0800 Subject: [PATCH 07/15] release: sdk/typescript v0.1.8 Bump main package + 5 per-triple subpackages to 0.1.8 in lockstep. 0.1.7 shipped broken Linux binaries because zigbuild cross-compile from macOS dropped aws-lc-sys versioned symbols. 0.1.8 ships the same source rebuilt on ubuntu-latest in CI, where the native linker resolves aws-lc symbols correctly. Co-Authored-By: Claude Opus 4.7 (1M context) --- sdk/typescript/npm/darwin-arm64/package.json | 4 ++-- sdk/typescript/npm/darwin-x64/package.json | 4 ++-- sdk/typescript/npm/linux-arm64-gnu/package.json | 4 ++-- sdk/typescript/npm/linux-x64-gnu/package.json | 4 ++-- sdk/typescript/npm/win32-x64-msvc/package.json | 4 ++-- sdk/typescript/package.json | 12 ++++++------ 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/sdk/typescript/npm/darwin-arm64/package.json b/sdk/typescript/npm/darwin-arm64/package.json index 9911ba2a..3f224fec 100644 --- a/sdk/typescript/npm/darwin-arm64/package.json +++ b/sdk/typescript/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-arm64", - "version": "0.1.5", + "version": "0.1.8", "os": [ "darwin" ], @@ -16,4 +16,4 @@ "engines": { "node": ">= 18" } -} \ No newline at end of file +} diff --git a/sdk/typescript/npm/darwin-x64/package.json b/sdk/typescript/npm/darwin-x64/package.json index 41ab8646..b5dd22bb 100644 --- a/sdk/typescript/npm/darwin-x64/package.json +++ b/sdk/typescript/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-x64", - "version": "0.1.5", + "version": "0.1.8", "os": [ "darwin" ], @@ -16,4 +16,4 @@ "engines": { "node": ">= 18" } -} \ No newline at end of file +} diff --git a/sdk/typescript/npm/linux-arm64-gnu/package.json b/sdk/typescript/npm/linux-arm64-gnu/package.json index f4be87d1..8cfaa1bf 100644 --- a/sdk/typescript/npm/linux-arm64-gnu/package.json +++ b/sdk/typescript/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-arm64-gnu", - "version": "0.1.5", + "version": "0.1.8", "os": [ "linux" ], @@ -19,4 +19,4 @@ "libc": [ "glibc" ] -} \ No newline at end of file +} diff --git a/sdk/typescript/npm/linux-x64-gnu/package.json b/sdk/typescript/npm/linux-x64-gnu/package.json index dbc741e9..d6118f3c 100644 --- a/sdk/typescript/npm/linux-x64-gnu/package.json +++ b/sdk/typescript/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-x64-gnu", - "version": "0.1.5", + "version": "0.1.8", "os": [ "linux" ], @@ -19,4 +19,4 @@ "libc": [ "glibc" ] -} \ No newline at end of file +} diff --git a/sdk/typescript/npm/win32-x64-msvc/package.json b/sdk/typescript/npm/win32-x64-msvc/package.json index fa8bafe4..06b123f4 100644 --- a/sdk/typescript/npm/win32-x64-msvc/package.json +++ b/sdk/typescript/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-win32-x64-msvc", - "version": "0.1.5", + "version": "0.1.8", "os": [ "win32" ], @@ -16,4 +16,4 @@ "engines": { "node": ">= 18" } -} \ No newline at end of file +} diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index 14a07baa..f5b73a5c 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk", - "version": "0.1.5", + "version": "0.1.8", "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", "main": "lib.js", "types": "lib.d.ts", @@ -43,11 +43,11 @@ "tsx": "^4.20.0" }, "optionalDependencies": { - "@codegraff/sdk-darwin-arm64": "0.1.5", - "@codegraff/sdk-darwin-x64": "0.1.5", - "@codegraff/sdk-linux-arm64-gnu": "0.1.5", - "@codegraff/sdk-linux-x64-gnu": "0.1.5", - "@codegraff/sdk-win32-x64-msvc": "0.1.5" + "@codegraff/sdk-darwin-arm64": "0.1.8", + "@codegraff/sdk-darwin-x64": "0.1.8", + "@codegraff/sdk-linux-arm64-gnu": "0.1.8", + "@codegraff/sdk-linux-x64-gnu": "0.1.8", + "@codegraff/sdk-win32-x64-msvc": "0.1.8" }, "files": [ "lib.js", From 76e5b9882b329113c6bf05ea78d1d102f893ba77 Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Wed, 20 May 2026 14:42:40 +0800 Subject: [PATCH 08/15] release: sdk/typescript v0.1.9 Switch x86_64-apple-darwin CI matrix from macos-13 (Intel) to macos-latest (Apple Silicon). macos-13 runners are over-subscribed on GitHub Actions and routinely sit in queued for 40+ min; the 0.1.8 release stalled on this twice. Apple Silicon cross-compiles to x86_64-apple-darwin via the macOS SDK natively, no zig needed, no broken aws-lc symbols. Bumps main + 5 per-triple subpackages 0.1.7 -> 0.1.9 in lockstep. 0.1.8 on npm will remain absent (CI never reached publish step before cancel). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/sdk-typescript-publish.yml | 5 ++++- sdk/typescript/npm/darwin-arm64/package.json | 2 +- sdk/typescript/npm/darwin-x64/package.json | 2 +- sdk/typescript/npm/linux-arm64-gnu/package.json | 2 +- sdk/typescript/npm/linux-x64-gnu/package.json | 2 +- sdk/typescript/npm/win32-x64-msvc/package.json | 2 +- sdk/typescript/package.json | 12 ++++++------ 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/workflows/sdk-typescript-publish.yml b/.github/workflows/sdk-typescript-publish.yml index 029e7734..363f39b4 100644 --- a/.github/workflows/sdk-typescript-publish.yml +++ b/.github/workflows/sdk-typescript-publish.yml @@ -84,7 +84,10 @@ jobs: - target: aarch64-apple-darwin os: macos-latest - target: x86_64-apple-darwin - os: macos-13 + # macos-13 (Intel) runners are over-subscribed and routinely sit + # queued for an hour+. macos-latest is Apple Silicon and cross- + # compiles to x86_64-apple-darwin via the macOS SDK natively. + os: macos-latest - target: x86_64-unknown-linux-gnu os: ubuntu-latest - target: x86_64-pc-windows-msvc diff --git a/sdk/typescript/npm/darwin-arm64/package.json b/sdk/typescript/npm/darwin-arm64/package.json index 3f224fec..88e17142 100644 --- a/sdk/typescript/npm/darwin-arm64/package.json +++ b/sdk/typescript/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-arm64", - "version": "0.1.8", + "version": "0.1.9", "os": [ "darwin" ], diff --git a/sdk/typescript/npm/darwin-x64/package.json b/sdk/typescript/npm/darwin-x64/package.json index b5dd22bb..77c256e4 100644 --- a/sdk/typescript/npm/darwin-x64/package.json +++ b/sdk/typescript/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-x64", - "version": "0.1.8", + "version": "0.1.9", "os": [ "darwin" ], diff --git a/sdk/typescript/npm/linux-arm64-gnu/package.json b/sdk/typescript/npm/linux-arm64-gnu/package.json index 8cfaa1bf..30647b35 100644 --- a/sdk/typescript/npm/linux-arm64-gnu/package.json +++ b/sdk/typescript/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-arm64-gnu", - "version": "0.1.8", + "version": "0.1.9", "os": [ "linux" ], diff --git a/sdk/typescript/npm/linux-x64-gnu/package.json b/sdk/typescript/npm/linux-x64-gnu/package.json index d6118f3c..4e580df5 100644 --- a/sdk/typescript/npm/linux-x64-gnu/package.json +++ b/sdk/typescript/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-x64-gnu", - "version": "0.1.8", + "version": "0.1.9", "os": [ "linux" ], diff --git a/sdk/typescript/npm/win32-x64-msvc/package.json b/sdk/typescript/npm/win32-x64-msvc/package.json index 06b123f4..ea8c0596 100644 --- a/sdk/typescript/npm/win32-x64-msvc/package.json +++ b/sdk/typescript/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-win32-x64-msvc", - "version": "0.1.8", + "version": "0.1.9", "os": [ "win32" ], diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index f5b73a5c..866df450 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk", - "version": "0.1.8", + "version": "0.1.9", "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", "main": "lib.js", "types": "lib.d.ts", @@ -43,11 +43,11 @@ "tsx": "^4.20.0" }, "optionalDependencies": { - "@codegraff/sdk-darwin-arm64": "0.1.8", - "@codegraff/sdk-darwin-x64": "0.1.8", - "@codegraff/sdk-linux-arm64-gnu": "0.1.8", - "@codegraff/sdk-linux-x64-gnu": "0.1.8", - "@codegraff/sdk-win32-x64-msvc": "0.1.8" + "@codegraff/sdk-darwin-arm64": "0.1.9", + "@codegraff/sdk-darwin-x64": "0.1.9", + "@codegraff/sdk-linux-arm64-gnu": "0.1.9", + "@codegraff/sdk-linux-x64-gnu": "0.1.9", + "@codegraff/sdk-win32-x64-msvc": "0.1.9" }, "files": [ "lib.js", From a78a4fb06c483e70c6dc842a6b73bcea4218514b Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Wed, 20 May 2026 15:08:09 +0800 Subject: [PATCH 09/15] release: sdk/typescript v0.1.10 Add 'repository' field to each per-triple subpackage's package.json so npm provenance verification (sigstore) can match the publish to the GitHub workflow's claimed source. 0.1.9 publish failed with E422 'package.json: "repository.url" is "", expected to match https://github.com/justrach/codegraff from provenance'. Bumps main + 5 subpackages 0.1.9 -> 0.1.10 in lockstep. Co-Authored-By: Claude Opus 4.7 (1M context) --- sdk/typescript/npm/darwin-arm64/package.json | 7 ++++++- sdk/typescript/npm/darwin-x64/package.json | 7 ++++++- sdk/typescript/npm/linux-arm64-gnu/package.json | 9 +++++++-- sdk/typescript/npm/linux-x64-gnu/package.json | 9 +++++++-- sdk/typescript/npm/win32-x64-msvc/package.json | 7 ++++++- sdk/typescript/package.json | 12 ++++++------ 6 files changed, 38 insertions(+), 13 deletions(-) diff --git a/sdk/typescript/npm/darwin-arm64/package.json b/sdk/typescript/npm/darwin-arm64/package.json index 88e17142..eed7182f 100644 --- a/sdk/typescript/npm/darwin-arm64/package.json +++ b/sdk/typescript/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-arm64", - "version": "0.1.9", + "version": "0.1.10", "os": [ "darwin" ], @@ -15,5 +15,10 @@ "license": "MIT", "engines": { "node": ">= 18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/justrach/codegraff.git", + "directory": "sdk/typescript/npm/darwin-arm64" } } diff --git a/sdk/typescript/npm/darwin-x64/package.json b/sdk/typescript/npm/darwin-x64/package.json index 77c256e4..0d3689e1 100644 --- a/sdk/typescript/npm/darwin-x64/package.json +++ b/sdk/typescript/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-x64", - "version": "0.1.9", + "version": "0.1.10", "os": [ "darwin" ], @@ -15,5 +15,10 @@ "license": "MIT", "engines": { "node": ">= 18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/justrach/codegraff.git", + "directory": "sdk/typescript/npm/darwin-x64" } } diff --git a/sdk/typescript/npm/linux-arm64-gnu/package.json b/sdk/typescript/npm/linux-arm64-gnu/package.json index 30647b35..a6c89938 100644 --- a/sdk/typescript/npm/linux-arm64-gnu/package.json +++ b/sdk/typescript/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-arm64-gnu", - "version": "0.1.9", + "version": "0.1.10", "os": [ "linux" ], @@ -18,5 +18,10 @@ }, "libc": [ "glibc" - ] + ], + "repository": { + "type": "git", + "url": "git+https://github.com/justrach/codegraff.git", + "directory": "sdk/typescript/npm/linux-arm64-gnu" + } } diff --git a/sdk/typescript/npm/linux-x64-gnu/package.json b/sdk/typescript/npm/linux-x64-gnu/package.json index 4e580df5..33a51fc6 100644 --- a/sdk/typescript/npm/linux-x64-gnu/package.json +++ b/sdk/typescript/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-x64-gnu", - "version": "0.1.9", + "version": "0.1.10", "os": [ "linux" ], @@ -18,5 +18,10 @@ }, "libc": [ "glibc" - ] + ], + "repository": { + "type": "git", + "url": "git+https://github.com/justrach/codegraff.git", + "directory": "sdk/typescript/npm/linux-x64-gnu" + } } diff --git a/sdk/typescript/npm/win32-x64-msvc/package.json b/sdk/typescript/npm/win32-x64-msvc/package.json index ea8c0596..068ea3b7 100644 --- a/sdk/typescript/npm/win32-x64-msvc/package.json +++ b/sdk/typescript/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-win32-x64-msvc", - "version": "0.1.9", + "version": "0.1.10", "os": [ "win32" ], @@ -15,5 +15,10 @@ "license": "MIT", "engines": { "node": ">= 18" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/justrach/codegraff.git", + "directory": "sdk/typescript/npm/win32-x64-msvc" } } diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index 866df450..0bb6c588 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk", - "version": "0.1.9", + "version": "0.1.10", "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", "main": "lib.js", "types": "lib.d.ts", @@ -43,11 +43,11 @@ "tsx": "^4.20.0" }, "optionalDependencies": { - "@codegraff/sdk-darwin-arm64": "0.1.9", - "@codegraff/sdk-darwin-x64": "0.1.9", - "@codegraff/sdk-linux-arm64-gnu": "0.1.9", - "@codegraff/sdk-linux-x64-gnu": "0.1.9", - "@codegraff/sdk-win32-x64-msvc": "0.1.9" + "@codegraff/sdk-darwin-arm64": "0.1.10", + "@codegraff/sdk-darwin-x64": "0.1.10", + "@codegraff/sdk-linux-arm64-gnu": "0.1.10", + "@codegraff/sdk-linux-x64-gnu": "0.1.10", + "@codegraff/sdk-win32-x64-msvc": "0.1.10" }, "files": [ "lib.js", From 35385f46f67b7c422cfe4cc5cc5fc4eda879c1c1 Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Wed, 20 May 2026 15:49:22 +0800 Subject: [PATCH 10/15] release: sdk/typescript v0.1.12 Run the x86_64-unknown-linux-gnu build inside a debian-bullseye container (GLIBC 2.31) instead of on bare ubuntu-latest (GLIBC 2.39). The previous .node required GLIBC 2.38 and failed to load on Vercel sandbox (2.34), RHEL 9, Amazon Linux 2023, Debian 11, and Ubuntu 20/22. Adds a separate build-linux job (GHA matrices cannot conditionally apply container:) and makes publish depend on both build + build-linux. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/sdk-typescript-publish.yml | 49 +++++++++++++++++-- sdk/typescript/npm/darwin-arm64/package.json | 2 +- sdk/typescript/npm/darwin-x64/package.json | 2 +- .../npm/linux-arm64-gnu/package.json | 2 +- sdk/typescript/npm/linux-x64-gnu/package.json | 2 +- .../npm/win32-x64-msvc/package.json | 2 +- sdk/typescript/package.json | 12 ++--- 7 files changed, 57 insertions(+), 14 deletions(-) diff --git a/.github/workflows/sdk-typescript-publish.yml b/.github/workflows/sdk-typescript-publish.yml index 363f39b4..7bb3e7e5 100644 --- a/.github/workflows/sdk-typescript-publish.yml +++ b/.github/workflows/sdk-typescript-publish.yml @@ -88,8 +88,12 @@ jobs: # queued for an hour+. macos-latest is Apple Silicon and cross- # compiles to x86_64-apple-darwin via the macOS SDK natively. os: macos-latest - - target: x86_64-unknown-linux-gnu - os: ubuntu-latest + # NOTE: x86_64-unknown-linux-gnu intentionally NOT in this matrix. + # bare ubuntu-latest produces a .node that requires GLIBC 2.38 + # (Ubuntu 24.04 default), which fails to load on Vercel sandbox + # (GLIBC 2.34), RHEL 9, Amazon Linux 2023, etc. The build-linux + # job below runs in a debian-bullseye container (GLIBC 2.31) so + # the resulting .node is broadly compatible. - target: x86_64-pc-windows-msvc os: windows-latest steps: @@ -134,9 +138,48 @@ jobs: if-no-files-found: error retention-days: 7 + build-linux: + name: build (x86_64-unknown-linux-gnu) + needs: verify-tag + runs-on: ubuntu-latest + # Debian Bullseye = GLIBC 2.31, broadly compatible with every + # production Linux deployment (Vercel sandbox, RHEL 9, Amazon Linux + # 2/2023, Debian 11+, Ubuntu 20.04+). + container: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install protoc (apt) + run: | + apt-get update + apt-get install -y --no-install-recommends protobuf-compiler + + - name: Cache Cargo build + uses: Swatinem/rust-cache@v2 + with: + workspaces: . -> target + shared-key: sdk-typescript-x86_64-unknown-linux-gnu + + - name: Install npm dependencies + working-directory: sdk/typescript + run: npm ci + + - name: Build native addon (linux x64 gnu) + working-directory: sdk/typescript + run: npm run build -- --target x86_64-unknown-linux-gnu + + - name: Upload .node artifact + uses: actions/upload-artifact@v4 + with: + name: bindings-x86_64-unknown-linux-gnu + path: sdk/typescript/codegraff-sdk.*.node + if-no-files-found: error + retention-days: 7 + publish: name: publish to npm - needs: [verify-tag, build] + needs: [verify-tag, build, build-linux] runs-on: ubuntu-latest environment: name: npm-publish diff --git a/sdk/typescript/npm/darwin-arm64/package.json b/sdk/typescript/npm/darwin-arm64/package.json index eed7182f..fdf3c0c4 100644 --- a/sdk/typescript/npm/darwin-arm64/package.json +++ b/sdk/typescript/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-arm64", - "version": "0.1.10", + "version": "0.1.12", "os": [ "darwin" ], diff --git a/sdk/typescript/npm/darwin-x64/package.json b/sdk/typescript/npm/darwin-x64/package.json index 0d3689e1..86602413 100644 --- a/sdk/typescript/npm/darwin-x64/package.json +++ b/sdk/typescript/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-x64", - "version": "0.1.10", + "version": "0.1.12", "os": [ "darwin" ], diff --git a/sdk/typescript/npm/linux-arm64-gnu/package.json b/sdk/typescript/npm/linux-arm64-gnu/package.json index a6c89938..26327ede 100644 --- a/sdk/typescript/npm/linux-arm64-gnu/package.json +++ b/sdk/typescript/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-arm64-gnu", - "version": "0.1.10", + "version": "0.1.12", "os": [ "linux" ], diff --git a/sdk/typescript/npm/linux-x64-gnu/package.json b/sdk/typescript/npm/linux-x64-gnu/package.json index 33a51fc6..91b20dee 100644 --- a/sdk/typescript/npm/linux-x64-gnu/package.json +++ b/sdk/typescript/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-x64-gnu", - "version": "0.1.10", + "version": "0.1.12", "os": [ "linux" ], diff --git a/sdk/typescript/npm/win32-x64-msvc/package.json b/sdk/typescript/npm/win32-x64-msvc/package.json index 068ea3b7..1c2a64ac 100644 --- a/sdk/typescript/npm/win32-x64-msvc/package.json +++ b/sdk/typescript/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-win32-x64-msvc", - "version": "0.1.10", + "version": "0.1.12", "os": [ "win32" ], diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index 0bb6c588..d929b224 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk", - "version": "0.1.10", + "version": "0.1.12", "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", "main": "lib.js", "types": "lib.d.ts", @@ -43,11 +43,11 @@ "tsx": "^4.20.0" }, "optionalDependencies": { - "@codegraff/sdk-darwin-arm64": "0.1.10", - "@codegraff/sdk-darwin-x64": "0.1.10", - "@codegraff/sdk-linux-arm64-gnu": "0.1.10", - "@codegraff/sdk-linux-x64-gnu": "0.1.10", - "@codegraff/sdk-win32-x64-msvc": "0.1.10" + "@codegraff/sdk-darwin-arm64": "0.1.12", + "@codegraff/sdk-darwin-x64": "0.1.12", + "@codegraff/sdk-linux-arm64-gnu": "0.1.12", + "@codegraff/sdk-linux-x64-gnu": "0.1.12", + "@codegraff/sdk-win32-x64-msvc": "0.1.12" }, "files": [ "lib.js", From 706dcd8fc50d984e456cffdb66bf727257904d09 Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Wed, 20 May 2026 16:16:49 +0800 Subject: [PATCH 11/15] release: sdk/typescript v0.1.13 Swap linux container from napi-rs/nodejs-rust:lts-debian to plain node:20-bullseye. The napi-rs image presets a cross-compile toolchain (CC=x86_64-unknown-linux-gnu-gcc with sysroot), which made aws-lc-sys fail with '-fuse-ld=lld: unrecognized'. We want native compilation in Debian Bullseye, not cross from a hosted sysroot. Install rustup + protoc + build-essential inside the container explicitly. Adds an objdump verification step that prints the .node's GLIBC requirements so a regression to a newer GLIBC ABI is visible at build time. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/sdk-typescript-publish.yml | 37 +++++++++++++++---- sdk/typescript/npm/darwin-arm64/package.json | 2 +- sdk/typescript/npm/darwin-x64/package.json | 2 +- .../npm/linux-arm64-gnu/package.json | 2 +- sdk/typescript/npm/linux-x64-gnu/package.json | 2 +- .../npm/win32-x64-msvc/package.json | 2 +- sdk/typescript/package.json | 12 +++--- 7 files changed, 41 insertions(+), 18 deletions(-) diff --git a/.github/workflows/sdk-typescript-publish.yml b/.github/workflows/sdk-typescript-publish.yml index 7bb3e7e5..102c6f9e 100644 --- a/.github/workflows/sdk-typescript-publish.yml +++ b/.github/workflows/sdk-typescript-publish.yml @@ -142,18 +142,35 @@ jobs: name: build (x86_64-unknown-linux-gnu) needs: verify-tag runs-on: ubuntu-latest - # Debian Bullseye = GLIBC 2.31, broadly compatible with every - # production Linux deployment (Vercel sandbox, RHEL 9, Amazon Linux - # 2/2023, Debian 11+, Ubuntu 20.04+). - container: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian + # Debian Bullseye base = GLIBC 2.31. NATIVE compile (no cross-sysroot + # tricks) so aws-lc-sys is happy. node:20-bullseye gives us Node + a + # plain Debian Bullseye toolchain; we install Rust + protoc explicitly. + # The resulting .node loads on every Linux with GLIBC >= 2.31: + # Vercel sandbox, RHEL 9, Amazon Linux 2/2023, Debian 11+, Ubuntu 20+. + container: node:20-bullseye steps: - name: Checkout uses: actions/checkout@v4 - - name: Install protoc (apt) + - name: Install build deps (protoc, build-essential) run: | apt-get update - apt-get install -y --no-install-recommends protobuf-compiler + apt-get install -y --no-install-recommends \ + protobuf-compiler \ + build-essential \ + pkg-config \ + ca-certificates \ + curl + + - name: Install Rust toolchain (matches rust-toolchain.toml) + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + - name: Verify rustc + cargo + run: | + rustc --version + cargo --version - name: Cache Cargo build uses: Swatinem/rust-cache@v2 @@ -165,10 +182,16 @@ jobs: working-directory: sdk/typescript run: npm ci - - name: Build native addon (linux x64 gnu) + - name: Build native addon (linux x64 gnu, native debian-bullseye) working-directory: sdk/typescript run: npm run build -- --target x86_64-unknown-linux-gnu + - name: Verify .node GLIBC requirement is <= 2.31 + working-directory: sdk/typescript + run: | + ls -lh codegraff-sdk.*.node + objdump -T codegraff-sdk.linux-x64-gnu.node | grep GLIBC | sort -u | tail -10 || true + - name: Upload .node artifact uses: actions/upload-artifact@v4 with: diff --git a/sdk/typescript/npm/darwin-arm64/package.json b/sdk/typescript/npm/darwin-arm64/package.json index fdf3c0c4..1222e0b7 100644 --- a/sdk/typescript/npm/darwin-arm64/package.json +++ b/sdk/typescript/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-arm64", - "version": "0.1.12", + "version": "0.1.13", "os": [ "darwin" ], diff --git a/sdk/typescript/npm/darwin-x64/package.json b/sdk/typescript/npm/darwin-x64/package.json index 86602413..53dc9911 100644 --- a/sdk/typescript/npm/darwin-x64/package.json +++ b/sdk/typescript/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-x64", - "version": "0.1.12", + "version": "0.1.13", "os": [ "darwin" ], diff --git a/sdk/typescript/npm/linux-arm64-gnu/package.json b/sdk/typescript/npm/linux-arm64-gnu/package.json index 26327ede..178a6792 100644 --- a/sdk/typescript/npm/linux-arm64-gnu/package.json +++ b/sdk/typescript/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-arm64-gnu", - "version": "0.1.12", + "version": "0.1.13", "os": [ "linux" ], diff --git a/sdk/typescript/npm/linux-x64-gnu/package.json b/sdk/typescript/npm/linux-x64-gnu/package.json index 91b20dee..3c67c436 100644 --- a/sdk/typescript/npm/linux-x64-gnu/package.json +++ b/sdk/typescript/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-x64-gnu", - "version": "0.1.12", + "version": "0.1.13", "os": [ "linux" ], diff --git a/sdk/typescript/npm/win32-x64-msvc/package.json b/sdk/typescript/npm/win32-x64-msvc/package.json index 1c2a64ac..6bcfcd70 100644 --- a/sdk/typescript/npm/win32-x64-msvc/package.json +++ b/sdk/typescript/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-win32-x64-msvc", - "version": "0.1.12", + "version": "0.1.13", "os": [ "win32" ], diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index d929b224..edf6fa90 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk", - "version": "0.1.12", + "version": "0.1.13", "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", "main": "lib.js", "types": "lib.d.ts", @@ -43,11 +43,11 @@ "tsx": "^4.20.0" }, "optionalDependencies": { - "@codegraff/sdk-darwin-arm64": "0.1.12", - "@codegraff/sdk-darwin-x64": "0.1.12", - "@codegraff/sdk-linux-arm64-gnu": "0.1.12", - "@codegraff/sdk-linux-x64-gnu": "0.1.12", - "@codegraff/sdk-win32-x64-msvc": "0.1.12" + "@codegraff/sdk-darwin-arm64": "0.1.13", + "@codegraff/sdk-darwin-x64": "0.1.13", + "@codegraff/sdk-linux-arm64-gnu": "0.1.13", + "@codegraff/sdk-linux-x64-gnu": "0.1.13", + "@codegraff/sdk-win32-x64-msvc": "0.1.13" }, "files": [ "lib.js", From 313caad4427706bb257ebc5da93b5f07f113dd09 Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Wed, 20 May 2026 16:27:03 +0800 Subject: [PATCH 12/15] release: sdk/typescript v0.1.14 Linux container: install protoc from the official release tarball instead of apt's protobuf-compiler 3.12. The apt package didn't ship the google/protobuf/*.proto well-known files where prost-build looks for them, so forge_repo's build script failed with 'google/protobuf/timestamp.proto: File not found'. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/sdk-typescript-publish.yml | 18 +++++++++++++++--- sdk/typescript/npm/darwin-arm64/package.json | 2 +- sdk/typescript/npm/darwin-x64/package.json | 2 +- .../npm/linux-arm64-gnu/package.json | 2 +- sdk/typescript/npm/linux-x64-gnu/package.json | 2 +- sdk/typescript/npm/win32-x64-msvc/package.json | 2 +- sdk/typescript/package.json | 12 ++++++------ 7 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.github/workflows/sdk-typescript-publish.yml b/.github/workflows/sdk-typescript-publish.yml index 102c6f9e..6c5da819 100644 --- a/.github/workflows/sdk-typescript-publish.yml +++ b/.github/workflows/sdk-typescript-publish.yml @@ -152,15 +152,27 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install build deps (protoc, build-essential) + - name: Install build deps (build-essential, curl, unzip) run: | apt-get update apt-get install -y --no-install-recommends \ - protobuf-compiler \ build-essential \ pkg-config \ ca-certificates \ - curl + curl \ + unzip + + - name: Install protoc (release tarball, includes well-knowns) + # Debian Bullseye apt protobuf-compiler is 3.12 and doesn't ship the + # google/protobuf/*.proto well-known files where prost-build looks. + # Pull protoc 29.3 from the official release which bundles them. + run: | + PROTOC_VERSION=29.3 + curl -fsSL "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip" -o /tmp/protoc.zip + unzip -o /tmp/protoc.zip -d /usr/local + chmod +x /usr/local/bin/protoc + protoc --version + ls /usr/local/include/google/protobuf/timestamp.proto - name: Install Rust toolchain (matches rust-toolchain.toml) run: | diff --git a/sdk/typescript/npm/darwin-arm64/package.json b/sdk/typescript/npm/darwin-arm64/package.json index 1222e0b7..d96a2685 100644 --- a/sdk/typescript/npm/darwin-arm64/package.json +++ b/sdk/typescript/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-arm64", - "version": "0.1.13", + "version": "0.1.14", "os": [ "darwin" ], diff --git a/sdk/typescript/npm/darwin-x64/package.json b/sdk/typescript/npm/darwin-x64/package.json index 53dc9911..1dd80aaa 100644 --- a/sdk/typescript/npm/darwin-x64/package.json +++ b/sdk/typescript/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-x64", - "version": "0.1.13", + "version": "0.1.14", "os": [ "darwin" ], diff --git a/sdk/typescript/npm/linux-arm64-gnu/package.json b/sdk/typescript/npm/linux-arm64-gnu/package.json index 178a6792..185e1ef2 100644 --- a/sdk/typescript/npm/linux-arm64-gnu/package.json +++ b/sdk/typescript/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-arm64-gnu", - "version": "0.1.13", + "version": "0.1.14", "os": [ "linux" ], diff --git a/sdk/typescript/npm/linux-x64-gnu/package.json b/sdk/typescript/npm/linux-x64-gnu/package.json index 3c67c436..5d6cff9e 100644 --- a/sdk/typescript/npm/linux-x64-gnu/package.json +++ b/sdk/typescript/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-x64-gnu", - "version": "0.1.13", + "version": "0.1.14", "os": [ "linux" ], diff --git a/sdk/typescript/npm/win32-x64-msvc/package.json b/sdk/typescript/npm/win32-x64-msvc/package.json index 6bcfcd70..7a8ca6c7 100644 --- a/sdk/typescript/npm/win32-x64-msvc/package.json +++ b/sdk/typescript/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-win32-x64-msvc", - "version": "0.1.13", + "version": "0.1.14", "os": [ "win32" ], diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index edf6fa90..d7d4d671 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk", - "version": "0.1.13", + "version": "0.1.14", "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", "main": "lib.js", "types": "lib.d.ts", @@ -43,11 +43,11 @@ "tsx": "^4.20.0" }, "optionalDependencies": { - "@codegraff/sdk-darwin-arm64": "0.1.13", - "@codegraff/sdk-darwin-x64": "0.1.13", - "@codegraff/sdk-linux-arm64-gnu": "0.1.13", - "@codegraff/sdk-linux-x64-gnu": "0.1.13", - "@codegraff/sdk-win32-x64-msvc": "0.1.13" + "@codegraff/sdk-darwin-arm64": "0.1.14", + "@codegraff/sdk-darwin-x64": "0.1.14", + "@codegraff/sdk-linux-arm64-gnu": "0.1.14", + "@codegraff/sdk-linux-x64-gnu": "0.1.14", + "@codegraff/sdk-win32-x64-msvc": "0.1.14" }, "files": [ "lib.js", From 43801ec4f01dbf47fcdc4d3aa5fa75a5d9df61b3 Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Wed, 20 May 2026 21:47:46 +0800 Subject: [PATCH 13/15] feat(sdk): 1:1 with CLI + native codedb on install (v0.2.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the npm SDK to feature parity with the Rust CLI's MCP support AND auto-equips it with codedb so 'npm install @codegraff/sdk' gives consumers the same toolset CLI users get out of the box. Three changes: 1. Expose forge_api's MCP config surface via napi: GraffApi::read_mcp_config(scope) -> JSON GraffApi::write_mcp_config(scope, json) -> () Same code path as 'forge mcp add' / 'forge mcp set' — SDK and CLI share one config file, true 1:1 parity. 2. Add lib.js convenience methods on Graff: graff.readMcpConfig(scope?) graff.writeMcpConfig(scope, config) 3. Auto-install + auto-register codedb on Graff.init(): - postinstall script downloads codedb- from the codedb GitHub release into bin/codedb at npm install time - Graff.init() detects the bundled binary and writes a User-scope MCP entry pointing at it (idempotent — only writes if missing or stale) - Forge then loads it on next chat call, so the agent has codedb's symbol-aware tools natively, no manual config required. Opt-out: CODEGRAFF_SKIP_POSTINSTALL=1 disables download, CODEGRAFF_SKIP_AUTO_MCP=1 disables registration. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + sdk/typescript/lib.d.ts | 12 ++++ sdk/typescript/lib.js | 66 ++++++++++++++++++++++ sdk/typescript/package.json | 19 ++++--- sdk/typescript/scripts/postinstall.cjs | 78 ++++++++++++++++++++++++++ sdk/typescript/src/lib.rs | 49 +++++++++++++++- 6 files changed, 215 insertions(+), 10 deletions(-) create mode 100755 sdk/typescript/scripts/postinstall.cjs diff --git a/.gitignore b/.gitignore index e36e884b..9d7acb0a 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,4 @@ bench/__pycache__ .ai/ slides/ .codex/ +sdk/typescript/bin/ diff --git a/sdk/typescript/lib.d.ts b/sdk/typescript/lib.d.ts index 80af2675..11cffae3 100644 --- a/sdk/typescript/lib.d.ts +++ b/sdk/typescript/lib.d.ts @@ -4,6 +4,11 @@ import { GraffApi, ChatStreamHandle, newConversationId, version } from "./index.js"; export { GraffApi, ChatStreamHandle, newConversationId, version }; +/** Shape of one entry in an MCP config map (forge_domain::McpServerConfig). */ +export type McpServerEntry = + | { command: string; args?: string[]; env?: Record; timeout?: number; disable?: boolean } + | { url: string; headers?: Record; timeout?: number; disable?: boolean }; + // ── Options ────────────────────────────────────────────────────────────── @@ -154,6 +159,13 @@ export class Graff { /** Create a multi-turn session that shares this Graff's underlying GraffApi. */ session(opts?: GraffSessionOptions): GraffSession; + /** Read merged MCP server config from disk. `scope` is "User" / "Local" / + * undefined (merged-both, Local wins). */ + readMcpConfig(scope?: "User" | "Local"): Promise>; + /** Write MCP server config at `scope`. Same persistence path as + * `forge mcp add` / `forge mcp set`. */ + writeMcpConfig(scope: "User" | "Local", config: Record): Promise; + // Auth (BYOK) upsertCredential( providerId: string, diff --git a/sdk/typescript/lib.js b/sdk/typescript/lib.js index 07770ecb..80f09b9c 100644 --- a/sdk/typescript/lib.js +++ b/sdk/typescript/lib.js @@ -13,9 +13,53 @@ // rename / compact / agents / trajectory). const native = require("./index.js"); +const fs = require("node:fs"); +const path = require("node:path"); const { GraffApi, ChatStreamHandle, version, newConversationId } = native; +/** Path to a bundled codedb binary the postinstall script downloaded (or + * undefined if not present — e.g. when --ignore-scripts was used or no + * codedb release exists for this triple). */ +function bundledCodedbPath() { + const candidate = path.join(__dirname, "bin", process.platform === "win32" ? "codedb.exe" : "codedb"); + try { + fs.accessSync(candidate, fs.constants.X_OK); + return candidate; + } catch { + return undefined; + } +} + +/** If codedb is bundled, make sure it's registered as a User-scope MCP server + * so the forge agent picks it up natively. Idempotent: writes the entry only + * if missing or pointing at a different path. Non-fatal on failure (we still + * want Graff.init to succeed even if MCP config write barfs). */ +async function autoRegisterCodedb(api) { + if (process.env.CODEGRAFF_SKIP_AUTO_MCP === "1") return; + const codedb = bundledCodedbPath(); + if (!codedb) return; + try { + const currentJson = await api.readMcpConfig("User"); + const current = JSON.parse(currentJson) || {}; + const servers = current.mcpServers || {}; + const existing = servers.codedb; + const matches = existing && existing.command === codedb && Array.isArray(existing.args) + && existing.args.length === 1 && existing.args[0] === "mcp"; + if (matches) return; + const next = { + ...current, + mcpServers: { ...servers, codedb: { command: codedb, args: ["mcp"] } }, + }; + await api.writeMcpConfig("User", JSON.stringify(next)); + } catch (e) { + // Don't block init on registration trouble. + if (process.env.CODEGRAFF_DEBUG === "1") { + console.warn("[codegraff] autoRegisterCodedb failed:", e?.message ?? e); + } + } +} + /** Pull events off a ChatStreamHandle and yield decoded objects. Calls * cancel() on `return()` (e.g. when the caller breaks out of `for await`) * so the underlying tokio task is aborted. */ @@ -136,6 +180,10 @@ class Graff { if (opts.apiKey) { await graff.upsertCredential(opts.provider, opts.apiKey, opts.extraParams); } + // Auto-register bundled codedb as an MCP server so the agent has its + // symbol-aware tools available natively (1:1 with what the Rust CLI + // gets when `forge mcp add codedb -- codedb mcp` is run by hand). + await autoRegisterCodedb(graff._api); return graff; } @@ -158,6 +206,24 @@ class Graff { return new GraffSession({ ...opts, _api: this._api }); } + // ── MCP server config ─────────────────────────────────────────────────── + + /** Read merged MCP server config from disk. Returns a `{name: serverConfig}` + * object. `scope` is "User", "Local", or undefined for merged-both. Same + * shape forge's CLI reads. */ + async readMcpConfig(scope) { + const raw = await this._api.readMcpConfig(scope ?? null); + return JSON.parse(raw); + } + + /** Write MCP server config to disk at `scope` ("User" or "Local"). `config` + * is a `{name: {command, args?, env?} | {url, headers?}}` map. Persists + * through the same code path as `forge mcp add/set` so CLI + SDK share + * one configuration file. */ + async writeMcpConfig(scope, config) { + await this._api.writeMcpConfig(scope, JSON.stringify(config ?? {})); + } + // ── Auth (BYOK) ────────────────────────────────────────────────────────── /** Upsert an API key credential for a provider. After this returns, any diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index d7d4d671..cdf8bbb0 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk", - "version": "0.1.14", + "version": "0.2.0", "description": "TypeScript / Node SDK for the codegraff agent (N-API bindings).", "main": "lib.js", "types": "lib.d.ts", @@ -35,7 +35,8 @@ "smoke": "node examples/smoke.mjs", "demo": "tsx examples/agent-demo.ts", "compare": "tsx examples/compare.ts", - "benchmark": "tsx examples/benchmark.ts" + "benchmark": "tsx examples/benchmark.ts", + "postinstall": "node scripts/postinstall.cjs" }, "devDependencies": { "@cursor/sdk": "^1.0.12", @@ -43,16 +44,18 @@ "tsx": "^4.20.0" }, "optionalDependencies": { - "@codegraff/sdk-darwin-arm64": "0.1.14", - "@codegraff/sdk-darwin-x64": "0.1.14", - "@codegraff/sdk-linux-arm64-gnu": "0.1.14", - "@codegraff/sdk-linux-x64-gnu": "0.1.14", - "@codegraff/sdk-win32-x64-msvc": "0.1.14" + "@codegraff/sdk-darwin-arm64": "0.2.0", + "@codegraff/sdk-darwin-x64": "0.2.0", + "@codegraff/sdk-linux-arm64-gnu": "0.2.0", + "@codegraff/sdk-linux-x64-gnu": "0.2.0", + "@codegraff/sdk-win32-x64-msvc": "0.2.0" }, "files": [ "lib.js", "lib.d.ts", "index.js", - "index.d.ts" + "index.d.ts", + "scripts/", + "bin/" ] } diff --git a/sdk/typescript/scripts/postinstall.cjs b/sdk/typescript/scripts/postinstall.cjs new file mode 100755 index 00000000..1309f124 --- /dev/null +++ b/sdk/typescript/scripts/postinstall.cjs @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/** + * @codegraff/sdk postinstall — fetches the codedb binary for the host + * platform and stores it next to the package. lib.js then auto-registers + * it as an MCP server on Graff.init() so the forge agent natively has + * codedb's symbol-aware search tools. + * + * Disabled with CODEGRAFF_SKIP_POSTINSTALL=1 or npm --ignore-scripts. + */ +const fs = require("node:fs"); +const path = require("node:path"); +const https = require("node:https"); +const crypto = require("node:crypto"); + +if (process.env.CODEGRAFF_SKIP_POSTINSTALL === "1") { + console.log("[codegraff postinstall] skipped (CODEGRAFF_SKIP_POSTINSTALL=1)"); + process.exit(0); +} + +const PLATFORM_MAP = { + "darwin-arm64": "codedb-darwin-arm64", + "darwin-x64": "codedb-darwin-x86_64", + "linux-x64": "codedb-linux-x86_64", +}; + +const key = `${process.platform}-${process.arch}`; +const asset = PLATFORM_MAP[key]; +if (!asset) { + console.log(`[codegraff postinstall] no codedb release for ${key} — skipping. ` + + `Agent will fall back to standard tools (Read/Grep/Bash).`); + process.exit(0); +} + +const pkgRoot = path.resolve(__dirname, ".."); +const binDir = path.join(pkgRoot, "bin"); +const binPath = path.join(binDir, "codedb"); + +function get(url, redirects = 5) { + return new Promise((resolve, reject) => { + https.get(url, { headers: { "User-Agent": "codegraff-sdk-postinstall" } }, (res) => { + if ((res.statusCode === 301 || res.statusCode === 302) && redirects > 0 && res.headers.location) { + res.resume(); + return resolve(get(res.headers.location, redirects - 1)); + } + if (res.statusCode !== 200) { + return reject(new Error(`HTTP ${res.statusCode} for ${url}`)); + } + const chunks = []; + res.on("data", c => chunks.push(c)); + res.on("end", () => resolve(Buffer.concat(chunks))); + res.on("error", reject); + }).on("error", reject); + }); +} + +(async () => { + console.log(`[codegraff postinstall] fetching ${asset} for ${key}`); + const releaseUrl = `https://github.com/justrach/codedb/releases/latest/download/${asset}`; + const binary = await get(releaseUrl); + const checksumsText = (await get("https://github.com/justrach/codedb/releases/latest/download/checksums.sha256")).toString("utf8"); + const expectedHash = checksumsText.split("\n").map(l => l.trim().split(/\s+/)).find(([, name]) => name === asset)?.[0]; + if (expectedHash) { + const actualHash = crypto.createHash("sha256").update(binary).digest("hex"); + if (actualHash !== expectedHash) { + throw new Error(`checksum mismatch for ${asset}: expected ${expectedHash}, got ${actualHash}`); + } + console.log(`[codegraff postinstall] sha256 verified`); + } else { + console.log(`[codegraff postinstall] no checksum for ${asset} (continuing)`); + } + fs.mkdirSync(binDir, { recursive: true }); + fs.writeFileSync(binPath, binary, { mode: 0o755 }); + console.log(`[codegraff postinstall] wrote ${binPath} (${binary.length} bytes)`); +})().catch((e) => { + console.warn(`[codegraff postinstall] ${e.message} — agent will fall back to standard tools.`); + // Postinstall failures must NOT block npm install. Continue silently. + process.exit(0); +}); diff --git a/sdk/typescript/src/lib.rs b/sdk/typescript/src/lib.rs index 2eb6323f..6f5415e1 100644 --- a/sdk/typescript/src/lib.rs +++ b/sdk/typescript/src/lib.rs @@ -14,8 +14,8 @@ use forge_api::{API, ChatRequest, ForgeAPI}; use forge_config::ForgeConfig; use forge_domain::{ AgentId, ApiKey, ApiKeyResponse, AuthContext, AuthContextRequest, AuthContextResponse, - AuthMethod, ChatResponse, Conversation, ConversationId, Event, ModelId, ProviderId, URLParam, - URLParamValue, + AuthMethod, ChatResponse, Conversation, ConversationId, Event, McpConfig, ModelId, + ProviderId, Scope, URLParam, URLParamValue, }; use forge_stream::MpscStream; use futures::StreamExt; @@ -144,6 +144,51 @@ impl GraffApi { Ok(()) } + /// Read merged MCP server configuration from disk. + /// + /// `scope` is "User", "Local", or `None` to merge both (Local takes + /// precedence). The returned JSON string mirrors forge_domain's + /// `McpConfig` shape and is identical to what `forge mcp ls` consumes — + /// SDK and CLI users see the same config. + #[napi] + pub async fn read_mcp_config(&self, scope: Option) -> Result { + let scope_arg = match scope.as_deref() { + Some("User") | Some("user") => Some(Scope::User), + Some("Local") | Some("local") => Some(Scope::Local), + Some(other) => return Err(err(format!( + "invalid scope '{}': use 'User' or 'Local'", other + ))), + None => None, + }; + let config = self.inner.read_mcp_config(scope_arg.as_ref()).await.map_err(map_err)?; + to_json(&config, "McpConfig") + } + + /// Write MCP server configuration to disk at the given scope. + /// + /// `scope` is "User" (writes to `~/.forge/mcp.json`) or "Local" + /// (writes to `/.forge/mcp.json`). `config_json` must + /// deserialize into forge_domain's `McpConfig` — a map of + /// `{ serverName: { command, args, env? } | { url, headers? } }`. + /// + /// Calling this mirrors `forge mcp add` / `forge mcp set`, so SDK + /// consumers register MCP servers (codedb, etc.) exactly the same + /// way CLI users do. + #[napi] + pub async fn write_mcp_config(&self, scope: String, config_json: String) -> Result<()> { + let scope = match scope.as_str() { + "User" | "user" => Scope::User, + "Local" | "local" => Scope::Local, + other => return Err(err(format!( + "invalid scope '{}': use 'User' or 'Local'", other + ))), + }; + let config: McpConfig = serde_json::from_str(&config_json) + .map_err(|e| err(format!("invalid McpConfig JSON: {e}")))?; + self.inner.write_mcp_config(&scope, &config).await.map_err(map_err)?; + Ok(()) + } + /// Send a chat request and return a streaming handle. /// /// If `conversation_id` is omitted a fresh `Conversation` is created and From 9a106636290f86c531da2050dbfa2ab9ef4a5bc8 Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Wed, 20 May 2026 21:47:46 +0800 Subject: [PATCH 14/15] release: sdk/typescript v0.2.0 (subpackage version sync) Co-Authored-By: Claude Opus 4.7 (1M context) --- sdk/typescript/npm/darwin-arm64/package.json | 2 +- sdk/typescript/npm/darwin-x64/package.json | 2 +- sdk/typescript/npm/linux-arm64-gnu/package.json | 2 +- sdk/typescript/npm/linux-x64-gnu/package.json | 2 +- sdk/typescript/npm/win32-x64-msvc/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/typescript/npm/darwin-arm64/package.json b/sdk/typescript/npm/darwin-arm64/package.json index d96a2685..edc04c0b 100644 --- a/sdk/typescript/npm/darwin-arm64/package.json +++ b/sdk/typescript/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-arm64", - "version": "0.1.14", + "version": "0.2.0", "os": [ "darwin" ], diff --git a/sdk/typescript/npm/darwin-x64/package.json b/sdk/typescript/npm/darwin-x64/package.json index 1dd80aaa..47ec56e1 100644 --- a/sdk/typescript/npm/darwin-x64/package.json +++ b/sdk/typescript/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-darwin-x64", - "version": "0.1.14", + "version": "0.2.0", "os": [ "darwin" ], diff --git a/sdk/typescript/npm/linux-arm64-gnu/package.json b/sdk/typescript/npm/linux-arm64-gnu/package.json index 185e1ef2..e2b6088b 100644 --- a/sdk/typescript/npm/linux-arm64-gnu/package.json +++ b/sdk/typescript/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-arm64-gnu", - "version": "0.1.14", + "version": "0.2.0", "os": [ "linux" ], diff --git a/sdk/typescript/npm/linux-x64-gnu/package.json b/sdk/typescript/npm/linux-x64-gnu/package.json index 5d6cff9e..a5513cac 100644 --- a/sdk/typescript/npm/linux-x64-gnu/package.json +++ b/sdk/typescript/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-linux-x64-gnu", - "version": "0.1.14", + "version": "0.2.0", "os": [ "linux" ], diff --git a/sdk/typescript/npm/win32-x64-msvc/package.json b/sdk/typescript/npm/win32-x64-msvc/package.json index 7a8ca6c7..983c181f 100644 --- a/sdk/typescript/npm/win32-x64-msvc/package.json +++ b/sdk/typescript/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@codegraff/sdk-win32-x64-msvc", - "version": "0.1.14", + "version": "0.2.0", "os": [ "win32" ], From ea09191ff2aa9ce2420b4640ac559ff442354b3d Mon Sep 17 00:00:00 2001 From: Rach Pradhan <54503978+justrach@users.noreply.github.com> Date: Thu, 21 May 2026 00:39:49 +0800 Subject: [PATCH 15/15] fix(sdk): drop broken @codegraff/sdk-linux-arm64-gnu optional dep claim CI matrix doesn't currently build aarch64-unknown-linux-gnu, so the subpackage @codegraff/sdk-linux-arm64-gnu has never been published to npm. Listing it in optionalDependencies misleads tooling and confuses arm64-linux users who get a silent skip + a 'cannot find module' at runtime. Drops the claim until the build matrix actually produces it. See follow- up for properly adding aarch64-unknown-linux-gnu to the CI matrix. Address P1 finding from PR #87 swarm review (4330389512). Co-Authored-By: Claude Opus 4.7 (1M context) --- sdk/typescript/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index cdf8bbb0..a72b5a9f 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -46,7 +46,6 @@ "optionalDependencies": { "@codegraff/sdk-darwin-arm64": "0.2.0", "@codegraff/sdk-darwin-x64": "0.2.0", - "@codegraff/sdk-linux-arm64-gnu": "0.2.0", "@codegraff/sdk-linux-x64-gnu": "0.2.0", "@codegraff/sdk-win32-x64-msvc": "0.2.0" },