diff --git a/.github/workflows/sdk-typescript-publish.yml b/.github/workflows/sdk-typescript-publish.yml new file mode 100644 index 000000000..6c5da819f --- /dev/null +++ b/.github/workflows/sdk-typescript-publish.yml @@ -0,0 +1,268 @@ +# 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 + # 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 + # 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: + - 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 + + build-linux: + name: build (x86_64-unknown-linux-gnu) + needs: verify-tag + runs-on: ubuntu-latest + # 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 build deps (build-essential, curl, unzip) + run: | + apt-get update + apt-get install -y --no-install-recommends \ + build-essential \ + pkg-config \ + ca-certificates \ + 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: | + 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 + 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, 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: + 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, build-linux] + 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" diff --git a/.github/workflows/sdk-typescript.yml b/.github/workflows/sdk-typescript.yml new file mode 100644 index 000000000..d8a46d79b --- /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 d9e9b17c1..9d7acb0af 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/ @@ -41,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/ @@ -48,3 +57,4 @@ bench/__pycache__ .ai/ slides/ .codex/ +sdk/typescript/bin/ diff --git a/Cargo.lock b/Cargo.lock index 752635d60..6704fec5b 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 713940979..737473ead 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/*"] +members = ["crates/*", "sdk/typescript"] resolver = "2" @@ -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/app.rs b/crates/forge_app/src/app.rs index 199abebc6..e42510c5c 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 3515b74e7..92dbbb37b 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 145261744..5bf4585ce 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 2adc8fd1f..9931c44b4 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 c33c8349b..1e02fbb0d 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 5a28d4821..6659e3a7e 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_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 c3bebe419..9ff97b357 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 0b1b42f35..4a9e0d8ad 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 c52482f50..0cc8fed98 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_config/.forge.toml b/crates/forge_config/.forge.toml index 7df89c283..7a59a9bbb 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 9c29ef222..1172ab9aa 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 e24cd9d73..8ed438b58 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_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 9192079f1..5c20b9c8f 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_main/src/ui.rs b/crates/forge_main/src/ui.rs index fc62b2b29..35a070a3e 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/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 1cd286cd6..cafb5f151 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" } ] diff --git a/forge.schema.json b/forge.schema.json index e6add9e2f..c051a698e 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/.gitignore b/sdk/typescript/.gitignore new file mode 100644 index 000000000..0d851ab3c --- /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 000000000..a84f0dcbf --- /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 000000000..09b3c9dde --- /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 000000000..0f1b01002 --- /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 000000000..e4ceca364 --- /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 000000000..ae43c60da --- /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 000000000..06d3d2295 --- /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 000000000..66368fffa --- /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 000000000..981d78115 --- /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/lib.d.ts b/sdk/typescript/lib.d.ts new file mode 100644 index 000000000..11cffae31 --- /dev/null +++ b/sdk/typescript/lib.d.ts @@ -0,0 +1,194 @@ +// 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 }; +/** 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 ────────────────────────────────────────────────────────────── + +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; +} + +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 = + | "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`. */ + /** 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; + + /** 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, + 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 000000000..80f09b9c0 --- /dev/null +++ b/sdk/typescript/lib.js @@ -0,0 +1,307 @@ +// 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 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. */ +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; + } + + /** 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); + } + // 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; + } + + /** 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 }); + } + + // ── 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 + * 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/npm/darwin-arm64/README.md b/sdk/typescript/npm/darwin-arm64/README.md new file mode 100644 index 000000000..504a5c701 --- /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 000000000..edc04c0b0 --- /dev/null +++ b/sdk/typescript/npm/darwin-arm64/package.json @@ -0,0 +1,24 @@ +{ + "name": "@codegraff/sdk-darwin-arm64", + "version": "0.2.0", + "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" + }, + "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/README.md b/sdk/typescript/npm/darwin-x64/README.md new file mode 100644 index 000000000..e296bf5ca --- /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 000000000..47ec56e12 --- /dev/null +++ b/sdk/typescript/npm/darwin-x64/package.json @@ -0,0 +1,24 @@ +{ + "name": "@codegraff/sdk-darwin-x64", + "version": "0.2.0", + "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" + }, + "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/README.md b/sdk/typescript/npm/linux-arm64-gnu/README.md new file mode 100644 index 000000000..50c6a2812 --- /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 000000000..e2b6088bd --- /dev/null +++ b/sdk/typescript/npm/linux-arm64-gnu/package.json @@ -0,0 +1,27 @@ +{ + "name": "@codegraff/sdk-linux-arm64-gnu", + "version": "0.2.0", + "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" + ], + "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/README.md b/sdk/typescript/npm/linux-x64-gnu/README.md new file mode 100644 index 000000000..4c7477751 --- /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 000000000..a5513cac2 --- /dev/null +++ b/sdk/typescript/npm/linux-x64-gnu/package.json @@ -0,0 +1,27 @@ +{ + "name": "@codegraff/sdk-linux-x64-gnu", + "version": "0.2.0", + "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" + ], + "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/README.md b/sdk/typescript/npm/win32-x64-msvc/README.md new file mode 100644 index 000000000..7514f7f9a --- /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 000000000..983c181fb --- /dev/null +++ b/sdk/typescript/npm/win32-x64-msvc/package.json @@ -0,0 +1,24 @@ +{ + "name": "@codegraff/sdk-win32-x64-msvc", + "version": "0.2.0", + "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" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/justrach/codegraff.git", + "directory": "sdk/typescript/npm/win32-x64-msvc" + } +} diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json new file mode 100644 index 000000000..9ed498d39 --- /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 000000000..a72b5a9f7 --- /dev/null +++ b/sdk/typescript/package.json @@ -0,0 +1,60 @@ +{ + "name": "@codegraff/sdk", + "version": "0.2.0", + "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", + "postinstall": "node scripts/postinstall.cjs" + }, + "devDependencies": { + "@cursor/sdk": "^1.0.12", + "@napi-rs/cli": "^2.18.4", + "tsx": "^4.20.0" + }, + "optionalDependencies": { + "@codegraff/sdk-darwin-arm64": "0.2.0", + "@codegraff/sdk-darwin-x64": "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", + "scripts/", + "bin/" + ] +} diff --git a/sdk/typescript/scripts/postinstall.cjs b/sdk/typescript/scripts/postinstall.cjs new file mode 100755 index 000000000..1309f124a --- /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 new file mode 100644 index 000000000..6f5415e19 --- /dev/null +++ b/sdk/typescript/src/lib.rs @@ -0,0 +1,402 @@ +//! 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::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, ApiKey, ApiKeyResponse, AuthContext, AuthContextRequest, AuthContextResponse, + AuthMethod, ChatResponse, Conversation, ConversationId, Event, McpConfig, ModelId, + ProviderId, Scope, URLParam, URLParamValue, +}; +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 }) + } + + /// 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(()) + } + + /// 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 + /// 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 000000000..3f4704753 --- /dev/null +++ b/sdk/typescript/src/wire.rs @@ -0,0 +1,158 @@ +//! 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 }, + EndHookRearmLimitReached { 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 } + } + InterruptionReason::EndHookRearmLimitReached { limit } => { + WireInterrupt::EndHookRearmLimitReached { limit } + } + } + } +} + +fn cause_to_string(cause: &Cause) -> String { + cause.as_str().to_string() +}