From c857bbe809509e41859020a182ccc32eae1119c1 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:49:32 +0000 Subject: [PATCH 1/7] Delete the CLI Everything the CLI built because it needed an agent front-end -- sessions and resume, skills, the TUI, print and RPC modes, model selection -- pi supplies, so the extension replaces it rather than reimplementing it. The cua act executor path and the --print -o jsonl telemetry go with it. Removes a third of the rename surface before the rename touches it: 45 of 122 files that mention cua, and 8,946 lines. ptywright stays: it is independently useful and published, but it now has no in-repo consumer, which the architecture doc records. --- .agents/skills/release/SKILL.md | 55 +- .agents/skills/update-docs/SKILL.md | 8 +- .github/workflows/ci.yml | 68 -- .github/workflows/release-cua-cli.yml | 253 ----- README.md | 384 ++------ docs/architecture.md | 108 +-- docs/cua-cli-harness-migration.md | 13 - docs/npm-releases.md | 79 +- package-lock.json | 203 +++- package.json | 4 +- packages/agent/examples/shared/tools.ts | 5 +- packages/agent/test/attach-session.test.ts | 3 +- .../test/example-provider-matrix.test.ts | 2 +- packages/ai/README.md | 2 +- packages/ai/src/tool-catalog.ts | 4 +- packages/cli/CHANGELOG.md | 189 ---- packages/cli/README.md | 274 ------ packages/cli/package.json | 50 - packages/cli/src/action/harness-runner.ts | 178 ---- packages/cli/src/action/prompts.ts | 59 -- packages/cli/src/action/result.ts | 144 --- packages/cli/src/browser-act-input.ts | 176 ---- packages/cli/src/cli-executor.ts | 350 ------- packages/cli/src/cli-harness.ts | 862 ----------------- packages/cli/src/cli.ts | 345 ------- packages/cli/src/harness-browser.ts | 130 --- packages/cli/src/harness-models.ts | 67 -- packages/cli/src/harness-named-sessions.ts | 307 ------ packages/cli/src/harness-sessions.ts | 180 ---- packages/cli/src/harness-skills.ts | 138 --- packages/cli/src/harness.ts | 211 ----- packages/cli/src/output/harness-jsonl.ts | 193 ---- packages/cli/src/print.ts | 88 -- packages/cli/src/tui/debug-log.ts | 137 --- packages/cli/src/tui/diagnostics.ts | 75 -- packages/cli/src/tui/keybindings.ts | 61 -- packages/cli/src/tui/main.ts | 872 ------------------ packages/cli/src/tui/message-list.ts | 137 --- packages/cli/src/tui/model-picker.ts | 236 ----- packages/cli/src/tui/mutation-queue.ts | 37 - packages/cli/src/tui/screenshot-widget.ts | 27 - packages/cli/src/tui/slash-commands.ts | 109 --- packages/cli/src/tui/status-line.ts | 47 - packages/cli/src/tui/telemetry-footer.ts | 93 -- packages/cli/src/tui/themes.ts | 52 -- packages/cli/src/tui/tool-selection.ts | 128 --- packages/cli/src/tui/tools-picker.ts | 289 ------ packages/cli/src/tui/version.ts | 11 - packages/cli/test/action-runner.test.ts | 151 --- packages/cli/test/cli-executor.test.ts | 522 ----------- packages/cli/test/fixtures/fake-kernel.ts | 66 -- packages/cli/test/fixtures/harness.ts | 63 -- .../cli/test/fixtures/scripted-provider.ts | 247 ----- .../cli/test/fixtures/tui-fixture-runner.ts | 87 -- .../cli/test/fixtures/tui-fixtures/abort.json | 26 - .../cli/test/fixtures/tui-fixtures/error.json | 13 - .../tui-fixtures/interrupt-cancel.json | 25 - .../test/fixtures/tui-fixtures/model-arg.json | 10 - .../tui-fixtures/model-picker-cancel.json | 10 - .../fixtures/tui-fixtures/model-picker.json | 10 - .../test/fixtures/tui-fixtures/multiline.json | 13 - .../test/fixtures/tui-fixtures/resources.json | 33 - .../cli/test/fixtures/tui-fixtures/steer.json | 23 - .../test/fixtures/tui-fixtures/streaming.json | 15 - .../fixtures/tui-fixtures/tools-picker.json | 11 - .../tui-fixtures/tools-reset-on-model.json | 11 - packages/cli/test/harness-assembly.test.ts | 175 ---- packages/cli/test/harness-browser.test.ts | 46 - packages/cli/test/harness-models.test.ts | 55 -- .../cli/test/harness-named-sessions.test.ts | 105 --- packages/cli/test/harness-sessions.test.ts | 85 -- packages/cli/test/harness-skills.test.ts | 135 --- .../cli/test/kimi-reasoning-payload.test.ts | 74 -- packages/cli/test/model-picker.test.ts | 100 -- packages/cli/test/mutation-queue.test.ts | 95 -- packages/cli/test/print.test.ts | 166 ---- packages/cli/test/slash-commands.test.ts | 67 -- packages/cli/test/tool-revalidation.test.ts | 97 -- packages/cli/test/tool-selection.test.ts | 130 --- packages/cli/test/tools-picker.test.ts | 149 --- packages/cli/test/tui-keybindings.test.ts | 48 - packages/cli/test/tui.fixture.test.ts | 544 ----------- packages/cli/tsconfig.build.json | 15 - packages/cli/tsconfig.json | 3 - packages/cli/tsdown.config.ts | 18 - packages/cli/vitest.config.ts | 18 - packages/pi-extension/CHANGELOG.md | 5 + packages/ptywright/scripts/build-ghostty.mjs | 2 +- skills/cua-cli/SKILL.md | 314 ------- tsconfig.json | 1 - 90 files changed, 378 insertions(+), 10848 deletions(-) delete mode 100644 .github/workflows/release-cua-cli.yml delete mode 100644 docs/cua-cli-harness-migration.md delete mode 100644 packages/cli/CHANGELOG.md delete mode 100644 packages/cli/README.md delete mode 100644 packages/cli/package.json delete mode 100644 packages/cli/src/action/harness-runner.ts delete mode 100644 packages/cli/src/action/prompts.ts delete mode 100644 packages/cli/src/action/result.ts delete mode 100644 packages/cli/src/browser-act-input.ts delete mode 100644 packages/cli/src/cli-executor.ts delete mode 100644 packages/cli/src/cli-harness.ts delete mode 100644 packages/cli/src/cli.ts delete mode 100644 packages/cli/src/harness-browser.ts delete mode 100644 packages/cli/src/harness-models.ts delete mode 100644 packages/cli/src/harness-named-sessions.ts delete mode 100644 packages/cli/src/harness-sessions.ts delete mode 100644 packages/cli/src/harness-skills.ts delete mode 100644 packages/cli/src/harness.ts delete mode 100644 packages/cli/src/output/harness-jsonl.ts delete mode 100644 packages/cli/src/print.ts delete mode 100644 packages/cli/src/tui/debug-log.ts delete mode 100644 packages/cli/src/tui/diagnostics.ts delete mode 100644 packages/cli/src/tui/keybindings.ts delete mode 100644 packages/cli/src/tui/main.ts delete mode 100644 packages/cli/src/tui/message-list.ts delete mode 100644 packages/cli/src/tui/model-picker.ts delete mode 100644 packages/cli/src/tui/mutation-queue.ts delete mode 100644 packages/cli/src/tui/screenshot-widget.ts delete mode 100644 packages/cli/src/tui/slash-commands.ts delete mode 100644 packages/cli/src/tui/status-line.ts delete mode 100644 packages/cli/src/tui/telemetry-footer.ts delete mode 100644 packages/cli/src/tui/themes.ts delete mode 100644 packages/cli/src/tui/tool-selection.ts delete mode 100644 packages/cli/src/tui/tools-picker.ts delete mode 100644 packages/cli/src/tui/version.ts delete mode 100644 packages/cli/test/action-runner.test.ts delete mode 100644 packages/cli/test/cli-executor.test.ts delete mode 100644 packages/cli/test/fixtures/fake-kernel.ts delete mode 100644 packages/cli/test/fixtures/harness.ts delete mode 100644 packages/cli/test/fixtures/scripted-provider.ts delete mode 100644 packages/cli/test/fixtures/tui-fixture-runner.ts delete mode 100644 packages/cli/test/fixtures/tui-fixtures/abort.json delete mode 100644 packages/cli/test/fixtures/tui-fixtures/error.json delete mode 100644 packages/cli/test/fixtures/tui-fixtures/interrupt-cancel.json delete mode 100644 packages/cli/test/fixtures/tui-fixtures/model-arg.json delete mode 100644 packages/cli/test/fixtures/tui-fixtures/model-picker-cancel.json delete mode 100644 packages/cli/test/fixtures/tui-fixtures/model-picker.json delete mode 100644 packages/cli/test/fixtures/tui-fixtures/multiline.json delete mode 100644 packages/cli/test/fixtures/tui-fixtures/resources.json delete mode 100644 packages/cli/test/fixtures/tui-fixtures/steer.json delete mode 100644 packages/cli/test/fixtures/tui-fixtures/streaming.json delete mode 100644 packages/cli/test/fixtures/tui-fixtures/tools-picker.json delete mode 100644 packages/cli/test/fixtures/tui-fixtures/tools-reset-on-model.json delete mode 100644 packages/cli/test/harness-assembly.test.ts delete mode 100644 packages/cli/test/harness-browser.test.ts delete mode 100644 packages/cli/test/harness-models.test.ts delete mode 100644 packages/cli/test/harness-named-sessions.test.ts delete mode 100644 packages/cli/test/harness-sessions.test.ts delete mode 100644 packages/cli/test/harness-skills.test.ts delete mode 100644 packages/cli/test/kimi-reasoning-payload.test.ts delete mode 100644 packages/cli/test/model-picker.test.ts delete mode 100644 packages/cli/test/mutation-queue.test.ts delete mode 100644 packages/cli/test/print.test.ts delete mode 100644 packages/cli/test/slash-commands.test.ts delete mode 100644 packages/cli/test/tool-revalidation.test.ts delete mode 100644 packages/cli/test/tool-selection.test.ts delete mode 100644 packages/cli/test/tools-picker.test.ts delete mode 100644 packages/cli/test/tui-keybindings.test.ts delete mode 100644 packages/cli/test/tui.fixture.test.ts delete mode 100644 packages/cli/tsconfig.build.json delete mode 100644 packages/cli/tsconfig.json delete mode 100644 packages/cli/tsdown.config.ts delete mode 100644 packages/cli/vitest.config.ts delete mode 100644 skills/cua-cli/SKILL.md diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md index 7bd20e53..4ead80ca 100644 --- a/.agents/skills/release/SKILL.md +++ b/.agents/skills/release/SKILL.md @@ -1,12 +1,17 @@ --- name: release -description: Prepare and publish @onkernel/cua-ai, @onkernel/cua-agent, and @onkernel/cua-cli npm releases from kernel/cua. Use when checking release readiness, choosing package versions, writing package changelogs, committing release metadata to main, pushing package-prefixed tags, or monitoring release workflows. +description: Prepare and publish @onkernel/cua-ai, @onkernel/cua-agent, and @onkernel/cua-pi-extension npm releases from kernel/cua. Use when checking release readiness, choosing package versions, writing package changelogs, committing release metadata to main, pushing package-prefixed tags, or monitoring release workflows. --- # Release Use this workflow to release `@onkernel/cua-ai`, `@onkernel/cua-agent`, and -`@onkernel/cua-cli`. The packages do not need to release in lockstep. +`@onkernel/cua-pi-extension`. The packages do not need to release in lockstep. + +`@onkernel/cua-pi-extension` has no release workflow yet, and that is +deliberate: it merges into the renamed single package, and a first publish under +a new name is manual regardless, because npm binds a trusted publisher to a +(repository, workflow filename) pair and a brand-new package name has none. If a release run hits an unexpected bump, unclear decision, missing command, or avoidable manual step, update this skill as part of the release cleanup. Keep @@ -19,11 +24,9 @@ error-prone. | --- | --- | --- | --- | | `@onkernel/cua-ai` | `packages/ai` | `cua-ai/v` | `release-cua-ai.yml` | | `@onkernel/cua-agent` | `packages/agent` | `cua-agent/v` | `release-cua-agent.yml` | -| `@onkernel/cua-cli` | `packages/cli` | `cua-cli/v` | `release-cua-cli.yml` | +| `@onkernel/cua-pi-extension` | `packages/pi-extension` | — | none yet (manual) | -When all three change, release in dependency order: `cua-ai`, then `cua-agent`, -then `cua-cli`. The CLI production workflow verifies that its exact AI and agent -dependency versions already exist on npm. +When both change, release in dependency order: `cua-ai`, then `cua-agent`. ## Quick Start @@ -41,10 +44,8 @@ git fetch --tags origin git status --short npm view @onkernel/cua-ai versions --json npm view @onkernel/cua-agent versions --json -npm view @onkernel/cua-cli versions --json test -f .github/workflows/release-cua-ai.yml test -f .github/workflows/release-cua-agent.yml -test -f .github/workflows/release-cua-cli.yml ``` 3. For each package, find the previous release tag: @@ -52,7 +53,6 @@ test -f .github/workflows/release-cua-cli.yml ```bash git tag --list "cua-ai/v*" --sort=-v:refname | head -1 git tag --list "cua-agent/v*" --sort=-v:refname | head -1 -git tag --list "cua-cli/v*" --sort=-v:refname | head -1 ``` If no tag exists, treat the next release as the package's current @@ -66,9 +66,6 @@ git diff --name-status ..HEAD -- packages/ai package.json package-lock git log --oneline ..HEAD -- packages/agent packages/ai package.json package-lock.json tsconfig.base.json git diff --name-status ..HEAD -- packages/agent packages/ai package.json package-lock.json tsconfig.base.json - -git log --oneline ..HEAD -- packages/cli packages/agent packages/ai package.json package-lock.json tsconfig.base.json -git diff --name-status ..HEAD -- packages/cli packages/agent packages/ai package.json package-lock.json tsconfig.base.json ``` For a dependent package, include upstream package changes only when they affect @@ -98,7 +95,7 @@ has at most one unreleased section: - `packages/ai/CHANGELOG.md` - `packages/agent/CHANGELOG.md` -- `packages/cli/CHANGELOG.md` +- `packages/pi-extension/CHANGELOG.md` Releasing renames that heading in place — do not add a second top entry: @@ -132,12 +129,11 @@ Set versions explicitly: ```bash npm pkg set version= --workspace @onkernel/cua-ai npm pkg set version= --workspace @onkernel/cua-agent -npm pkg set version= --workspace @onkernel/cua-cli ``` Ensure exact internal dependencies point at the versions that will be published -first: agent to AI, and CLI to both AI and agent. Edit the package manifests -directly if `npm pkg set` is awkward for scoped dependency keys. +first: agent to AI. Edit the package manifests directly if `npm pkg set` is +awkward for scoped dependency keys. Refresh the lockfile: @@ -165,17 +161,6 @@ npm test --workspace @onkernel/cua-agent npm pack --workspace @onkernel/cua-agent --dry-run ``` -For `@onkernel/cua-cli`: - -```bash -npm run build --workspace @onkernel/cua-ai -npm run build --workspace @onkernel/cua-agent -npm run build --workspace @onkernel/ptywright -npm run build --workspace @onkernel/cua-cli -PTYWRIGHT_REQUIRED=1 npm test --workspace @onkernel/cua-cli -npm pack --workspace @onkernel/cua-cli --dry-run -``` - Run the full unit suites — do not pass individual test files. `cua-ai` excludes integration/live tests by default (`npm run test:integration --workspace @onkernel/cua-ai` runs them separately), and the `cua-agent` live @@ -190,7 +175,7 @@ limited to package versions, changelogs, and `package-lock.json`. ```bash git status --short -git add package-lock.json packages/ai/package.json packages/ai/CHANGELOG.md packages/agent/package.json packages/agent/CHANGELOG.md packages/cli/package.json packages/cli/CHANGELOG.md +git add package-lock.json packages/ai/package.json packages/ai/CHANGELOG.md packages/agent/package.json packages/agent/CHANGELOG.md git commit -m "Release CUA packages" git push origin main ``` @@ -213,13 +198,9 @@ For the agent package: ```bash git tag -a cua-agent/v -m "@onkernel/cua-agent v" git push origin cua-agent/v - -git tag -a cua-cli/v -m "@onkernel/cua-cli v" -git push origin cua-cli/v ``` -Push and verify each dependency tag before the next one: AI, then agent, then -CLI. +Push and verify the AI tag before the agent tag. ## Monitor @@ -231,9 +212,6 @@ gh run watch --exit-status gh run list --workflow release-cua-agent.yml --json databaseId,status,conclusion,headBranch,displayTitle,url --limit 10 gh run watch --exit-status - -gh run list --workflow release-cua-cli.yml --json databaseId,status,conclusion,headBranch,displayTitle,url --limit 10 -gh run watch --exit-status ``` After a workflow succeeds, verify npm: @@ -243,8 +221,6 @@ npm view @onkernel/cua-ai@ version npm dist-tag ls @onkernel/cua-ai npm view @onkernel/cua-agent@ version npm dist-tag ls @onkernel/cua-agent -npm view @onkernel/cua-cli@ version -npm dist-tag ls @onkernel/cua-cli ``` Then verify the published artifact actually imports — `npm view` only proves @@ -258,8 +234,7 @@ node --input-type=module -e "import('@onkernel/cua-ai').then((m) => { if (typeof ``` For `@onkernel/cua-agent`, install `@onkernel/cua-agent@` the same -way and check `typeof m.attach === "function"`. For the CLI, install it in a -fresh directory and verify `./node_modules/.bin/cua --help` prints `Usage:`. +way and check `typeof m.attach === "function"`. If a workflow fails after a tag is pushed, do not reuse the same package version unless npm did not publish it. Fix forward with a new commit and a new diff --git a/.agents/skills/update-docs/SKILL.md b/.agents/skills/update-docs/SKILL.md index 89873769..3c33f1f6 100644 --- a/.agents/skills/update-docs/SKILL.md +++ b/.agents/skills/update-docs/SKILL.md @@ -41,10 +41,10 @@ For every doc update: Start with these source-of-truth checks: - Package topology: `package.json`, `tsconfig.json`, and `packages/*/package.json`. -- Design invariants: `@onkernel/cua-ai` owns provider-specific policy (catalog, tool schemas, payload transforms); `@onkernel/cua-agent` is provider-neutral runtime glue around `pi-agent-core` (no provider names in `packages/agent/src`); provider differences reach the agent as compiled `CuaToolCatalog` data; `@onkernel/cua-cli` composes both for orchestration. +- Design invariants: `@onkernel/cua-ai` owns provider-specific policy (catalog, tool schemas, payload transforms); `@onkernel/cua-agent` is provider-neutral runtime glue around `pi-agent-core` (no provider names in `packages/agent/src`); provider differences reach the agent as compiled `CuaToolCatalog` data; `@onkernel/cua-pi-extension` composes both inside a pi session. - Model layer: `packages/ai/src/index.ts`, `cua.ts`, `tool-catalog.ts`, `getCuaModel`/`listCuaModels`/`parseCuaModelRef`, provider adapters, and `api-keys.ts`. -- Execution layer: `packages/agent/src/index.ts`, `CuaAgent` and `CuaAgentHarness` wiring, and the canonical CUA tool executors against `@onkernel/sdk`. -- CLI runtime flow: `packages/cli/src/cli.ts`, `cli-harness.ts`, `harness.ts`, `harness-browser.ts`, `harness-models.ts`, `harness-sessions.ts`, `harness-named-sessions.ts`, `harness-skills.ts`, `print.ts`, `output/harness-jsonl.ts`, `action/`, and `tui/`. +- Execution layer: `packages/agent/src/index.ts`, `attach.ts`, `tool-manager.ts`, `resources.ts`, and the canonical CUA tool executors against `@onkernel/sdk`. +- Extension runtime flow: `packages/pi-extension/src/index.ts`, `selection.ts`, `browser-runtime.ts`, `state.ts`, and `render.ts`. - TUI test infrastructure: `packages/ptywright/package.json`, `src/index.ts`, `src/session.ts`, `src/terminal.ts`, and `README.md`. - External drift: provider computer-use docs, `@earendil-works/pi-*` versions, and `@onkernel/sdk` versions in package manifests. @@ -53,7 +53,7 @@ Questions `architecture.md` should answer after each update: - What owns the canonical action vocabulary and the model catalog? - Where is the cua-ai vs cua-agent ownership boundary, and how do provider differences reach the agent without provider conditionals in `packages/agent/src`? - Where does Kernel SDK browser execution happen? -- What does the CLI compose at runtime via `buildCuaHarness`? +- What does the pi extension compose at runtime, and which selectors does it offer? - Which package is dev/test infrastructure only? ## Validation diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e686e12b..00c8aa2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,74 +62,6 @@ jobs: - name: Pi extension unit tests run: npm test --workspace @onkernel/cua-pi-extension - cli-unit: - runs-on: ubuntu-latest - timeout-minutes: 15 - env: - PTYWRIGHT_ZIG_VERSION: "0.15.2" - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 - with: - node-version: 22 - cache: npm - - run: npm ci - - name: Cache Zig toolchain - uses: actions/cache@v4 - with: - path: .dev/tools/zig-x86_64-linux-${{ env.PTYWRIGHT_ZIG_VERSION }} - key: zig-${{ runner.os }}-${{ env.PTYWRIGHT_ZIG_VERSION }} - - name: Install Zig ${{ env.PTYWRIGHT_ZIG_VERSION }} - run: | - set -euo pipefail - ZIG_DIR=".dev/tools/zig-x86_64-linux-${PTYWRIGHT_ZIG_VERSION}" - if [ ! -x "${ZIG_DIR}/zig" ]; then - mkdir -p .dev/tools - curl -fsSL "https://ziglang.org/download/${PTYWRIGHT_ZIG_VERSION}/zig-x86_64-linux-${PTYWRIGHT_ZIG_VERSION}.tar.xz" \ - | tar -xJ -C .dev/tools - fi - "${ZIG_DIR}/zig" version - echo "${GITHUB_WORKSPACE}/${ZIG_DIR}" >> "${GITHUB_PATH}" - - name: Cache ptywright native artifacts - uses: actions/cache@v4 - with: - path: | - packages/ptywright/.cache - packages/ptywright/native/build - key: ptywright-${{ runner.os }}-${{ hashFiles('packages/ptywright/GHOSTTY_UPSTREAM', 'packages/ptywright/native/**', 'packages/ptywright/scripts/**') }} - - run: npm run build --workspace @onkernel/cua-ai - - run: npm run build --workspace @onkernel/cua-agent - - name: Typecheck workspace - run: npx tsc -b - - name: Build ptywright (native binding) - run: npm run build --workspace @onkernel/ptywright - - name: Ptywright tests - run: npm test --workspace @onkernel/ptywright - - name: CLI unit tests - env: - PTYWRIGHT_REQUIRED: "1" - run: npm test --workspace @onkernel/cua-cli - - name: Build cua-cli - run: npm run build --workspace @onkernel/cua-cli - - name: Pack tarballs - # Pack the workspace dependencies too so the smoke install resolves - # them from the tarballs instead of the registry, where the versions - # under development are not published yet. - run: | - npm pack --workspace @onkernel/cua-ai --pack-destination "$RUNNER_TEMP" - npm pack --workspace @onkernel/cua-agent --pack-destination "$RUNNER_TEMP" - npm pack --workspace @onkernel/cua-cli --pack-destination "$RUNNER_TEMP" - - name: CLI bin smoke test - run: | - SMOKE_DIR=$(mktemp -d) - cd "$SMOKE_DIR" - npm init -y > /dev/null - npm install "$RUNNER_TEMP"/onkernel-cua-ai-*.tgz "$RUNNER_TEMP"/onkernel-cua-agent-*.tgz "$RUNNER_TEMP"/onkernel-cua-cli-*.tgz - OUTPUT=$(./node_modules/.bin/cua --help) - echo "$OUTPUT" - echo "$OUTPUT" | grep -q "Usage:" - echo "$OUTPUT" | grep -q "cua \[options\] \[prompt\.\.\.\]" - integration: runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/.github/workflows/release-cua-cli.yml b/.github/workflows/release-cua-cli.yml deleted file mode 100644 index e0e27aa9..00000000 --- a/.github/workflows/release-cua-cli.yml +++ /dev/null @@ -1,253 +0,0 @@ -name: Release CUA CLI - -on: - push: - tags: - - "cua-cli/v*" - workflow_dispatch: - inputs: - dist_tag: - description: "npm dist-tag to publish under (never 'latest'); also used as the prerelease id" - required: true - default: "ext" - type: string - bundle_workspace_dependencies: - description: "Bundle branch builds of cua-ai and cua-agent instead of using their published versions" - required: true - default: true - type: boolean - -permissions: - contents: read - id-token: write - -concurrency: - group: release-cua-cli-${{ github.event_name }}-${{ github.ref_name }} - cancel-in-progress: false - -jobs: - publish: - runs-on: ubuntu-latest - timeout-minutes: 30 - env: - PTYWRIGHT_ZIG_VERSION: "0.15.2" - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Verify production release - if: github.event_name == 'push' - run: | - git fetch origin main:refs/remotes/origin/main - git merge-base --is-ancestor "$GITHUB_SHA" origin/main - - node --input-type=module <<'EOF' - import { readFileSync } from "node:fs"; - - const tag = process.env.GITHUB_REF_NAME; - const prefix = "cua-cli/v"; - if (!tag?.startsWith(prefix)) { - throw new Error(`Expected tag to start with ${prefix}, got ${tag}`); - } - - const tagVersion = tag.slice(prefix.length); - const pkg = JSON.parse(readFileSync("packages/cli/package.json", "utf8")); - if (pkg.version !== tagVersion) { - throw new Error(`Tag version ${tagVersion} does not match ${pkg.name} package.json version ${pkg.version}`); - } - - console.log(`${pkg.name}@${pkg.version}`); - EOF - - - uses: actions/setup-node@v5 - with: - node-version: 24 - registry-url: https://registry.npmjs.org - - - name: Ensure npm supports trusted publishing - run: npm install -g npm@^11.5.1 - - - run: npm ci - - - name: Compute prerelease version - if: github.event_name == 'workflow_dispatch' - id: prerelease - env: - DIST_TAG: ${{ inputs.dist_tag }} - run: | - if [[ ! "$DIST_TAG" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then - echo "dist_tag must be lowercase alphanumeric/hyphen, e.g. ext, next, pr-41 (got '$DIST_TAG')" >&2 - exit 1 - fi - if [[ "$DIST_TAG" == "latest" ]]; then - echo "refusing to publish a prerelease to the 'latest' dist-tag" >&2 - exit 1 - fi - - BASE=$(node -p "require('./packages/cli/package.json').version") - VERSION="${BASE}-${DIST_TAG}.${GITHUB_RUN_NUMBER}" - npm pkg set version="$VERSION" --workspace @onkernel/cua-cli - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "cua-cli prerelease version: $VERSION" - - - name: Cache Zig toolchain - if: github.event_name == 'push' - uses: actions/cache@v4 - with: - path: .dev/tools/zig-x86_64-linux-${{ env.PTYWRIGHT_ZIG_VERSION }} - key: zig-${{ runner.os }}-${{ env.PTYWRIGHT_ZIG_VERSION }} - - - name: Install Zig ${{ env.PTYWRIGHT_ZIG_VERSION }} - if: github.event_name == 'push' - run: | - set -euo pipefail - ZIG_DIR=".dev/tools/zig-x86_64-linux-${PTYWRIGHT_ZIG_VERSION}" - if [ ! -x "${ZIG_DIR}/zig" ]; then - mkdir -p .dev/tools - curl -fsSL "https://ziglang.org/download/${PTYWRIGHT_ZIG_VERSION}/zig-x86_64-linux-${PTYWRIGHT_ZIG_VERSION}.tar.xz" \ - | tar -xJ -C .dev/tools - fi - "${ZIG_DIR}/zig" version - echo "${GITHUB_WORKSPACE}/${ZIG_DIR}" >> "${GITHUB_PATH}" - - - name: Cache ptywright native artifacts - if: github.event_name == 'push' - uses: actions/cache@v4 - with: - path: | - packages/ptywright/.cache - packages/ptywright/native/build - key: ptywright-${{ runner.os }}-${{ hashFiles('packages/ptywright/GHOSTTY_UPSTREAM', 'packages/ptywright/native/**', 'packages/ptywright/scripts/**') }} - - - run: npm run build --workspace @onkernel/cua-ai - - run: npm run build --workspace @onkernel/cua-agent - - - name: Verify published production dependencies - if: github.event_name == 'push' || !inputs.bundle_workspace_dependencies - run: | - node -p 'require("./packages/cli/package.json").dependencies["@onkernel/cua-ai"]' \ - | xargs -I{} npm view @onkernel/cua-ai@{} version - node -p 'require("./packages/cli/package.json").dependencies["@onkernel/cua-agent"]' \ - | xargs -I{} npm view @onkernel/cua-agent@{} version - - - name: Build ptywright (native binding) - if: github.event_name == 'push' - run: npm run build --workspace @onkernel/ptywright - - - name: Build cua-cli - run: npm run build --workspace @onkernel/cua-cli - - - name: CLI unit tests - if: github.event_name == 'push' - env: - PTYWRIGHT_REQUIRED: "1" - run: npm test --workspace @onkernel/cua-cli - - - name: Pack production tarball - if: github.event_name == 'push' || !inputs.bundle_workspace_dependencies - run: npm pack --workspace @onkernel/cua-cli --pack-destination "$RUNNER_TEMP" - - - name: Pack branch workspace dependencies - if: github.event_name == 'workflow_dispatch' && inputs.bundle_workspace_dependencies - run: | - mkdir -p "$RUNNER_TEMP/workspace-packages" - npm pack --workspace @onkernel/cua-ai --pack-destination "$RUNNER_TEMP/workspace-packages" - npm pack --workspace @onkernel/cua-agent --pack-destination "$RUNNER_TEMP/workspace-packages" - - - name: Pack bundled CLI prerelease - if: github.event_name == 'workflow_dispatch' && inputs.bundle_workspace_dependencies - run: | - export STAGE_DIR="$RUNNER_TEMP/cua-cli-prerelease" - mkdir -p "$STAGE_DIR/node_modules/@onkernel" - cp packages/cli/package.json packages/cli/README.md "$STAGE_DIR/" - cp -R packages/cli/dist "$STAGE_DIR/" - - for package in cua-ai cua-agent; do - tarball=$(find "$RUNNER_TEMP/workspace-packages" -name "onkernel-${package}-*.tgz" -print -quit) - mkdir -p "$STAGE_DIR/node_modules/@onkernel/$package" - tar -xzf "$tarball" --strip-components=1 -C "$STAGE_DIR/node_modules/@onkernel/$package" - done - - node --input-type=module <<'EOF' - import { readFileSync, writeFileSync } from "node:fs"; - import { join } from "node:path"; - - const packagePath = join(process.env.STAGE_DIR, "package.json"); - const pkg = JSON.parse(readFileSync(packagePath, "utf8")); - const bundled = ["@onkernel/cua-ai", "@onkernel/cua-agent"]; - - for (const name of bundled) { - const dependency = JSON.parse( - readFileSync(join(process.env.STAGE_DIR, "node_modules", name, "package.json"), "utf8"), - ); - pkg.dependencies[name] = dependency.version; - for (const [dependencyName, version] of Object.entries(dependency.dependencies ?? {})) { - if (bundled.includes(dependencyName)) continue; - const current = pkg.dependencies[dependencyName]; - if (current && current !== version) { - throw new Error(`Conflicting ${dependencyName} versions: ${current} and ${version}`); - } - pkg.dependencies[dependencyName] = version; - } - } - - pkg.bundledDependencies = bundled; - writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`); - EOF - - npm pack "$STAGE_DIR" --pack-destination "$RUNNER_TEMP" - - - name: CLI bin smoke test - env: - BUNDLE_WORKSPACE_DEPENDENCIES: ${{ inputs.bundle_workspace_dependencies }} - run: | - TARBALL=$(find "$RUNNER_TEMP" -maxdepth 1 -name "onkernel-cua-cli-*.tgz" -print -quit) - SMOKE_DIR=$(mktemp -d) - cd "$SMOKE_DIR" - npm init -y > /dev/null - npm install "$TARBALL" - if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && "$BUNDLE_WORKSPACE_DEPENDENCIES" == "true" ]]; then - test -f node_modules/@onkernel/cua-cli/node_modules/@onkernel/cua-ai/dist/index.js - test -f node_modules/@onkernel/cua-cli/node_modules/@onkernel/cua-agent/dist/index.js - fi - OUTPUT=$(./node_modules/.bin/cua --help) - echo "$OUTPUT" - echo "$OUTPUT" | grep -q "Usage:" - echo "$OUTPUT" | grep -q "cua \[options\] \[prompt\.\.\.\]" - - GLOBAL_DIR=$(mktemp -d) - npm install --global --prefix "$GLOBAL_DIR" "$TARBALL" - MODELS_OUTPUT=$("$GLOBAL_DIR/bin/cua" models -p openrouter) - echo "$MODELS_OUTPUT" - echo "$MODELS_OUTPUT" | grep -q "openrouter:moonshotai/kimi-k3" - - - name: Publish production release - if: github.event_name == 'push' - run: npm publish --workspace @onkernel/cua-cli --access public - - - name: Publish prerelease - if: github.event_name == 'workflow_dispatch' - env: - DIST_TAG: ${{ inputs.dist_tag }} - run: npm publish "$RUNNER_TEMP"/onkernel-cua-cli-*.tgz --access public --tag "$DIST_TAG" - - - name: Prerelease summary - if: github.event_name == 'workflow_dispatch' - env: - DIST_TAG: ${{ inputs.dist_tag }} - VERSION: ${{ steps.prerelease.outputs.version }} - run: | - { - echo "### Prerelease published" - echo "" - echo "- version: \`$VERSION\`" - echo "- dist-tag: \`$DIST_TAG\`" - echo "- branch: \`$GITHUB_REF_NAME\` (\`$GITHUB_SHA\`)" - echo "" - echo "Install:" - echo "" - echo '```' - echo "npm install -g @onkernel/cua-cli@$DIST_TAG" - echo '```' - } >> "$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index 75142856..91476f43 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,50 @@ # cua -A computer-use CLI for agents (and TUI for humans) built on [pi-agent](https://github.com/earendil-works/pi/tree/main/packages/agent). +Browser tools for your agent, built on [pi](https://github.com/earendil-works/pi). -```bash -cua "go to news.ycombinator.com and tell me the top 3 story titles" +Point any model at a [Kernel cloud browser](https://kernel.sh/): pick the tools, +get plain agent objects back, and run whatever loop you already have. + +```ts +import { attach } from "@onkernel/cua-agent"; +import { cua } from "@onkernel/cua-ai"; + +const kb = attach({ client, browser }); +const { model, agentTools, models } = kb.compile({ + model: "anthropic:claude-opus-5", + tools: cua.toolsets.browser(), +}); ``` -`cua` provisions a [Kernel cloud browser](https://kernel.sh/), turns the model's computer-use tool calls into real mouse/keyboard/scroll/screenshot actions, and streams the result back to your terminal. +Already in pi? Install the extension instead and keep pi's session, UI, and +model selection: + +```bash +pi install npm:@onkernel/cua-pi-extension +pi -p --cua-tools browser,browser-act "open example.com and report the heading" +``` --- ## Why this exists Frontier models expose computer use through different protocols: native -computer/browser declarations, predefined browser action sets, ordinary -function tools, different coordinate systems, and different screenshot/result -contracts. `@onkernel/cua-ai` represents those differences as an explicit, -identity-keyed tool catalog. Callers choose the exact tools; provider transforms -compose only the declarations and request fields required by those identities. +computer/browser declarations, predefined browser action sets, ordinary function +tools, different coordinate systems, and different screenshot/result contracts. All of them expect you to: 1. Run a real browser somewhere (locally is annoying, on a server is hard). 2. Translate every action into an actual SDK call against that browser. -3. Capture appropriate feedback from each action so the model can verify whether it had the intended effect. -4. Keep doing this in a loop until the task is done. +3. Capture appropriate feedback from each action so the model can verify whether + it had the intended effect. +4. Know which of those protocols the model you picked actually accepts. -`cua` does all of this for you. The repo is structured as several focused npm packages so the per-provider plumbing is also reusable outside of this binary (e.g. by agents of your own spun up via [`kernel/cli`](https://github.com/kernel/cli) templates). +This repo does all of that and stops there. `@onkernel/cua-ai` represents the +provider differences as an explicit, identity-keyed tool catalog; you choose the +exact tools, and provider transforms compose only the declarations and request +fields those identities require. It does not supply an agent class, a session +format, or a front-end — your framework already has those. --- @@ -36,326 +54,124 @@ All of them expect you to: packages/ ├── ai/ # @onkernel/cua-ai - model catalog, tool schemas, provider adapters ├── agent/ # @onkernel/cua-agent - Kernel-browser tool execution -├── pi-extension/ # @onkernel/cua-pi-extension - Kernel browser tools inside pi's own session -├── cli/ # @onkernel/cua-cli - the `cua` binary +├── pi-extension/ # @onkernel/cua-pi-extension - the same tools inside pi's own session └── ptywright/ # @onkernel/ptywright - development-only PTY/TUI test infrastructure ``` -**Using pi already?** [`packages/pi-extension`](packages/pi-extension) adds these -tools to a pi session without a second agent loop: `pi install` it, select tools -with `--cua-tools`, and pi keeps owning the session, UI, and model. - -**Building your own agent? Start here:** [`packages/agent`](packages/agent) -(`@onkernel/cua-agent`) — `attach()` binds a Kernel browser and compiles a -(model, tools) pair into plain pi objects you drive yourself. It sits on -[`packages/ai`](packages/ai) (`@onkernel/cua-ai`), the model layer with the -pi-ai model catalog, canonical tool schemas, and per-provider -adapters on top of pi-ai; reach for cua-ai directly only when you bring your -own execution. +| Package | What it ships | +| --- | --- | +| [`@onkernel/cua-ai`](packages/ai) | Model catalog, tool factories/toolsets, per-model compatibility checks, provider adapters. | +| [`@onkernel/cua-agent`](packages/agent) | `attach()`: binds a Kernel browser and compiles a (model, tools) pair into plain pi objects. | +| [`@onkernel/cua-pi-extension`](packages/pi-extension) | A pi extension contributing those tools to pi's own agent session. | +| [`@onkernel/ptywright`](packages/ptywright) | Development-only PTY/TUI test infrastructure. | ```mermaid flowchart LR ai[("@onkernel/cua-ai")] agent[("@onkernel/cua-agent")] - cli[("@onkernel/cua-cli")] ext[("@onkernel/cua-pi-extension")] - pi[("pi-agent-core / pi-ai / pi-tui / pi-coding-agent")] + pi[("pi-agent-core / pi-ai / pi-coding-agent")] sdk[("@onkernel/sdk")] ai --> agent - agent --> cli - ai --> cli agent --> ext ai --> ext pi --> agent - pi --> cli pi --> ext sdk --> agent - sdk --> cli sdk --> ext ``` -| Package | What it ships | -| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| [`@onkernel/cua-ai`](packages/ai) | Computer-use model catalog, tool factories/toolsets, compatibility checks, and provider adapters. | -| [`@onkernel/cua-agent`](packages/agent) | `attach()`: binds a Kernel browser and compiles a (model, tools) pair into plain pi objects. | -| [`@onkernel/cua-pi-extension`](packages/pi-extension) | A pi extension contributing these tools to pi's own agent session. | -| [`@onkernel/cua-cli`](packages/cli) | The `cua` binary: argv parsing, sessions, skills, JSONL output, pi-tui front-end. | -| [`@onkernel/ptywright`](packages/ptywright) | Development-only PTY/TUI test infrastructure. | - --- -## Quickstart +## Building an agent -```bash -git clone https://github.com/kernel/cua -cd cua -npm install - -# run the CLI directly from source (no global install required): -npx tsx packages/cli/src/cli.ts --help - -# if you want `cua` on $PATH from any directory, add a shell function to -# your rc that pins the repo location while preserving the caller's cwd -# (so `--out`, transcript bucketing, and `.agents/skills` discovery use -# the directory you invoked from), e.g. in ~/.bashrc: -# CUA_REPO=/absolute/path/to/cua -# cua() { "$CUA_REPO/node_modules/.bin/tsx" "$CUA_REPO/packages/cli/src/cli.ts" "$@"; } - -# set API keys via env vars -export OPENAI_API_KEY=sk-... # for gpt-5.6-sol -export ANTHROPIC_API_KEY=sk-ant-... # for claude-opus-5 -export GOOGLE_API_KEY=... # for gemini-3.6-flash -export XAI_API_KEY=xai-... # for grok-4.5 -export MOONSHOT_API_KEY=sk-... # for kimi-k3 -export KERNEL_API_KEY=sk_... # always required +`attach()` binds the browser once; `compile()` turns a (model, tools) pair into +plain pi objects. Nothing here is a CUA type you have to learn: -# single-shot -cua -p "Open https://news.ycombinator.com and tell me the top story" +```ts +import Kernel from "@onkernel/sdk"; +import { cua } from "@onkernel/cua-ai"; +import { Agent, attach } from "@onkernel/cua-agent"; -# list selectable model ids -cua models +const client = new Kernel({ apiKey: process.env.KERNEL_API_KEY! }); +const browser = await client.browsers.create({ stealth: true }); +const kb = attach({ client, browser }); -# Claude -cua -p --model claude-opus-5 "Same prompt" +const { model, agentTools, models } = kb.compile({ + model: "anthropic:claude-opus-5", + tools: [...cua.toolsets.browser(), cua.tools.browser.act()], +}); -# Gemini 3 Flash (built-in computer use) -cua -p --model gemini-3.6-flash "Same prompt" +const agent = new Agent({ + streamFn: (selected, context, options) => models.streamSimple(selected, context, options), + initialState: { model, tools: [...agentTools], systemPrompt: "Use the supplied browser tools." }, +}); -# Meta Muse Spark, through OpenRouter -cua -p --model openrouter:meta/muse-spark-1.1 "Same prompt" - -# xAI Grok 4.5 -cua -p --model xai:grok-4.5 "Same prompt" +try { + await agent.prompt("Open example.com and report the heading."); +} finally { + await kb.dispose(); + await client.browsers.deleteByID(browser.session_id); +} +``` -# Moonshot Kimi K3 -cua -p --model moonshotai:kimi-k3 "Same prompt" +The compiled `model` carries the transport its tools derive: selecting a +provider-native browser or computer surface can change `model.api`, so the pair +has to reach the agent together. See +[`packages/agent/README.md`](packages/agent/README.md) for the harness variant, +swapping tools on a running session, and tool contexts. -# interactive TUI (default mode) -cua -cua "summarize https://news.ycombinator.com" +## Choosing tools for a model -# agent-friendly subcommands (one-shot, see "Agent-friendly subcommands" below -# for the full surface and named-session workflow) -cua open https://github.com/login -cua click "Sign in" -cua url -cua screenshot --out shot.png +Not every model accepts every tool. Ask, rather than guess: -# resume the most recent session for this cwd (fresh browser, prior context) -cua -c "now click on the second result" +```ts +import { cuaToolMenu, getCuaModel } from "@onkernel/cua-ai"; -# JSONL events for scripting -cua -p -o jsonl "open example.com and tell me the heading" +for (const entry of cuaToolMenu(getCuaModel("openai:gpt-5.6-sol"))) { + console.log(entry.label, entry.available ? "ok" : `unavailable: ${entry.unavailableReason}`); +} ``` +Availability is decided by compiling the candidate catalog, so the menu cannot +drift from what the compiler accepts, and the reason shown for an unavailable +tool is the compiler's own. It is also pairwise: two providers' native surfaces +cannot coexist, so rebuild the menu after each change rather than caching a +per-tool verdict. + --- ## How it works -1. **Model layer** — `@onkernel/cua-ai` opens pi-ai's whole model catalog, - stable tool identities, explicit tool factories/toolsets, compatibility - checks, and provider declarations/headers/payload transforms. -2. **Execution layer** — `@onkernel/cua-agent` composes around - `pi-agent-core`'s `Agent`/`AgentHarness`. It materializes the caller's exact +1. **Model layer** — `@onkernel/cua-ai` opens pi-ai's whole model catalog, with + stable tool identities, explicit tool factories/toolsets, per-model + compatibility checks, and provider declarations/headers/payload transforms. + Compilation is declaration-only: it never sees an executor. +2. **Execution layer** — `@onkernel/cua-agent` materializes the caller's exact catalog over one shared resource pool and executes canonical actions through Kernel's computer API or a raw-CDP browser executor. -3. **CLI** — `@onkernel/cua-cli` assembles a pi `AgentHarness` from - command-line flags, env-var-based API keys, a `JsonlSessionRepo` for - transcripts, and pi skills; renders the result either as plain text - (`--print`), JSONL events (`-o jsonl`), or an interactive pi-tui - front-end. -4. **Browser** — a fresh Kernel cloud browser session per run (or per - resume) with optional named profile load/save. The model requests screenshots - explicitly when it needs visual feedback. - -See [`docs/architecture.md`](docs/architecture.md) for the full -end-to-end flow. - ---- - -## CLI reference - -See [`packages/cli/README.md`](packages/cli/README.md) for the -full CLI reference, env-var configuration, and model selection. - -Highlights: - -- `-p`/`--print` for single-shot mode; `-o jsonl` for structured output. -- `cua models` to list supported `-m`/`--model` values and their providers. -- `-m`/`--model ` to choose any model pi-ai carries. -- `/model` in the TUI for a searchable model picker; `/model ` still - switches directly. -- `/tools` in the TUI to enable or disable tools for the current session. -- `-s`/`--session-name ` to reuse a `cua session start`-allocated - Kernel browser across calls. -- `-c`/`--continue`, `-r`/`--resume`, `--session ` for transcript - resume. -- `--skill ` / `/skill:` for Agent Skills (defaults: - `~/.agents/skills/`, `/.agents/skills/`). -- `--image-protocol` / `CUA_IMAGE_PROTOCOL` to force inline screenshot - rendering (`kitty` / `iterm2` / `none` / `auto`; Ghostty / WezTerm - are auto-detected as `kitty`). - ---- - -## Agent-friendly subcommands - -Each subcommand below is one-shot: it provisions a Kernel browser, runs -the action, prints a compact result on stdout, and exits with a -deterministic code. Designed for shell agents to chain. - -| Subcommand | Result on stdout | Exit codes | -| ----------------------------------- | ----------------------------------------------- | --------------------------- | -| `cua open ` | `ok` | 0 ok, 2 error | -| `cua act ''` | bounded semantic plan result | 0 worked, 1 unmet, 2 error | -| `cua click ""` | `ok clicked (x, y)` or `not_found ` | 0, 1 not_found, 2 error | -| `cua type "" ""` | `ok typed` or `not_found ` | 0, 1 not_found, 2 error | -| `cua press [...]` | `ok pressed` | 0 ok, 2 error | -| `cua observe [""]` | the description / answer | 0 ok, 2 error | -| `cua url` | the current URL | 0 ok, 2 error | -| `cua screenshot --out ` | the path (or `(stdout)` when `--out -`) | 0 ok, 2 error | -| `cua do ""` | the assistant's final text | 0 ok, 2 error | - -By default each call provisions a fresh browser, so the second call -can't see anything the first call did. For multi-step workflows, use a -named session. - -### Named sessions - -```bash -cua session start login # provisions a Kernel browser, prints `name=login` -cua -s login open https://github.com/login -cua -s login type "email field" "$EMAIL" -cua -s login type "password field" "$PASSWORD" -cua -s login click "Sign in" -cua -s login url # stdout: the post-login URL -cua session stop login # tears down the Kernel browser -``` - -Inspect: - -```bash -cua session list # tab-formatted: NAME, KERNEL_ID, AGE, LIVE_URL -cua session show login # full JSON: kernel_session_id, live_url, transcript_path, ... -``` +3. **Transport** — the compiled catalog derives `model.api` from the selected + tools, so a provider-native surface reaches the wire with the transport, + headers, and payload shape it requires. +4. **Browser** — a Kernel cloud browser with optional profile and proxy. The + model requests screenshots explicitly when it needs visual feedback. -`-s ` works for all invocation styles (action subcommands, `--print`, the -interactive TUI). Liveness is checked before each attach: if the Kernel -browser timed out, the call fails with a clear "session no longer -alive" error suggesting `cua session stop && cua session start -`. - -Named-session metadata lives in `$XDG_DATA_HOME/cua/named-sessions/.json` -(default `~/.local/share/cua/named-sessions/`). - ---- - -## Session transcripts - -Every `--print`, interactive TUI, and `-s ` invocation persists a -JSONL transcript by default — useful for analyzing or self-improving -agent behavior. - -**Where**: `$XDG_DATA_HOME/cua/sessions//.jsonl` (default -`~/.local/share/cua/sessions/`). For named sessions, the exact path is -in the `transcript_path` field of `cua session show `. - -**Format**: one pi `SessionManager` record per line. Conversation records have -`type: "message"`; their role and content are nested under `.message`. A custom -record with `customType: "cua-browser"` stores `sessionId`, `liveUrl`, and -optional `profileId` under `.data`. - -**Opting out**: `--no-session` keeps the run in-memory only. One-shot -action subcommands (without `-s`) also skip the transcript, since -they're already self-contained. - -**Analyzing**: anything that reads JSONL works. A few `jq` starters: - -```bash -TRANSCRIPT=~/.local/share/cua/sessions//.jsonl - -# Every tool call the agent made, in order -jq -c 'select(.type == "message" and .message.role == "assistant") - | .message.content[]? | select(.type == "toolCall") - | {name, arguments}' "$TRANSCRIPT" - -# Largest tool-result screenshot (handy when chasing context-window blowups) -jq -c 'select(.type == "message" and .message.role == "toolResult") - | .message.content[]? | select(.type == "image") - | {len: (.data | length)}' "$TRANSCRIPT" \ - | sort -t: -k2 -n | tail -1 - -# Final assistant text (the answer) -jq -r 'select(.type == "message" and .message.role == "assistant") - | .message.content[]? | select(.type == "text") | .text' \ - "$TRANSCRIPT" | tail -1 -``` - -`--print -o jsonl` is a separate live-event stream (one event per line -on stdout, different schema). Both are useful for analysis but they're -NOT the same thing: the `-o jsonl` stream describes turns / tool calls -/ deltas as they happen; the transcript JSONL is the persisted message -history pi-coding-agent's `SessionManager` writes. +See [`docs/architecture.md`](docs/architecture.md) for the full end-to-end flow. --- -## Skills - -`cua` follows the cross-agent [`~/.agents/skills/`](https://agentskills.io) -emerging standard. Skills loaded from any of these locations are -auto-discovered (first wins on name collision): - -1. Explicit `--skill ` flags (file or directory; repeatable). -2. `~/.agents/skills/` (user-global). -3. `/.agents/skills/` (project-local). - -Each skill's `name`, `description`, and file `location` are added to -the system prompt. The model is instructed to use the `read` tool to -load a skill's full body when its description matches the task — only -descriptions and locations live in the prompt by default, so the prompt -stays small no matter how many skills you have. - -To force-load a skill body inline on a single turn, prefix the prompt -with `/skill:` (works in both `--print` and the interactive TUI): - -```bash -cua -p "/skill:my-workflow open https://..." -``` - -Disable discovery entirely with `--no-skills` / `-ns`. - -This repo ships a `skills/cua-cli/SKILL.md` aimed at OTHER agents -(Claude Code, Cursor, pi-coding-agent, etc.) that want to drive `cua` -as a CLI subcommand. To install it globally: +## Development ```bash -mkdir -p ~/.agents/skills -ln -s "$(pwd)/skills/cua-cli" ~/.agents/skills/cua-cli -``` - ---- - -## Project layout - -``` -skills/ -└── cua-cli/SKILL.md # skill aimed at OTHER agents driving cua via shell -packages/ -├── ai/ # @onkernel/cua-ai — model layer (see packages/ai/README.md) -├── agent/ # @onkernel/cua-agent — Kernel-browser execution layer (see packages/agent/README.md) -├── cli/ # @onkernel/cua-cli — the `cua` binary (see packages/cli/README.md) -└── ptywright/ # @onkernel/ptywright — development-only PTY/TUI test infrastructure +npm ci +npm run typecheck +npm test --workspace @onkernel/cua-ai +npm test --workspace @onkernel/cua-agent +npm test --workspace @onkernel/cua-pi-extension ``` ---- - -## Roadmap - -- Auto-respawn dead Kernel sessions when `-s ` is used (today we - refuse with a clear error and ask the user to re-`session start`). -- `--local` Docker-backed browser as an alternative to Kernel cloud. -- Anthropic `hold_key` / `zoom` action support. -- pi-tui SelectList-based picker for `-r` instead of plain readline. +`cua-agent`'s live end-to-end tests skip unless `CUA_E2E_LIVE=1` is set, and +`cua-ai` runs integration tests separately via `npm run test:integration`. --- diff --git a/docs/architecture.md b/docs/architecture.md index 48e0410e..0df6d2ff 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -27,10 +27,9 @@ both explicitly and may use pi's orchestration primitives directly. takes the two pieces that are not pi-shaped — the catalog compiler and `CuaExecutionResources` — and applies headers and payload transforms through pi's own `before_provider_headers` and `before_provider_request` hooks. -- `@onkernel/cua-cli` owns application policy: it chooses an explicit tool list - for each selected model, adds pi coding tools, supplies the system prompt, - resolves credentials/sessions/skills, and renders text, JSONL, or TUI output. -- `@onkernel/ptywright` is development-only PTY/TUI test infrastructure. +- `@onkernel/ptywright` is development-only PTY/TUI test infrastructure. It has + no in-repo consumer since the CLI was retired; its own tests are what exercise + it. The invariant is that `packages/agent/src` contains no provider-name branches. Adding provider behavior means adding data and transforms in `cua-ai`, not a @@ -40,16 +39,16 @@ conditional in `cua-agent`. flowchart LR ai["@onkernel/cua-ai"] agent["@onkernel/cua-agent"] - cli["@onkernel/cua-cli"] - pi["pi-agent-core / pi-ai / pi-tui / pi-coding-agent"] + ext["@onkernel/cua-pi-extension"] + pi["pi-agent-core / pi-ai / pi-coding-agent"] sdk["@onkernel/sdk"] ai --> agent - agent --> cli - ai --> cli + agent --> ext + ai --> ext pi --> agent - pi --> cli + pi --> ext sdk --> agent - sdk --> cli + sdk --> ext ``` ## Explicit tool catalog @@ -219,59 +218,34 @@ activated; `activate()` is what redirects it, and `apply()` calls it. Generated payload processing has fixed order: model preparation, tool serialization, provider fields, then the caller's `onPayload` hook. -## CLI composition - -`packages/cli/src/harness.ts` is the application composition root. It: - -1. resolves the provider-qualified model; -2. chooses `defaultInteractionTools(model)` explicitly: - - CUA browser primitives plus the explicit `browser_act` verified-plan tool - for OpenAI, Meta, xAI, and Anthropic models without native-browser support; - - CUA browser primitives alone for Moonshot, whose API rejects - `browser_act`'s larger schema; - - Anthropic's native browser tool when the model supports it; - - Google's native browser action set; -3. creates and retains its own application-level coding-tool list; -4. compiles the complete list through the handle and hands the result to a - stock pi `AgentHarness`, retaining the selection in `CuaCliCatalog` so - `/model` and `/tools` can recompile it; -5. builds a caller-owned prompt from loaded skills and context files; -6. uses one `Session` for transcript persistence and resume; -7. exposes `cua act ''` as a model-free path to the same `browser_act` - executor and bounded formatter used by agent tool calls. - -### Interactive selectors - -`packages/cli/src/tui/main.ts` mounts pickers with pi's swap-in-place pattern: -the editor lives in its own `editorContainer`, and a selector temporarily -replaces it so the status line and telemetry footer stay visible. While a -selector is mounted it owns all keyboard input; the global input listener yields -to it so `ctrl+c` cancels the selector instead of quitting. - -- `tui/model-picker.ts` — searchable `/model` picker over `listCuaModels()`, - plus the pure helpers (`modelSearchText`, `sortModelsForPicker`, - `filterModelsForPicker`, `moveSelection`, `visibleWindow`) that make its - behavior unit-testable without a terminal. -- `tui/tool-selection.ts` — pure `/tools` state machine: identity keys matching - `normalizeTool`'s scheme, group badges, toggle/bulk operations, and - `describeMenu()`, which turns `cuaToolMenu()` plus the application's own tools - into rows. -- `tui/tools-picker.ts` — the `/tools` component. Staged edits applied through - `harness.setTools()`, in menu order. A selection is no longer confined to the - application-composed baseline: the picker offers the model's whole menu, and - the baseline is what `ctrl+r` restores. -- `tui/keybindings.ts` — registers `cua.tools.*` ids on top of pi-tui's - `TUI_KEYBINDINGS` and formats their hints. -- `tui/mutation-queue.ts` — the serialization queue both catalog mutations run - through. - -Both catalog mutations a selector can trigger — a `/tools` apply and a `/model` -switch — run through that one queue, because each suspends across several -`setTools()`/`setModel()` calls. Without it an apply could land between a -switch's `setModel()` and its final `setTools()` and fail its compile against -the wrong provider. Selectors also refuse to open mid-turn: the agent's -compiled pair is immutable, so this TUI-side check is what keeps a swap from -landing mid-request. +## Extension composition + +`packages/pi-extension/src/index.ts` is the composition root for pi sessions. pi +owns the agent loop, session, UI, and model selection; the extension contributes +only what Kernel owns: + +1. registers every selectable tool as a pi tool, and keeps the model-facing + names identical to what the library produces; +2. resolves a selection from `--cua-tools` (or a persisted command selection), + and validates it by compiling for the active model, so an incompatible tool + deactivates with the compiler's own reason instead of failing at request time; +3. re-validates on `model_select` and `before_agent_start`, restoring a + previously forced-off selection when the new model can take it; +4. applies the catalog's headers and payload transforms through + `before_provider_headers` and `before_provider_request`; +5. owns the stream for the providers it registers, swapping pi's resolved model + for the compiled `catalog.model` and adding the incoming native-call plan; +6. provisions one browser lazily on first tool execution, and deletes it on + shutdown if this session created it. + +Step 5 is what makes provider-native surfaces work under a host that owns model +resolution. `catalog.model` is the resolved model with only `api` replaced, so +cost and context window are preserved, and the transport a native surface derives +is what reaches the wire. Any future framework binding needs the same seam: a +place to register a provider whose stream receives the compiled model. + +Compiling is declaration-only, so steps 2 through 5 never provision a browser. +Only step 6 does. ## Per-turn flow @@ -294,7 +268,11 @@ user prompt - `packages/ai/test/tool-catalog.test.ts`: identities, collisions, provider composition, compatibility, declarations, and coordinate contracts. - `packages/agent/test/resources.test.ts`: action feedback and batch boundaries. -- `packages/agent/test/agent.test.ts`: exact catalogs and dynamic replacement. +- `packages/agent/test/attach.test.ts` and `attach-session.test.ts`: compiled + pairs, applying one to a running harness, and the behaviors `activate()` + installs. - `packages/agent/test/translator-browser.test.ts`: browser behavior and ref lifecycle. -- `packages/cli/test/`: explicit CLI assembly, sessions, actions, and TUI flows. +- `packages/pi-extension/test/`: selection and availability, provider stream + ownership, browser lifecycle, and an end-to-end run against real `pi` in print + and RPC modes. diff --git a/docs/cua-cli-harness-migration.md b/docs/cua-cli-harness-migration.md deleted file mode 100644 index f53029ba..00000000 --- a/docs/cua-cli-harness-migration.md +++ /dev/null @@ -1,13 +0,0 @@ -# CUA CLI Harness Migration - -**Status:** Historical (2026-08-14). The CLI now composes a stock pi `AgentHarness` from -`attach()` rather than `CuaAgentHarness`, which no longer exists. Retained as the record of -the print/action/interactive consolidation this describes. - -The CLI uses one shared composition path for print, -action, and TUI flows. Its current architecture—including explicit tool-list -selection, coding-tool composition, sessions, skills, and rendering—is -documented in [`architecture.md`](architecture.md#cli-composition). - -This file intentionally contains no historical API guidance; use the package -READMEs and changelogs when migrating a consumer. diff --git a/docs/npm-releases.md b/docs/npm-releases.md index 9c3bedb1..52e0ab5b 100644 --- a/docs/npm-releases.md +++ b/docs/npm-releases.md @@ -1,11 +1,13 @@ # npm releases -`@onkernel/cua-ai`, `@onkernel/cua-agent`, and `@onkernel/cua-cli` publish from -package-specific tags: +`@onkernel/cua-ai` and `@onkernel/cua-agent` publish from package-specific tags: - `cua-ai/v0.1.0` runs `.github/workflows/release-cua-ai.yml` - `cua-agent/v0.1.0` runs `.github/workflows/release-cua-agent.yml` -- `cua-cli/v0.1.0` runs `.github/workflows/release-cua-cli.yml` + +`@onkernel/cua-pi-extension` has no workflow yet. It merges into the renamed +single package, and a first publish under a new name is manual either way — see +below. The tag version must match the target package's `package.json` version, and the tagged commit must be contained in `main`. @@ -18,7 +20,7 @@ Configure each package on npm with a GitHub Actions trusted publisher: | --- | --- | --- | --- | --- | | `@onkernel/cua-ai` | `kernel` | `cua` | `release-cua-ai.yml` | leave blank | | `@onkernel/cua-agent` | `kernel` | `cua` | `release-cua-agent.yml` | leave blank | -| `@onkernel/cua-cli` | `kernel` | `cua` | `release-cua-cli.yml` | leave blank | + The same configuration can be created from the npm CLI: @@ -26,7 +28,6 @@ The same configuration can be created from the npm CLI: npm install -g npm@^11.17.0 npm trust github @onkernel/cua-ai --repo kernel/cua --file release-cua-ai.yml --allow-publish npm trust github @onkernel/cua-agent --repo kernel/cua --file release-cua-agent.yml --allow-publish -npm trust github @onkernel/cua-cli --repo kernel/cua --file release-cua-cli.yml --allow-publish ``` npm requires packages to exist before a trusted publisher can be configured. If @@ -52,66 +53,48 @@ git tag cua-agent/v0.1.0 git push origin cua-agent/v0.1.0 ``` -## Releasing `@onkernel/cua-cli` 0.1.0 +## First publish of a new package name + +npm requires a package to exist before a trusted publisher can be configured for +it, so the first release of any new name is a manual publish from a local +checkout. This applies to `@onkernel/cua-pi-extension` today, and will apply to +the renamed single package. -`@onkernel/cua-cli` has not been published yet. npm requires a package to exist -before a trusted publisher can be configured for it, so the first release is a -manual publish from a local checkout. Subsequent releases come from -`cua-cli/v*` tags via `.github/workflows/release-cua-cli.yml`. +Two related constraints, because a trusted publisher is bound to a +*(repository, workflow filename)* pair: -The CLI's runtime dependencies, including `@onkernel/cua-ai`, -`@onkernel/cua-agent`, `@onkernel/sdk`, `@earendil-works/pi-coding-agent`, -and `@earendil-works/pi-tui`, must already be on npm at the pinned versions -before publishing; verify with `npm view @onkernel/cua-ai@` etc. if -unsure. +- Renaming the repository invalidates every existing entry. +- Renaming a release workflow file does the same. -First-publish steps (run from a maintainer machine with an npm account in the -`onkernel` org and Zig available on `PATH` for the ptywright dev build): +So a repository rename and a first publish are cheapest done together: one +reconfiguration instead of two. + +Manual first-publish steps (from a maintainer machine with an npm account in the +`onkernel` org): ```sh -# 1. Fresh checkout of main git clone https://github.com/kernel/cua.git -cd cua -git checkout main -git pull --ff-only +cd cua && git checkout main && git pull --ff-only -# 2. Install and build the workspace (Node >= 22.19) npm ci npm run build +npm test --workspace @onkernel/ -# 3. Run cua-cli unit tests with the native ptywright binding required -PTYWRIGHT_REQUIRED=1 npm test --workspace @onkernel/cua-cli - -# 4. Pre-publish smoke test: pack the tarball, install it into a fresh temp -# project, and run the installed `cua` bin. Do NOT proceed to step 5 until -# this passes — published npm versions are immutable, and the tag-driven -# workflow runs this same check on subsequent releases. +# Pack, install into a throwaway project, and import it before publishing — +# published npm versions are immutable. PACK_DIR=$(mktemp -d) -npm pack --workspace @onkernel/cua-cli --pack-destination "$PACK_DIR" +npm pack --workspace @onkernel/ --pack-destination "$PACK_DIR" SMOKE_DIR=$(mktemp -d) -(cd "$SMOKE_DIR" && npm init -y > /dev/null && \ - npm install "$PACK_DIR"/onkernel-cua-cli-*.tgz && \ - ./node_modules/.bin/cua --help) +(cd "$SMOKE_DIR" && npm init -y > /dev/null && npm install "$PACK_DIR"/*.tgz) -# 5. Log in to npm as a user in the onkernel org, then publish npm login -npm publish --workspace @onkernel/cua-cli --access public +npm publish --workspace @onkernel/ --access public ``` -After `@onkernel/cua-cli@0.1.0` is on the registry, configure the trusted -publisher — either via the package page in the npm web UI (Settings → -Publishing access → Add trusted publisher) using the row from the -[trusted publishing setup](#trusted-publishing-setup) table, or from the CLI: +Then configure the trusted publisher, either on the package page in the npm web +UI (Settings → Publishing access → Add trusted publisher) or from the CLI: ```sh npm install -g npm@^11.17.0 -npm trust github @onkernel/cua-cli --repo kernel/cua --file release-cua-cli.yml --allow-publish -``` - -From `0.1.1` onward, bump `packages/cli/package.json` on `main`, then tag and -push: - -```sh -git tag cua-cli/v0.1.1 -git push origin cua-cli/v0.1.1 +npm trust github @onkernel/ --repo kernel/ --file .yml --allow-publish ``` diff --git a/package-lock.json b/package-lock.json index 30c15781..78a99e57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "packages/ai", "packages/agent", "packages/ptywright", - "packages/cli", "packages/pi-extension" ], "devDependencies": { @@ -599,6 +598,7 @@ "integrity": "sha512-uYhF+FsZxogoSX/AxBcUdiY+ZklubwaXyAoEGA2eQwsHcyEAhUYIKh/WLXe/a8+k8eTCmxb+ZN2Zo9mzQtzbWw==", "hasShrinkwrap": true, "license": "MIT", + "peer": true, "dependencies": { "@earendil-works/pi-agent-core": "^0.83.0", "@earendil-works/pi-ai": "^0.83.0", @@ -634,6 +634,7 @@ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", "license": "MIT", + "peer": true, "dependencies": { "json-schema-to-ts": "^3.1.1" }, @@ -654,6 +655,7 @@ "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", @@ -668,6 +670,7 @@ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", @@ -683,6 +686,7 @@ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", @@ -697,6 +701,7 @@ "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" } @@ -706,6 +711,7 @@ "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", @@ -717,6 +723,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -742,6 +749,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.24", @@ -761,6 +769,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", @@ -777,6 +786,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", @@ -795,6 +805,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-env": "^3.972.37", @@ -819,6 +830,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", @@ -836,6 +848,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.37", "@aws-sdk/credential-provider-http": "^3.972.39", @@ -858,6 +871,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", @@ -874,6 +888,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", @@ -892,6 +907,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", @@ -909,6 +925,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", @@ -924,6 +941,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", @@ -939,6 +957,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", @@ -957,6 +976,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -978,6 +998,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", @@ -994,6 +1015,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", @@ -1011,6 +1033,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" @@ -1024,6 +1047,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -1036,6 +1060,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@nodable/entities": "2.1.0", "@smithy/types": "^4.14.1", @@ -1051,6 +1076,7 @@ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18.0.0" } @@ -1060,6 +1086,7 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -1068,6 +1095,7 @@ "version": "0.83.0", "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.83.0.tgz", "license": "MIT", + "peer": true, "dependencies": { "@earendil-works/pi-ai": "^0.83.0", "diff": "8.0.4", @@ -1083,6 +1111,7 @@ "version": "0.83.0", "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.83.0.tgz", "license": "MIT", + "peer": true, "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", @@ -1107,6 +1136,7 @@ "version": "0.83.0", "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.83.0.tgz", "license": "MIT", + "peer": true, "dependencies": { "get-east-asian-width": "1.6.0", "marked": "18.0.5" @@ -1121,6 +1151,7 @@ "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", @@ -1145,6 +1176,7 @@ "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 10" }, @@ -1173,6 +1205,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10" } @@ -1186,6 +1219,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10" } @@ -1202,6 +1236,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 10" } @@ -1218,6 +1253,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -1234,6 +1270,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -1250,6 +1287,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -1266,6 +1304,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -1282,6 +1321,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 10" } @@ -1298,6 +1338,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10" } @@ -1314,6 +1355,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 10" } @@ -1323,6 +1365,7 @@ "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", @@ -1348,13 +1391,15 @@ "url": "https://github.com/sponsors/nodable" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -1364,6 +1409,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -1372,31 +1418,36 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/codegen": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/fetch": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@protobufjs/aspromise": "^1.1.1" } @@ -1405,37 +1456,43 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/utf8": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@silvia-odwyer/photon-node": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/core": { "version": "3.24.3", "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", @@ -1450,6 +1507,7 @@ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", @@ -1464,6 +1522,7 @@ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", @@ -1478,6 +1537,7 @@ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -1490,6 +1550,7 @@ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", @@ -1504,6 +1565,7 @@ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", @@ -1518,6 +1580,7 @@ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -1530,6 +1593,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" @@ -1543,6 +1607,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" @@ -1556,6 +1621,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1565,6 +1631,7 @@ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 14" } @@ -1574,6 +1641,7 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", + "peer": true, "engines": { "node": "18 || 20 || >=22" } @@ -1596,13 +1664,15 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -1611,13 +1681,15 @@ "version": "2.14.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^4.0.2" }, @@ -1629,13 +1701,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", + "peer": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -1648,6 +1722,7 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", + "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1662,6 +1737,7 @@ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "license": "MIT", + "peer": true, "engines": { "node": ">= 12" } @@ -1671,6 +1747,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -1688,6 +1765,7 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.3.1" } @@ -1697,6 +1775,7 @@ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "safe-buffer": "^5.0.1" } @@ -1705,7 +1784,8 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-builder": { "version": "1.2.0", @@ -1718,6 +1798,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" @@ -1734,6 +1815,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", @@ -1759,6 +1841,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -1772,6 +1855,7 @@ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "license": "MIT", + "peer": true, "dependencies": { "fetch-blob": "^3.1.2" }, @@ -1784,6 +1868,7 @@ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -1798,6 +1883,7 @@ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", @@ -1812,6 +1898,7 @@ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1824,6 +1911,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", @@ -1841,6 +1929,7 @@ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", @@ -1858,6 +1947,7 @@ "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -1866,13 +1956,15 @@ "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==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": "*" } @@ -1882,6 +1974,7 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", "license": "ISC", + "peer": true, "dependencies": { "lru-cache": "^11.1.0" }, @@ -1894,6 +1987,7 @@ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", + "peer": true, "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -1907,6 +2001,7 @@ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", + "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -1920,6 +2015,7 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -1928,13 +2024,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -1944,6 +2042,7 @@ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "license": "MIT", + "peer": true, "dependencies": { "bignumber.js": "^9.0.0" } @@ -1953,6 +2052,7 @@ "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" @@ -1966,6 +2066,7 @@ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", + "peer": true, "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -1977,6 +2078,7 @@ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", + "peer": true, "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" @@ -1986,13 +2088,15 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/lru-cache": { "version": "11.4.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", "license": "BlueOak-1.0.0", + "peer": true, "engines": { "node": "20 || >=22" } @@ -2002,6 +2106,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -2014,6 +2119,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "brace-expansion": "^5.0.5" }, @@ -2029,6 +2135,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "license": "BlueOak-1.0.0", + "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -2037,7 +2144,8 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/node-domexception": { "version": "1.0.0", @@ -2055,6 +2163,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=10.5.0" } @@ -2064,6 +2173,7 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", + "peer": true, "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -2082,6 +2192,7 @@ "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", "license": "Apache-2.0", + "peer": true, "bin": { "openai": "bin/cli" }, @@ -2103,6 +2214,7 @@ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" @@ -2115,13 +2227,15 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/partial-json": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/path-expression-matcher": { "version": "1.5.0", @@ -2134,6 +2248,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=14.0.0" } @@ -2143,6 +2258,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -2152,6 +2268,7 @@ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", + "peer": true, "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" @@ -2168,6 +2285,7 @@ "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", @@ -2179,6 +2297,7 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -2189,6 +2308,7 @@ "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "hasInstallScript": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -2211,6 +2331,7 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 4" } @@ -2233,13 +2354,15 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/semver": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -2252,6 +2375,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "license": "MIT", + "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2264,6 +2388,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -2272,7 +2397,8 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" + "license": "ISC", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/strnum": { "version": "2.3.0", @@ -2284,31 +2410,36 @@ "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/ts-algebra": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", "license": "MIT", + "peer": true, "engines": { "node": ">=22.19.0" } @@ -2317,13 +2448,15 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@earendil-works/pi-coding-agent/node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 8" } @@ -2333,6 +2466,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", + "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -2348,6 +2482,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -2375,6 +2510,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=16.0.0" } @@ -2384,6 +2520,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -2399,6 +2536,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -2408,6 +2546,7 @@ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "license": "ISC", + "peer": true, "peerDependencies": { "zod": "^3.25.28 || ^4" } @@ -2417,6 +2556,7 @@ "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.83.0.tgz", "integrity": "sha512-IoYrb0rORjELmEpNtoCA/U8je3KopMkRAVJRdSzvXRvgb+Huo1gNh8Q5CSZvNOiYtDxJdj2tYZZHZ4B3+IN3hA==", "license": "MIT", + "peer": true, "dependencies": { "get-east-asian-width": "1.6.0", "marked": "18.0.5" @@ -3510,10 +3650,6 @@ "resolved": "packages/ai", "link": true }, - "node_modules/@onkernel/cua-cli": { - "resolved": "packages/cli", - "link": true - }, "node_modules/@onkernel/cua-pi-extension": { "resolved": "packages/pi-extension", "link": true @@ -4949,6 +5085,7 @@ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -5126,6 +5263,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -6174,6 +6312,7 @@ "packages/cli": { "name": "@onkernel/cua-cli", "version": "0.9.0", + "extraneous": true, "license": "MIT", "dependencies": { "@earendil-works/pi-agent-core": "0.83.0", diff --git a/package.json b/package.json index 6bc5ff94..a650d141 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,10 @@ "packages/ai", "packages/agent", "packages/ptywright", - "packages/cli", "packages/pi-extension" ], "scripts": { - "build": "npm run build --workspace @onkernel/cua-ai && npm run build --workspace @onkernel/cua-agent && tsc -b && npm run build --workspace @onkernel/cua-cli && npm run build:native --workspace @onkernel/ptywright --if-present", - "build:cli": "npm run build --workspace @onkernel/cua-cli", + "build": "npm run build --workspace @onkernel/cua-ai && npm run build --workspace @onkernel/cua-agent && tsc -b && npm run build:native --workspace @onkernel/ptywright --if-present", "dev": "tsc -b --watch", "typecheck": "npm run build --workspace @onkernel/cua-ai && npm run build --workspace @onkernel/cua-agent && tsc -b", "clean": "tsc -b --clean && npm run clean:native --workspace @onkernel/ptywright --if-present" diff --git a/packages/agent/examples/shared/tools.ts b/packages/agent/examples/shared/tools.ts index e436193c..33b6ae61 100644 --- a/packages/agent/examples/shared/tools.ts +++ b/packages/agent/examples/shared/tools.ts @@ -12,9 +12,8 @@ function structuredBrowserTools(): CuaAgentTool[] { } /** - * Interaction policy shared by the agent and harness provider matrices, mirroring - * the CLI defaults in `packages/cli/src/harness.ts`. Both examples read it from - * here so the two cannot drift apart. + * Interaction policy shared by the agent and harness provider matrices. Both + * examples read it from here so the two cannot drift apart. */ export function toolsForModel(model: CuaModelRef): CuaAgentTool[] { const { provider, model: modelId } = parseCuaModelRef(model); diff --git a/packages/agent/test/attach-session.test.ts b/packages/agent/test/attach-session.test.ts index 2f8da1f6..1e24f873 100644 --- a/packages/agent/test/attach-session.test.ts +++ b/packages/agent/test/attach-session.test.ts @@ -79,8 +79,7 @@ function modelsFromStream(streamFn: StreamFn, provider = "openai"): Models { /** * What a consumer does with a handle: compile a pair, hand it to a stock pi - * harness, and recompile-then-apply to change it. The CLI's `CuaCliCatalog` is - * this same shape. + * harness, and recompile-then-apply to change it. */ async function openSession(options: { model: CuaModelInput; diff --git a/packages/agent/test/example-provider-matrix.test.ts b/packages/agent/test/example-provider-matrix.test.ts index 53cdb2c6..940b9825 100644 --- a/packages/agent/test/example-provider-matrix.test.ts +++ b/packages/agent/test/example-provider-matrix.test.ts @@ -11,7 +11,7 @@ import { toolsForModel } from "../examples/shared/tools"; * invisible until someone ran the script against a live key. * * Limited to models the registry can resolve, so Anthropic's older non-native - * fallback branch is covered by the CLI's `defaultInteractionTools` test instead. + * fallback branch is covered by the tool menu's availability tests instead. */ const models: readonly CuaModelRef[] = [ "openai:gpt-5.6-sol", diff --git a/packages/ai/README.md b/packages/ai/README.md index 75c43d3b..1bb7ee45 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -203,7 +203,7 @@ against a Kernel browser and owns implementation identity. A CUA-owned identity remains stable when its name is customized. Caller tools receive `caller.` identities through the canonical `callerToolIdentity()` -helper shared with cua-agent and cua-cli. Compilation rejects: +helper shared with every consumer. Compilation rejects: - duplicate identities; - exact or provider-normalized caller-visible name collisions; diff --git a/packages/ai/src/tool-catalog.ts b/packages/ai/src/tool-catalog.ts index 9a1bd476..eea1d4e2 100644 --- a/packages/ai/src/tool-catalog.ts +++ b/packages/ai/src/tool-catalog.ts @@ -92,8 +92,8 @@ export type CuaCallerToolDeclaration = Tool; export type CuaCatalogToolInput = CuaToolSpec | CuaCallerToolDeclaration; /** - * Canonical identity scheme for caller-owned tools. Exported so cua-agent and - * cua-cli share exactly one definition and cannot drift. + * Canonical identity scheme for caller-owned tools. Exported so every consumer + * shares exactly one definition and cannot drift. */ export function callerToolIdentity(name: string): string { return `caller.${name}`; diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md deleted file mode 100644 index 15d20c71..00000000 --- a/packages/cli/CHANGELOG.md +++ /dev/null @@ -1,189 +0,0 @@ -# Changelog - -## Unreleased - -- The CLI now builds a stock pi `AgentHarness` from `attach()` instead of - `CuaAgentHarness`. `buildCuaHarness()` returns `{ harness, catalog }`: the - harness is pi's, and `CuaCliCatalog` holds the live (model, tools) selection - that `/model` and `/tools` recompile. Behavior is unchanged, including the - guarantee that a rejected selection leaves the session exactly as it was. -- `/tools` now offers the model's whole tool menu instead of filtering the list - the CLI composed. Tools the CLI did not choose — `playwright_execute`, the - computer toolset, a provider-native surface — can be enabled, and tools the - model cannot take are shown as unavailable with the reason, re-evaluated as - the selection is staged. `ctrl+r` still restores the model's defaults. -- Add `cua tools`, which prints that menu for a model without the TUI, with - `--json` for scripting. -- `cua models` lists every model pi-ai carries, not a curated subset, and `-p` - accepts any provider it carries. -- `-m` accepts any model id. A bare id that several providers carry now resolves - to the first-party provider rather than erroring, since gateways resell the - same ids; pass a qualified `provider:model` ref to reach a specific one. -- The API-key preflight now runs only for providers CUA documents variable names - for. Any other pi-ai provider is still selectable; pi resolves its credential - when it streams, and failing up front would refuse a model that works. -- The default interaction toolset is chosen from the model rather than its - provider: a model with a native browser surface gets it, and everything else - gets CUA's CDP tools, with `browser_act` included only where the model accepts - its schema. A provider can front several model families — Kimi K3 rejects - `browser_act`'s schema while Muse Spark accepts it — so one answer per - provider was never right. -- `--print -o jsonl` schema bumps to version 2: every assistant message now also - emits an `assistant_usage` event (`turn`, `model`, `api`, `input`, `output`, - `cache_read`, `cache_write`, `reasoning`, `total_tokens`, and a derived - `cache_hit_ratio`), including tool-only turns with no text. -- `defaultInteractionTools` still selects Google's native browser toolset, so - `assistant_usage.api` for a Google model is unchanged as long as that toolset - stays selected. A `/tools` selection that drops it now reports pi's builtin - `google-generative-ai` api instead of the CUA-owned one, since the selected - tools decide the transport. - -Breaking: the Tzafon, Yutori, and Meta providers are removed. - -- Refs like `-m tzafon:…` and `-m yutori:…` are no longer accepted, `cua models` - no longer lists either provider, and `TZAFON_API_KEY`/`YUTORI_API_KEY` are no - longer read. -- `-m meta:muse-spark-1.1` is removed; use `-m openrouter:meta/muse-spark-1.1`. - `META_API_KEY` is no longer read. -- The `/tools` picker no longer has atomic tool groups. They existed only for - Yutori's n1 action set, which the catalog compiler refused to accept as a - partial selection; every remaining tool toggles on its own. - -## 0.9.0 - 2026-08-04 - -Breaking: upgrade the pi stack to 0.83.0 (`pi-ai`, `pi-agent-core`, -`pi-coding-agent`, `pi-tui`). - -- Coding tools now come from `@earendil-works/pi-agent-core` - (`createReadTool`/`createBashTool`/`createEditTool`/`createWriteTool`, - preserving the read/bash/edit/write order) instead of pi-coding-agent's - `createCodingTools`. The harness supplies - `toolContext: { env: new NodeExecutionEnv({ cwd }) }`, and the removed - `CuaAgentHarness` `env` option is no longer used. -- Kimi K3 (Moonshot and OpenRouter) sends `reasoning_effort: low` at the - default `--thinking` level, following pi's catalog metadata with no CLI - override. -- Update `@onkernel/cua-ai` and `@onkernel/cua-agent` to 0.10.0. - -## 0.8.0 - 2026-08-03 - -- Queue messages submitted during an active turn for steering at the next agent - step. Pressing `esc` interrupts the active work and immediately starts a new - turn with any steering messages that were still queued. - -## 0.7.0 - 2026-08-03 - -- Add `openrouter:moonshotai/kimi-k3` model selection and - `cua models -p openrouter`, authenticated with `OPENROUTER_API_KEY`. -- Use the same browser-primitives-only interaction catalog for Kimi K3 through - Moonshot and OpenRouter; neither transport receives the unsupported - `browser_act` schema. -- Resolve model references through the harness's `Models` collection so CLI - model selection, authentication, and streaming use the same concrete pi-ai - model. -- Update `@onkernel/cua-ai` and `@onkernel/cua-agent` to 0.9.0. - -## 0.6.0 - 2026-07-31 - -Breaking: the CLI now assembles one explicit model-specific tool list. - -- Remove `--mode`, `--native-tool`, `--playwright`, and the interactive `/mode` - command. -- Select browser-oriented provider defaults explicitly in - `packages/cli/src/harness.ts`: CUA browser primitives plus the verified - `browser_act` plan tool for OpenAI, Meta, xAI, and older Anthropic models; - browser primitives alone for Moonshot, whose API rejects `browser_act`'s - schema; native browser tools for current Anthropic, Google, and Yutori - models; and Tzafon's browser-scoped native computer tool. Then append pi - coding tools into the same list. -- Change the default model to the verified `openai:gpt-5.6-sol`. -- Stop attaching screenshots automatically to first prompts. Models request an - explicit browser/computer screenshot tool when visual feedback is needed. -- Keep an explicit CLI-owned coding-tool list across `/model` changes instead - of inspecting the compiled interaction catalog. -- Make the CLI own its complete system prompt (skills and context); the agent no - longer supplies provider defaults. -- Persist only the selected model in named-session runtime metadata. Legacy mode - and native-tool fields are no longer read or written. -- Add model-free `cua act ''` for direct `browser_act` execution with the - same schema and bounded semantic feedback agents receive. It exits 0 only for - a causally `worked` plan, 1 for `didnt`/`unknown`, and 2 for invalid input or - execution errors. -- Keep action subcommands, print mode, JSONL output, named sessions, transcript - resume, skills, and TUI model switching on the same explicit harness assembly - path. -- Add an interactive, searchable `/model` picker, modelled on pi's own model - selector: same frame, fuzzy search, centred 10-row scroll window, - `[provider]` badges, `✓` on the active model, wrapping navigation, and - single-press `esc`/`ctrl+c` cancel. `/model ` still switches - directly without opening any UI; an unresolvable ref now reports the error and - then opens the picker prefilled with what was typed. Unlike pi's selector, the - picker never writes global settings and does no background catalog refresh - (cua's catalog is static). -- Add `/tools`, an interactive session-local menu for enabling/disabling the - model-callable tools the CLI composed for the active model. It can only - restrict that caller-owned list, never add tools the model does not support. - Edits are staged and applied with `ctrl+s` (`enter` toggles, `space` toggles - while the search box is empty, `ctrl+a` all, `ctrl+x` none, `ctrl+r` model - defaults, `esc` cancel); cancelling leaves - live state untouched, and a selection rejected by catalog validation reports - the error without mutating the session. Provider-native sets that cannot be - partially suppressed (Yutori n1) toggle as one group, and disabling everything - is allowed. -- Reset the tool selection to the new model's defaults on `/model`, with a - notice. Tool identities are provider-specific, so carrying a selection across - a model change would silently substitute tools. -- Refuse to open either picker while a turn is running, because recompiling the - tool catalog while a request is streaming is unsafe. (The agent's - execution-scope guard only rejects mutation attempted from inside a tool's - `execute`, so this TUI-side refusal is the protection for this case.) -- Serialize `/tools` applies and `/model` switches through one queue, so a - queued `setTools()` can never land between a switch's `setModel()` and its - final `setTools()` and fail the compile against the wrong provider. -- Fix `ctrl+c` and `ctrl+d` quitting the TUI while a selector is open: the - global input listener now yields all input to an open picker. -- Register cua's `cua.tools.*` keybindings instead of constructing an unused - `KeybindingsManager`, so bulk-action keys resolve and their hints render. -- Security: inherits the `sharp` `^0.35.3` upgrade through - `@onkernel/cua-agent` 0.8.0 (GHSA-f88m-g3jw-g9cj). See that package's - changelog for the installer-visible packaging notes. - -### Known unfixed advisories - -Installing this release still reports two vulnerable packages (three advisory -IDs). `npm audit` counts them as 1 high + 1 moderate, and both are reachable -only through `@earendil-works/pi-coding-agent` 0.80.10, which publishes its own -`npm-shrinkwrap.json` and therefore pins its subtree verbatim: - -- **`brace-expansion` 5.0.6** (high) via that subtree's `minimatch` 10.2.5. - This single package is flagged by two separate advisories: - GHSA-mh99-v99m-4gvg (`<=5.0.7`, CVSS 7.5, unbounded expansion length causing - an out-of-memory process crash) and GHSA-3jxr-9vmj-r5cp (`>=3.0.0 <5.0.7`, - CVSS 5.3, exponential-time expansion of consecutive non-expanding `{}` - groups). Both are denial of service on pathological glob patterns; the impact - is a hung or OOM-killed local `cua` process, with no effect on the cloud - browser or on other users. -- **`protobufjs` 7.6.4** (moderate, GHSA-j3f2-48v5-ccww) via `@google/genai`. - Unreachable here: it is loaded only by `@google/genai`'s opt-in local - tokenizer entry point, which this CLI never imports, and the advisory needs - untrusted `.proto` source text, which the CLI never parses. - -Neither is suppressed or filtered out of `npm audit`. npm `overrides` were -tried and verified to be silently ignored across a dependency's published -shrinkwrap (the pins stay at 5.0.6/7.6.4 and the audit count does not move), so -adding them would be dead configuration and false assurance. - -`protobufjs` is fixed in `pi-coding-agent` 0.82.0+ (which pins 7.6.5); that -upgrade is deferred to a follow-up release because it is a two-minor bump of -the agent framework carrying a customer-visible provider behavior change -(pi-ai 0.82 revises Kimi K3's `compat` to `supportsReasoningEffort: true` and -`thinkingFormat` `"deepseek"` -> `"openai"`, altering the request payload sent -to Moonshot), which cannot be validated offline and is not worth bundling into -a security patch for an advisory that is unreachable from this CLI. - -`brace-expansion` cannot be fully resolved from this repository at all. The -newest published `pi-coding-agent`, 0.83.0, pins 5.0.7, which clears -GHSA-3jxr-9vmj-r5cp but *not* GHSA-mh99-v99m-4gvg, whose range is `<=5.0.7`. -Clearing the remaining high requires an upstream shrinkwrap refresh to -`minimatch` 10.2.6, which is the first release to depend on -`brace-expansion` `^5.0.8`. diff --git a/packages/cli/README.md b/packages/cli/README.md deleted file mode 100644 index 2e3c9d2b..00000000 --- a/packages/cli/README.md +++ /dev/null @@ -1,274 +0,0 @@ -# `@onkernel/cua-cli` - -The CLI / TUI binary for the [`cua`](../../README.md) monorepo. Wires -[`@onkernel/cua-agent`](../agent)'s `attach()` handle and a pi `AgentHarness` to -[`pi-tui`](https://www.npmjs.com/package/@earendil-works/pi-tui) for an -interactive front-end and to -[`pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)'s -coding tools for workspace access. - -## Install - -```bash -# global install (puts `cua` on your PATH): -npm install -g @onkernel/cua-cli -cua --help - -# or run a one-off without installing: -npx @onkernel/cua-cli --help -``` - -Requires Node >= 22.19.0. - -## Usage - -```bash -# Interactive TUI: -cua - -# Single-shot prompt: -cua --print "open https://example.com and tell me the heading" - -# Constrained one-shot subcommands (deterministic exit codes): -cua open https://example.com -cua snapshot --filter interactive -cua act '{"steps":[{"type":"click","ref":"e12","expect":{"type":"text","text":"Done"}}]}' -cua click "Sign in button" -cua type "email field" "alice@example.com" -cua press ctrl l # Ctrl+L (focus address bar) -cua url -cua observe "what page is loaded?" -cua screenshot --out shot.png -cua do "buy a pair of socks on amazon" --max-steps 20 - -# List and pick supported models: -cua models -cua models -p openai -cua --print --model openai:gpt-5.6-sol "..." -cua --print --model anthropic:claude-opus-5 "..." -cua --print --model google:gemini-3.6-flash "..." -cua --print --model openrouter:meta/muse-spark-1.1 "..." -cua --print --model xai:grok-4.5 "..." -cua --print --model moonshotai:kimi-k3 "..." - -# Named sessions (browser stays alive across calls): -cua session start login # provisions Kernel browser -cua -s login open https://github.com/login -cua -s login type "email field" "$EMAIL" -cua -s login click "Sign in" -cua session stop login - -cua session list # NAME / KERNEL_ID / AGE / LIVE_URL -cua session show login # full JSON metadata - -# Resume a prior session transcript into a fresh browser: -cua --continue -cua --resume # picker -cua --session abc12345 # by id prefix -``` - -## Interactive commands - -Inside the TUI, `/` opens the command autocomplete. The supported commands are: - -| Command | Behavior | -| --- | --- | -| `/model` | Open an interactive, searchable model picker. | -| `/model ` | Switch directly, without opening the picker. An unresolvable ref reports the error and then opens the picker prefilled with what you typed. | -| `/tools` | Open the model's tool menu and change this session's selection. | -| `/thinking ` | Set the reasoning level for future turns. | -| `/compact` | Summarize older turns to free context budget. | -| `/skill: [args]` | Invoke a loaded skill. | - -### `/model` picker - -Type to fuzzy-search across the provider, ref, model id, and display name. -`↑`/`↓` move (wrapping at both ends), `enter` selects, `esc` or `ctrl+c` -cancels. The active model is listed first and marked with `✓`. Selecting a model -runs the same switch as `/model `, including the tool revalidation -described below. Nothing is written to disk except a named session's recorded -model (`-s`). - -The picker lists every CUA-capable model; it does not check whether the -provider's API key is set. Run `cua models` for the same catalog on stdout. - -### `/tools` picker - -`/tools` lists everything CUA can offer the active model — every browser and -computer tool, `playwright_execute`, the provider-native surfaces the model has, -and the CLI's own coding tools — with the current session's selection marked. -You can add tools the CLI did not compose, not just remove ones it did. - -A tool the model cannot take is shown as unavailable and cannot be selected, -with the reason on the detail line. Availability is decided by compiling the -resulting catalog, so it matches exactly what the session will accept, and it is -re-evaluated as you stage: selecting a provider-native surface pins the -transport, which can make other rows unavailable. - -`cua tools` prints the same menu to stdout for a model, without the TUI. - -| Key | Action | -| --- | --- | -| `↑` / `↓` | Move the cursor | -| `enter` | Toggle the highlighted tool | -| `space` | Toggle the highlighted tool (only while the search box is empty, so queries stay typeable) | -| `ctrl+a` / `ctrl+x` | Enable / disable everything listed (respects an active search) | -| `ctrl+r` | Reset to the model's defaults | -| `ctrl+s` | Apply the selection | -| `esc` | Cancel | -| `ctrl+c` | Clear an active search, or cancel when the search box is empty | - -Edits are staged: nothing is applied until `ctrl+s`, and cancelling leaves the -live tool list untouched. Applying calls the harness's `setTools()`, which -compiles and validates the whole catalog before mutating anything — so a -rejected selection reports the error and leaves the session unchanged. - -Disabling every tool is allowed and yields a text-only agent. - -Selections are session-only and never persisted. `/model` rebuilds the tool list -from the new model's defaults and reports `tool selection reset to the new -model's defaults`; tool identities are provider-specific, so a previous -selection is not carried across a model change. - -Both pickers are unavailable while a turn is running: recompiling the tool -catalog while a request is streaming is unsafe, so the TUI refuses to open them. -(The agent's own execution-scope guard only covers mutation attempted from -*inside* a tool's `execute`, so this TUI-side check is the protection here.) - -## Models - -Run `cua models` to list every supported `-m` / `--model` value and the -provider it routes to. Filter by provider with `cua models -p openai`, -`cua models -p anthropic`, `cua models -p google` (alias: `gemini`), -`cua models -p meta`, `cua models -p xai`, `cua models -p moonshotai` -(alias: `moonshot`), or `cua models -p openrouter`. - -`-m` / `--model` accepts a provider-qualified `provider:model` ref (e.g. -`openai:gpt-5.6-sol`) or a bare model id when it matches exactly one catalog -entry. The default is `openai:gpt-5.6-sol`. - -## Configuration - -Configuration is by environment variable. There is no config file. - -| Env | Used for | -| -------------------- | ---------------------------------------------- | -| `KERNEL_API_KEY` | Kernel API key (required) | -| `OPENAI_API_KEY` | OpenAI API key (required when `-m openai:…`) | -| `ANTHROPIC_API_KEY` | Anthropic API key (required when `-m anthropic:…`) | -| `GOOGLE_API_KEY` | Google API key (required when `-m google:…`) | -| `GEMINI_API_KEY` | alias of `GOOGLE_API_KEY` | -| `XAI_API_KEY` | xAI API key (required when `-m xai:…`) | -| `MOONSHOT_API_KEY` | Moonshot AI API key (required when `-m moonshotai:…`) | -| `OPENROUTER_API_KEY` | OpenRouter API key (required when `-m openrouter:…`) | -| `KERNEL_BASE_URL` | override Kernel base URL | -| `OPENAI_BASE_URL` | override OpenAI base URL | -| `ANTHROPIC_BASE_URL` | override Anthropic base URL | -| `GOOGLE_BASE_URL` | override Google base URL | -| `META_BASE_URL` | override Meta Model API base URL | -| `XAI_BASE_URL` | override xAI API base URL | -| `MOONSHOTAI_BASE_URL` | override Moonshot AI base URL | -| `XDG_DATA_HOME` | sessions dir base (defaults to `~/.local/share`) | -| `CUA_IMAGE_PROTOCOL` | force inline image protocol (`kitty`/`iterm2`/`none`/`auto`) | - -Use `--thinking ` (`off | minimal | low | medium | high | xhigh | max`, -default `low`) for providers that support reasoning effort. - -The CLI chooses one explicit interaction catalog and appends pi's coding tools: -CUA browser primitives plus the verified `browser_act` plan tool for OpenAI, -Meta, xAI, and older Anthropic models; browser primitives alone for Moonshot, -whose API rejects `browser_act`'s larger schema; Anthropic's native browser tool -when supported; and Google's native browser action set. If the active Anthropic -credential cannot access `browser_20260701`, the same selected browser tool uses -its equivalent function transport. Library callers can select any catalog -directly; see [`@onkernel/cua-agent`](../agent). - -## Output formats - -`--print` defaults to streaming text. Pass `-o jsonl` for one -structured event per line (good for scripting): - -```bash -cua --print -o jsonl "open https://example.com" \ - | jq -c 'select(.type=="tool_call" or .type=="assistant_text_done")' -``` - -Add `--jsonl-include-deltas` for assistant-token deltas and -`--jsonl-include-images` for base64 screenshots in `tool_result` events. - -The first event of every `--print -o jsonl` run is -`session_created` with a `schema_version` field. The current schema -version is `2`. The `model` field carries a provider-qualified ref -(e.g. `openai:gpt-5.6-sol`); use `parseCuaModelRef` from `@onkernel/cua-ai` -if you only need the bare model id. - -Every assistant message also emits an `assistant_usage` event (including -tool-only turns with no text): `turn`, `model`, `api`, `input`, `output`, -`cache_read`, `cache_write`, `reasoning`, `total_tokens`, and -`cache_hit_ratio`. OpenAI's billed prompt tokens are `input + cache_read + -cache_write` (the provider already subtracts cached and cache-write tokens -out of `input`), so `cache_hit_ratio` is `cache_read` over that total, -reported as `0` when the total is `0`. - -## Sessions and transcripts - -`--print`, the interactive TUI, and any `-s ` invocation persist -a JSONL transcript to -`$XDG_DATA_HOME/cua/sessions//.jsonl` by default -(typically `~/.local/share/cua/sessions/...`). Pass `--no-session` to -keep a run in-memory only, or `--session-dir ` to override the -location. - -For named sessions, the exact transcript path is in -`cua session show ` under `transcript_path`. See the -[Session transcripts section in the top-level README](../../README.md#session-transcripts) -for the JSONL schema and `jq` analysis examples. - -## Skills and context - -`cua` resolves skills and context files through pi's resource loader -(the same loader pi's own TUI uses), so the discovery set matches pi. -Skills load from: - -- `~/.agents/skills/` (user-global, the cross-agent - [`~/.agents/skills/`](https://agentskills.io) standard) -- `/.agents/skills/` (project-local) -- the pi agent dir (`~/.pi/agent/`) -- pi-installed packages (`pi install …` records the package in pi's - settings and clones it under the agent dir; its bundled skills load - here too) - -Plus any explicit `--skill ` flags. Disable with `--no-skills` -(`-ns`). - -Each skill's `name`, `description`, and file `location` are added to -the system prompt; the model uses the `read` tool to load a skill's -full body when its description matches the task. Use `/skill:` -in a prompt to force-load a skill body inline. - -Context files (`AGENTS.md` / `CLAUDE.md`) discovered by the resource -loader are appended to the system prompt and listed in the TUI's -`[Context]` section. `--no-skills` disables skill discovery only; -context files still load, since they describe the project rather than -add agent capabilities. - -pi *extensions* are not executed by `cua`: extensions bind into pi's -`AgentSession`, and `cua` drives the lower-level `AgentHarness` -directly. Installed-package skills and context still load. - -## Image protocol - -Force the inline-screenshot protocol with `--image-protocol` or -`CUA_IMAGE_PROTOCOL`: - -- `kitty` — Kitty graphics protocol (also covers Ghostty / WezTerm). -- `iterm2` — iTerm2 inline images. -- `none` — disable inline images; show a compact text card instead. -- `auto` — auto-detect based on `TERM_PROGRAM` / `TMUX` / etc. (default). - -The TUI prints the resolved capability as the second header line so -you can see at a glance whether inline images will render. - -## License - -MIT. diff --git a/packages/cli/package.json b/packages/cli/package.json deleted file mode 100644 index f97c5c3c..00000000 --- a/packages/cli/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "@onkernel/cua-cli", - "version": "0.9.0", - "description": "Kernel-cloud-browser computer-use TUI built on @onkernel/cua-agent and pi-tui", - "license": "MIT", - "type": "module", - "main": "./dist/cli.js", - "repository": { - "type": "git", - "url": "git+https://github.com/kernel/cua.git", - "directory": "packages/cli" - }, - "bugs": { - "url": "https://github.com/kernel/cua/issues" - }, - "homepage": "https://github.com/kernel/cua/tree/main/packages/cli#readme", - "bin": { - "cua": "./dist/cli.js" - }, - "files": [ - "dist", - "README.md", - "CHANGELOG.md" - ], - "publishConfig": { - "access": "public" - }, - "engines": { - "node": ">=22.19.0" - }, - "scripts": { - "build": "tsdown && chmod +x dist/cli.js", - "clean": "tsc -b --clean && rm -rf dist dist-tsc", - "test": "vitest --run", - "typecheck": "tsc -b" - }, - "dependencies": { - "@earendil-works/pi-agent-core": "0.83.0", - "@earendil-works/pi-coding-agent": "0.83.0", - "@earendil-works/pi-tui": "0.83.0", - "@onkernel/cua-agent": "0.10.0", - "@onkernel/cua-ai": "0.10.0", - "@onkernel/sdk": "0.49.0" - }, - "devDependencies": { - "@onkernel/ptywright": "0.1.0", - "tsdown": "^0.22.2", - "vitest": "^3.2.4" - } -} diff --git a/packages/cli/src/action/harness-runner.ts b/packages/cli/src/action/harness-runner.ts deleted file mode 100644 index c4f5f001..00000000 --- a/packages/cli/src/action/harness-runner.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type { AgentHarnessEvent } from "@onkernel/cua-agent"; -import type { CuaCliHarness } from "../harness"; -import type { AssistantMessage } from "@onkernel/cua-ai"; -import { stderr, stdout } from "node:process"; -import { type ActionRequest, buildPrompt, DEFAULT_MAX_TURNS } from "./prompts"; -import { type ActionEventInfo, type ActionResult, exitCodeFor, formatCompact, parseResult } from "./result"; - -export interface HarnessRunOptions { - harness: CuaCliHarness; - maxTurns?: number; -} - -export interface RunActionResult { - result: ActionResult; - exitCode: number; -} - -/** - * Run a single model-mediated action subcommand against an existing - * harness + browser and return the parsed result plus exit code. Drives - * the harness for at most `maxTurns` turns. - */ -export async function runAction( - req: ActionRequest, - opts: HarnessRunOptions, -): Promise { - const startedAt = Date.now(); - - const prompt = buildPrompt(req); - const maxTurns = req.maxTurns ?? opts.maxTurns ?? DEFAULT_MAX_TURNS; - - const events: ActionEventInfo[] = []; - let assistantText = ""; - let turns = 0; - let aborted = false; - let lastToolError: string | undefined; - let lastToolErrorDetail: string | undefined; - - const unsubscribe = opts.harness.subscribe((event: AgentHarnessEvent) => { - switch (event.type) { - case "tool_execution_start": - collectActionEvent(event.toolName, event.args, events); - return; - case "tool_execution_end": { - if (event.isError) { - const { text, detail } = inspectToolError(event.result); - lastToolError = text ?? "tool execution failed"; - lastToolErrorDetail = detail; - } - return; - } - case "message_update": - if (event.assistantMessageEvent.type === "text_delta") { - assistantText += event.assistantMessageEvent.delta; - } - return; - case "turn_end": - turns += 1; - if (turns >= maxTurns && !aborted) { - aborted = true; - void opts.harness.abort(); - } - return; - default: - return; - } - }); - - let runError: Error | undefined; - let assistant: AssistantMessage | undefined; - try { - assistant = await opts.harness.prompt(prompt); - if (assistant.stopReason === "error") { - runError = new Error(assistant.errorMessage ?? "agent stopped with error"); - } - } catch (err) { - runError = err instanceof Error ? err : new Error(String(err)); - } finally { - unsubscribe(); - } - - const elapsed = Date.now() - startedAt; - - if (runError) { - const result: ActionResult = { - action: req.action, - status: "error", - text: runError.message, - elapsedMs: elapsed, - timestamp: Date.now(), - }; - return { result, exitCode: exitCodeFor(result) }; - } - - if (!assistantText.trim() && assistant) { - assistantText = textFromAssistant(assistant); - } - - const toolError = lastToolErrorDetail ?? lastToolError; - const result = parseResult(req.action, assistantText, events, elapsed, toolError); - return { result, exitCode: exitCodeFor(result) }; -} - -function textFromAssistant(message: AssistantMessage): string { - const parts: string[] = []; - for (const block of message.content) { - if (block && block.type === "text" && typeof block.text === "string") { - parts.push(block.text); - } - } - return parts.join(""); -} - -/** - * Collect click coordinates from canonical CUA computer or browser tool calls. - * Batch tools use `{ actions: [...] }`; single-action tools omit the canonical - * `type`, which is recovered from the tool name. - */ -function collectActionEvent(toolName: string, args: unknown, events: ActionEventInfo[]): void { - if (toolName === "computer_batch" || toolName === "browser_batch") { - const actions = (args as { actions?: unknown }).actions; - if (Array.isArray(actions)) { - for (const action of actions) { - if (action && typeof action === "object") { - addClickEvent( - (action as { action?: unknown }).action ?? (action as { type?: unknown }).type, - (action as { x?: unknown }).x, - (action as { y?: unknown }).y, - events, - ); - } - } - } - return; - } - if (args && typeof args === "object") { - const x = (args as { x?: unknown }).x; - const y = (args as { y?: unknown }).y; - const type = toolName.startsWith("computer_") - ? toolName.slice("computer_".length) - : toolName.startsWith("browser_") ? toolName.slice("browser_".length) : toolName; - addClickEvent(type, x, y, events); - } -} - -function addClickEvent(type: unknown, x: unknown, y: unknown, events: ActionEventInfo[]): void { - if (typeof type !== "string") return; - if (type !== "click" && type !== "double_click") return; - if (typeof x !== "number" || typeof y !== "number") return; - events.push({ actionType: type, x, y }); -} - -function inspectToolError(result: unknown): { text?: string; detail?: string } { - if (!result || typeof result !== "object") return {}; - const detailsError = (result as { details?: { error?: unknown } }).details?.error; - const detail = typeof detailsError === "string" ? detailsError.trim() : undefined; - const content = (result as { content?: unknown }).content; - if (!Array.isArray(content)) return { detail }; - const parts: string[] = []; - for (const block of content) { - if (block && typeof block === "object" && (block as { type?: unknown }).type === "text") { - const text = (block as { text?: unknown }).text; - if (typeof text === "string" && text.trim().length > 0) parts.push(text.trim()); - } - } - const text = parts.length > 0 ? parts.join("\n") : undefined; - return { text, detail }; -} - -/** Print a compact result line and return its exit code. */ -export function emitCompact(res: RunActionResult): number { - const text = formatCompact(res.result); - if (text) stdout.write(`${text}\n`); - if (res.exitCode !== 0 && !text.startsWith("error") && res.result.status === "error") { - stderr.write(`error ${res.result.text ?? ""}\n`); - } - return res.exitCode; -} diff --git a/packages/cli/src/action/prompts.ts b/packages/cli/src/action/prompts.ts deleted file mode 100644 index 4776f306..00000000 --- a/packages/cli/src/action/prompts.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Constrained one-shot prompts for the agent-friendly CLI subcommands. - */ - -export type ModelActionType = "click" | "type" | "observe" | "do"; - -export interface ActionRequest { - action: ModelActionType; - target?: string; - text?: string; - maxTurns?: number; -} - -export const DEFAULT_MAX_TURNS = 3; - -export function buildPrompt(req: ActionRequest): string { - switch (req.action) { - case "click": - if (!req.target) throw new Error("click action requires a target description"); - return clickPrompt(req.target); - case "type": - if (!req.target) throw new Error("type action requires a target description"); - if (!req.text) throw new Error("type action requires text to type"); - return typePrompt(req.target, req.text); - case "observe": - if (req.text) return observeWithQuestionPrompt(req.text); - return observePrompt(); - case "do": { - const instruction = req.text || req.target; - if (!instruction) throw new Error("do action requires an instruction"); - return instruction; - } - } -} - -function clickPrompt(target: string): string { - return `Look at the current screen. Locate and click the element that best matches this description: ${JSON.stringify(target)}. -Perform exactly ONE click on the best matching element, then stop. -If no matching element is visible on screen, respond with the text: NOT_FOUND: followed by a brief explanation. -Do not perform any other actions.`; -} - -function typePrompt(target: string, text: string): string { - return `Look at the current screen. Locate the input/text field that best matches this description: ${JSON.stringify(target)}. -Click on it to focus it, then type exactly this text: ${JSON.stringify(text)} -Perform only the click and type actions, then stop. -If no matching element is visible on screen, respond with the text: NOT_FOUND: followed by a brief explanation. -Do not perform any other actions.`; -} - -function observePrompt(): string { - return `Look at the current screen and describe what you see. Be concise and factual. -Do NOT perform any actions. Only observe and describe.`; -} - -function observeWithQuestionPrompt(question: string): string { - return `Look at the current screen and answer this question: ${JSON.stringify(question)} -Be concise and factual. Do NOT perform any actions. Only observe and respond.`; -} diff --git a/packages/cli/src/action/result.ts b/packages/cli/src/action/result.ts deleted file mode 100644 index 2eca9d27..00000000 --- a/packages/cli/src/action/result.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { ModelActionType } from "./prompts"; - -/** Subcommands that execute directly against the browser/OS planes, no model involved. */ -export type DeterministicActionType = - | "open" - | "url" - | "snapshot" - | "act" - | "text" - | "find" - | "fill" - | "press" - | "click" - | "tabs" - | "screenshot"; - -export type ActionType = ModelActionType | DeterministicActionType; - -export type Status = "ok" | "not_found" | "error" | "timeout"; - -export interface ActionEventInfo { - actionType: string; - x?: number; - y?: number; -} - -export interface ActionResult { - status: Status; - action: string; - coordinates?: [number, number]; - text?: string; - url?: string; - elapsedMs: number; - timestamp: number; -} - -/** - * Build a structured ActionResult from the agent's final assistant text - * and any action events captured during the run. - */ -export function parseResult( - action: ModelActionType, - textOutput: string, - actionEvents: ActionEventInfo[], - elapsedMs: number, - toolError?: string, -): ActionResult { - const trimmed = textOutput.trim(); - const result: ActionResult = { - action, - status: "ok", - elapsedMs, - timestamp: Date.now(), - }; - - if (toolError && toolError.trim().length > 0) { - result.status = "error"; - result.text = toolError.trim(); - return result; - } - - if (trimmed.startsWith("NOT_FOUND:")) { - result.status = "not_found"; - result.text = trimmed.slice("NOT_FOUND:".length).trim(); - return result; - } - - for (let i = actionEvents.length - 1; i >= 0; i--) { - const ev = actionEvents[i]!; - if ( - ev.x !== undefined && - ev.y !== undefined && - (ev.actionType === "click" || - ev.actionType === "double_click" || - ev.actionType === "click_mouse" || - ev.actionType === "left_click" || - ev.actionType === "right_click" || - ev.actionType === "middle_click" || - ev.actionType === "triple_click" || - ev.actionType === "click_at") - ) { - result.coordinates = [ev.x, ev.y]; - break; - } - } - - if (action === "observe") result.text = trimmed; - - return result; -} - -export function formatCompact(r: ActionResult): string { - switch (r.status) { - case "not_found": - return r.text ? `not_found ${r.text}` : "not_found"; - case "error": - return r.text ? `error ${r.text}` : "error"; - case "timeout": - return "timeout"; - } - - switch (r.action as ActionType) { - case "click": - if (r.coordinates) return `ok clicked (${r.coordinates[0]}, ${r.coordinates[1]})`; - if (r.text) return `ok clicked ${r.text}`; - return "ok clicked"; - case "type": - return "ok typed"; - case "open": - return "ok"; - case "press": - return "ok pressed"; - case "fill": - return r.text ? `ok filled ${r.text}` : "ok filled"; - case "observe": - case "act": - case "snapshot": - case "text": - case "find": - case "tabs": - return r.text ?? ""; - case "url": - return r.url ?? r.text ?? ""; - case "screenshot": - return r.text ?? "ok"; - case "do": - return r.text ?? "ok"; - default: - return "ok"; - } -} - -export function exitCodeFor(r: ActionResult): number { - switch (r.status) { - case "ok": - return 0; - case "not_found": - return 1; - case "error": - case "timeout": - default: - return 2; - } -} diff --git a/packages/cli/src/browser-act-input.ts b/packages/cli/src/browser-act-input.ts deleted file mode 100644 index b853cdc9..00000000 --- a/packages/cli/src/browser-act-input.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { CuaActionBrowserAct, CuaBrowserExpectation } from "@onkernel/cua-ai"; - -type BrowserActInput = Omit; -type JsonObject = Record; - -const STEP_TYPES = new Set(["click", "hover", "fill", "type", "key", "scroll_to", "wait"]); - -/** Parse one JSON argument and validate it before any browser side effect occurs. */ -export function parseBrowserActInput(raw: string): BrowserActInput { - let parsed: unknown; - try { - parsed = JSON.parse(raw); - } catch (error) { - throw new Error(`invalid cua act JSON: ${(error as Error).message}`); - } - const input = objectAt(parsed, "$", ["steps", "expect", "timeout_ms", "poll_ms", "successor", "tab_id"]); - const steps = input.steps; - if (!Array.isArray(steps) || steps.length < 1 || steps.length > 20) { - fail("$.steps", "expected an array with 1–20 steps"); - } - for (let index = 0; index < steps.length; index += 1) validateStep(steps[index], `$.steps[${index}]`); - if (input.expect !== undefined) validateExpectation(input.expect, "$.expect"); - optionalNumber(input.timeout_ms, "$.timeout_ms", 1, 30_000); - optionalNumber(input.poll_ms, "$.poll_ms", 10, 1_000); - optionalString(input.tab_id, "$.tab_id"); - if (input.successor !== undefined) { - const successor = objectAt(input.successor, "$.successor", ["filter", "depth"]); - if (successor.filter !== undefined && successor.filter !== "all" && successor.filter !== "interactive") { - fail("$.successor.filter", 'expected "all" or "interactive"'); - } - optionalNumber(successor.depth, "$.successor.depth"); - } - return { ...input, steps } as BrowserActInput; -} - -function validateStep(value: unknown, path: string): void { - const step = objectAt(value, path); - if (typeof step.type !== "string" || !STEP_TYPES.has(step.type)) { - fail(`${path}.type`, `expected one of ${[...STEP_TYPES].join(", ")}`); - } - const common = ["type", "timeout_ms", "expect"]; - switch (step.type) { - case "click": - assertOnlyKeys(step, path, [...common, "ref", "button", "num_clicks", "modifiers"]); - requiredString(step.ref, `${path}.ref`); - if (step.button !== undefined && !["left", "right", "middle"].includes(String(step.button))) { - fail(`${path}.button`, 'expected "left", "right", or "middle"'); - } - if (step.num_clicks !== undefined && (!Number.isInteger(step.num_clicks) || Number(step.num_clicks) < 1 || Number(step.num_clicks) > 3)) { - fail(`${path}.num_clicks`, "expected an integer from 1 to 3"); - } - if (step.modifiers !== undefined && (!Array.isArray(step.modifiers) || step.modifiers.some((entry) => typeof entry !== "string"))) { - fail(`${path}.modifiers`, "expected an array of strings"); - } - break; - case "hover": - case "scroll_to": - assertOnlyKeys(step, path, [...common, "ref"]); - requiredString(step.ref, `${path}.ref`); - break; - case "fill": - assertOnlyKeys(step, path, [...common, "ref", "value"]); - requiredString(step.ref, `${path}.ref`); - if (!["string", "number", "boolean"].includes(typeof step.value) || typeof step.value === "number" && !Number.isFinite(step.value)) { - fail(`${path}.value`, "expected a string, finite number, or boolean"); - } - break; - case "type": - assertOnlyKeys(step, path, [...common, "text"]); - requiredString(step.text, `${path}.text`); - break; - case "key": - assertOnlyKeys(step, path, [...common, "text", "repeat"]); - requiredString(step.text, `${path}.text`); - optionalNumber(step.repeat, `${path}.repeat`); - break; - case "wait": - assertOnlyKeys(step, path, [...common, "ms"]); - optionalNumber(step.ms, `${path}.ms`, 0, 30_000); - break; - } - optionalNumber(step.timeout_ms, `${path}.timeout_ms`, 1, 30_000); - if (step.expect !== undefined) validateExpectation(step.expect, `${path}.expect`); -} - -function validateExpectation(value: unknown, path: string): asserts value is CuaBrowserExpectation { - const expectation = objectAt(value, path); - if ("all" in expectation || "any" in expectation) { - const key = "all" in expectation ? "all" : "any"; - assertOnlyKeys(expectation, path, [key]); - const leaves = expectation[key]; - if (!Array.isArray(leaves) || leaves.length === 0) fail(`${path}.${key}`, "expected a non-empty array"); - for (let index = 0; index < leaves.length; index += 1) validateExpectationLeaf(leaves[index], `${path}.${key}[${index}]`); - return; - } - validateExpectationLeaf(expectation, path); -} - -function validateExpectationLeaf(value: unknown, path: string): void { - const leaf = objectAt(value, path); - switch (leaf.type) { - case "text": - assertOnlyKeys(leaf, path, ["type", "text", "exists"]); - requiredString(leaf.text, `${path}.text`); - optionalBoolean(leaf.exists, `${path}.exists`); - return; - case "role_name": - assertOnlyKeys(leaf, path, ["type", "role", "name", "exists"]); - optionalString(leaf.role, `${path}.role`); - optionalString(leaf.name, `${path}.name`); - if (leaf.role === undefined && leaf.name === undefined) fail(path, "role_name requires role or name"); - optionalBoolean(leaf.exists, `${path}.exists`); - return; - case "ref": { - assertOnlyKeys(leaf, path, ["type", "ref", "value", "checked", "selected", "expanded"]); - requiredString(leaf.ref, `${path}.ref`); - optionalString(leaf.value, `${path}.value`); - if (leaf.checked !== undefined && typeof leaf.checked !== "boolean" && leaf.checked !== "mixed") { - fail(`${path}.checked`, 'expected a boolean or "mixed"'); - } - optionalBoolean(leaf.selected, `${path}.selected`); - optionalBoolean(leaf.expanded, `${path}.expanded`); - if ([leaf.value, leaf.checked, leaf.selected, leaf.expanded].every((entry) => entry === undefined)) { - fail(path, "ref expectation requires value, checked, selected, or expanded"); - } - return; - } - case "url": - case "title": - assertOnlyKeys(leaf, path, ["type", "equals", "contains", "changed"]); - optionalString(leaf.equals, `${path}.equals`); - optionalString(leaf.contains, `${path}.contains`); - optionalBoolean(leaf.changed, `${path}.changed`); - if (leaf.equals === undefined && leaf.contains === undefined && leaf.changed === undefined) { - fail(path, `${String(leaf.type)} expectation requires equals, contains, or changed`); - } - return; - default: - fail(`${path}.type`, "expected text, role_name, ref, url, or title"); - } -} - -function objectAt(value: unknown, path: string, keys?: readonly string[]): JsonObject { - if (!value || typeof value !== "object" || Array.isArray(value)) fail(path, "expected an object"); - const object = value as JsonObject; - if (keys) assertOnlyKeys(object, path, keys); - return object; -} - -function assertOnlyKeys(value: JsonObject, path: string, allowed: readonly string[]): void { - const extras = Object.keys(value).filter((key) => !allowed.includes(key)); - if (extras.length) fail(path, `unexpected propert${extras.length === 1 ? "y" : "ies"}: ${extras.join(", ")}`); -} - -function requiredString(value: unknown, path: string): asserts value is string { - if (typeof value !== "string") fail(path, "expected a string"); -} - -function optionalString(value: unknown, path: string): void { - if (value !== undefined) requiredString(value, path); -} - -function optionalBoolean(value: unknown, path: string): void { - if (value !== undefined && typeof value !== "boolean") fail(path, "expected a boolean"); -} - -function optionalNumber(value: unknown, path: string, minimum?: number, maximum?: number): void { - if (value === undefined) return; - if (typeof value !== "number" || !Number.isFinite(value)) fail(path, "expected a finite number"); - if (minimum !== undefined && value < minimum) fail(path, `expected a number >= ${minimum}`); - if (maximum !== undefined && value > maximum) fail(path, `expected a number <= ${maximum}`); -} - -function fail(path: string, message: string): never { - throw new Error(`invalid cua act input at ${path}: ${message}`); -} diff --git a/packages/cli/src/cli-executor.ts b/packages/cli/src/cli-executor.ts deleted file mode 100644 index 5f1162e4..00000000 --- a/packages/cli/src/cli-executor.ts +++ /dev/null @@ -1,350 +0,0 @@ -import { - formatBrowserActResult, - InternalComputerTranslator, - type BatchReadResult, - type BrowserFindCandidate, - type BrowserRefState, -} from "@onkernel/cua-agent"; -import type { CuaActionBrowserAct } from "@onkernel/cua-ai"; -import { writeFile } from "node:fs/promises"; -import { stderr, stdout } from "node:process"; -import { emitCompact, type RunActionResult } from "./action/harness-runner"; -import { exitCodeFor, type ActionResult, type DeterministicActionType } from "./action/result"; -import { parseBrowserActInput } from "./browser-act-input"; -import { provisionForFlags, requireKernelApiKey, type HarnessCliFlags } from "./cli-harness"; -import { readNamedSessionRefs, writeNamedSessionRefs } from "./harness-named-sessions"; -import { captureScreenshot, type CuaBrowserHandle } from "./harness-browser"; - -/** - * Model-free subcommand plane. These commands validate argv, attach to (or - * provision) a Kernel browser, and call the executor directly over CDP or - * the computer batch API — no LLM harness, no model API key. - */ - -export type DeterministicRequest = - | { action: "open"; url: string } - | { action: "url" } - | { action: "snapshot"; filter?: "interactive" } - | { action: "act"; input: Omit } - | { action: "text" } - | { action: "find"; query: string } - | { action: "fill"; query: string; value: string } - | { action: "fill"; ref: string; value: string } - | { action: "press"; keys: string[] } - | { action: "click"; x: number; y: number } - | { action: "click"; ref: string } - | { action: "tabs" } - | { action: "screenshot"; out: string }; - -export const DETERMINISTIC_SUBCOMMANDS: ReadonlySet = new Set([ - "open", - "url", - "snapshot", - "act", - "text", - "find", - "fill", - "press", - "tabs", - "screenshot", -]); - -/** `cua click ` is deterministic; any other click argv is a model-mediated description. */ -export function isCoordinatePair(rest: string[]): boolean { - return rest.length === 2 && rest.every((token) => /^\d+$/.test(token)); -} - -/** An element ref minted by `cua snapshot` / `cua find`, e.g. `e12`. */ -export function isElementRef(token: string | undefined): token is string { - return token !== undefined && /^e\d+$/.test(token); -} - -/** Roles `cua fill` will target. Everything else is left to `click`/`type`. */ -const FILLABLE_ROLES: ReadonlySet = new Set([ - "textbox", - "searchbox", - "combobox", - "checkbox", - "radio", - "listbox", - "spinbutton", -]); - -/** Roles whose fill value is a checked state, not text. */ -const TOGGLE_ROLES: ReadonlySet = new Set(["checkbox", "radio"]); - -function parseToggleValue(raw: string): boolean { - const value = raw.trim().toLowerCase(); - if (["true", "1", "checked", "on"].includes(value)) return true; - if (["false", "0", "unchecked", "off"].includes(value)) return false; - throw new Error(`checkbox/radio value must be true|false|1|0|checked|unchecked|on|off, got ${JSON.stringify(raw)}`); -} - -/** - * Value for a ref-addressed fill, where the element's role is unknown until - * the browser resolves it. Toggle words become booleans — lossless for text - * controls (the page-side fill stringifies) and correct for checkboxes. - * "1"/"0" stay strings so select options and numeric inputs keep their value. - */ -function refFillValue(raw: string): string | boolean { - const value = raw.trim().toLowerCase(); - if (["true", "checked", "on"].includes(value)) return true; - if (["false", "unchecked", "off"].includes(value)) return false; - return raw; -} - -/** Resolve argv to a deterministic subcommand, or undefined when the model plane should handle it. */ -export function deterministicActionFor(first: string | undefined, rest: string[]): DeterministicActionType | undefined { - if (!first) return undefined; - if (DETERMINISTIC_SUBCOMMANDS.has(first)) return first as DeterministicActionType; - if (first === "click" && (isCoordinatePair(rest) || (rest.length === 1 && isElementRef(rest[0])))) return "click"; - return undefined; -} - -/** Parse and validate a deterministic subcommand's argv. Throws before any Kernel API call. */ -export function parseDeterministicArgs( - action: DeterministicActionType, - rest: string[], - flags: HarnessCliFlags, -): DeterministicRequest { - if (flags.filter !== undefined && action !== "snapshot") { - throw new Error("--filter only applies to cua snapshot"); - } - switch (action) { - case "open": { - const url = (rest[0] ?? "").trim(); - if (!url || rest.length > 1) throw new Error("usage: cua open "); - return { action, url }; - } - case "url": - if (rest.length > 0) throw new Error("usage: cua url"); - return { action }; - case "snapshot": { - if (rest.length > 0) throw new Error("usage: cua snapshot [--filter interactive]"); - const filter = flags.filter?.trim().toLowerCase(); - if (filter !== undefined && filter !== "interactive") { - throw new Error(`invalid --filter value "${flags.filter}"; expected: interactive`); - } - return { action, ...(filter === "interactive" ? { filter } : {}) }; - } - case "act": - if (rest.length !== 1) throw new Error("usage: cua act ''"); - return { action, input: parseBrowserActInput(rest[0]!) }; - case "text": - if (rest.length > 0) throw new Error("usage: cua text"); - return { action }; - case "find": { - const query = rest.join(" ").trim(); - if (!query) throw new Error('usage: cua find ""'); - return { action, query }; - } - case "fill": { - const target = (rest[0] ?? "").trim(); - const value = rest[1]; - if (!target || value === undefined || rest.length > 2) { - throw new Error('usage: cua fill ""'); - } - if (isElementRef(target)) return { action, ref: target, value }; - return { action, query: target, value }; - } - case "press": { - const keys = rest.map((key) => key.trim()).filter((key) => key.length > 0); - if (keys.length === 0) throw new Error("usage: cua press [key...]"); - return { action, keys }; - } - case "click": { - if (rest.length === 1 && isElementRef(rest[0])) return { action, ref: rest[0] }; - if (!isCoordinatePair(rest)) throw new Error("usage: cua click | cua click "); - return { action, x: Number(rest[0]), y: Number(rest[1]) }; - } - case "tabs": - if (rest.length > 0) throw new Error("usage: cua tabs"); - return { action }; - case "screenshot": { - if (rest.length > 0) throw new Error("usage: cua screenshot [--out file|-]"); - return { action, out: flags.out ?? "screenshot.png" }; - } - } -} - -/** - * Persistence seam for element refs so they survive across invocations of - * the same named session. Absent for fresh (non `-s`) browsers, whose refs - * cannot outlive the browser anyway. - */ -export interface RefStateStore { - load(): Promise; - save(state: BrowserRefState): Promise; -} - -/** Run a deterministic subcommand end to end: parse, provision/attach, execute, print, tear down. */ -export async function runDeterministicCommand( - action: DeterministicActionType, - rest: string[], - flags: HarnessCliFlags, -): Promise { - const req = parseDeterministicArgs(action, rest, flags); - const { apiKey, baseUrl } = requireKernelApiKey(); - const provisioned = await provisionForFlags(flags, { kernelApiKey: apiKey, kernelBaseUrl: baseUrl }); - const name = flags.namedSession; - const refStore: RefStateStore | undefined = name - ? { - load: () => readNamedSessionRefs(name), - save: (state) => writeNamedSessionRefs(name, state), - } - : undefined; - return runDeterministicOnHandle(req, provisioned.handle, defaultTranslator, refStore); -} - -/** Execute a parsed request against a browser handle. Split from provisioning for tests. */ -export async function runDeterministicOnHandle( - req: DeterministicRequest, - handle: CuaBrowserHandle, - createTranslator: (handle: CuaBrowserHandle) => InternalComputerTranslator = defaultTranslator, - refStore?: RefStateStore, -): Promise { - const translator = createTranslator(handle); - try { - if (refStore) { - const state = await refStore.load(); - if (state) translator.browser().importRefState(state); - } - const res = await executeDeterministic(req, translator, handle); - return emitCompact(res); - } finally { - if (refStore) { - try { - await refStore.save(translator.browser().exportRefState()); - } catch (err) { - stderr.write(`[cua] cleanup warning: ${(err as Error).message}\n`); - } - } - try { - translator.dispose(); - } catch (err) { - stderr.write(`[cua] cleanup warning: ${(err as Error).message}\n`); - } - try { - await handle.close(); - } catch (err) { - stderr.write(`[cua] cleanup warning: ${(err as Error).message}\n`); - } - } -} - -function defaultTranslator(handle: CuaBrowserHandle): InternalComputerTranslator { - return new InternalComputerTranslator({ browser: handle.browser, client: handle.client }); -} - -async function executeDeterministic( - req: DeterministicRequest, - translator: InternalComputerTranslator, - handle: CuaBrowserHandle, -): Promise { - const startedAt = Date.now(); - const finish = (partial: Omit): RunActionResult => { - const result: ActionResult = { ...partial, elapsedMs: Date.now() - startedAt, timestamp: Date.now() }; - return { result, exitCode: exitCodeFor(result) }; - }; - try { - switch (req.action) { - case "open": - await translator.browser().execute({ type: "browser_navigate", url: req.url }); - return finish({ action: req.action, status: "ok" }); - case "url": - return finish({ action: req.action, status: "ok", url: await translator.browser().currentUrl() }); - case "snapshot": { - const reads = await translator.browser().execute({ type: "browser_snapshot", ...(req.filter ? { filter: req.filter } : {}) }); - return finish({ action: req.action, status: "ok", text: readText(reads) }); - } - case "act": { - const reads = await translator.browser().execute({ type: "browser_act", ...req.input }); - const result = reads.find((read) => read.type === "browser_act")?.result; - if (!result) throw new Error("browser_act returned no plan result"); - const completed = finish({ action: req.action, status: "ok", text: formatBrowserActResult(result) }); - return { ...completed, exitCode: result.outcome === "worked" ? 0 : 1 }; - } - case "text": { - const reads = await translator.browser().execute({ type: "browser_text" }); - return finish({ action: req.action, status: "ok", text: readText(reads) }); - } - case "find": { - const candidates = await translator.browser().findCandidates(req.query); - if (candidates.length === 0) { - return finish({ action: req.action, status: "not_found", text: `no elements matched ${JSON.stringify(req.query)}` }); - } - return finish({ action: req.action, status: "ok", text: candidates.map(formatCandidate).join("\n") }); - } - case "fill": { - const executor = translator.browser(); - if ("ref" in req) { - await executor.execute({ type: "browser_fill", ref: req.ref, value: refFillValue(req.value) }); - return finish({ action: req.action, status: "ok", text: req.ref }); - } - const candidates = await executor.findCandidates(req.query, undefined, FILLABLE_ROLES); - if (candidates.length === 0) { - return finish({ action: req.action, status: "not_found", text: `no fillable element matched ${JSON.stringify(req.query)}` }); - } - const tied = candidates.filter((c) => c.score === candidates[0]!.score); - if (tied.length > 1) { - const listing = tied.map((c) => `${c.role} ${JSON.stringify(c.name)}`).join(", "); - return finish({ - action: req.action, - status: "not_found", - text: `ambiguous query ${JSON.stringify(req.query)} (${tied.length} matches): ${listing}`, - }); - } - const match = candidates[0]!; - const value = TOGGLE_ROLES.has(match.role) ? parseToggleValue(req.value) : req.value; - await executor.execute({ type: "browser_fill", ref: match.ref, value }); - return finish({ action: req.action, status: "ok", text: `${match.role} ${JSON.stringify(match.name)}` }); - } - case "press": - await translator.executeBatch([{ type: "keypress", keys: req.keys }]); - return finish({ action: req.action, status: "ok" }); - case "click": - if ("ref" in req) { - await translator.browser().execute({ type: "browser_click", ref: req.ref }); - return finish({ action: req.action, status: "ok", text: req.ref }); - } - await translator.executeBatch([{ type: "click", x: req.x, y: req.y }]); - return finish({ action: req.action, status: "ok", coordinates: [req.x, req.y] }); - case "tabs": { - const reads = await translator.browser().execute({ type: "browser_list_tabs" }); - return finish({ action: req.action, status: "ok", text: readText(reads) }); - } - case "screenshot": { - const png = await captureScreenshot(handle.client, handle.browser.session_id); - if (!png) { - return finish({ action: req.action, status: "error", text: "failed to capture screenshot" }); - } - if (req.out === "-") { - // stdout is the PNG bytes; the compact status line would corrupt a pipe. - stdout.write(png); - return finish({ action: req.action, status: "ok", text: "" }); - } - await writeFile(req.out, png); - return finish({ action: req.action, status: "ok", text: req.out }); - } - } - } catch (err) { - const message = (err as Error).message; - // A stale ref is "not found" (exit 1): the caller should re-snapshot, - // same as a failed description match — not an infrastructure error. - const status = /stale|not on the current page/i.test(message) ? "not_found" : "error"; - return finish({ action: req.action, status, text: message }); - } -} - -function formatCandidate(candidate: BrowserFindCandidate): string { - const name = candidate.name ? ` ${JSON.stringify(candidate.name)}` : ""; - return `${candidate.role || "node"}${name} [${candidate.ref}]`; -} - -function readText(reads: BatchReadResult[]): string { - const parts: string[] = []; - for (const read of reads) { - if (read.type === "browser_text") parts.push(read.text); - } - return parts.join("\n"); -} diff --git a/packages/cli/src/cli-harness.ts b/packages/cli/src/cli-harness.ts deleted file mode 100644 index f62cc812..00000000 --- a/packages/cli/src/cli-harness.ts +++ /dev/null @@ -1,862 +0,0 @@ -import { - InMemorySessionRepo, - type JsonlSessionMetadata, - type JsonlSessionRepo, - NodeExecutionEnv, - type Session, - type Skill, -} from "@onkernel/cua-agent"; -import { - cuaApiKeyEnvVarsForProvider, - type CuaModelRef, - type CuaToolMenuEntry, - cuaToolMenu, - isCuaToolSpec, - parseCuaModelRef, - requireCuaEnvApiKey, -} from "@onkernel/cua-ai"; -import { parseArgs } from "node:util"; -import { stderr, stdout } from "node:process"; -import type { CuaBrowserHandle } from "./harness-browser"; -import { - type ActionRequest, - type ModelActionType, -} from "./action/prompts"; -import { runAction, emitCompact } from "./action/harness-runner"; -import { - buildCuaHarness, - type CuaCliCatalog, - type CuaCliHarness, - defaultApplicationTools, - defaultInteractionTools, -} from "./harness"; -import { provisionBrowser } from "./harness-browser"; -import { DEFAULT_CUA_MODEL_REF, listSupportedModels, resolveCuaModelRef } from "./harness-models"; -import { - attachNamedSession, - formatRelativeAge, - listNamedSessions, - type NamedSessionMetadata, - readNamedSession, - recordSessionModel, - recordTranscriptPath, - shortKernelId, - startNamedSession, - stopNamedSession, - validateSlug, -} from "./harness-named-sessions"; -import { - appendBrowserEntry, - createSession, - createSessionRepo, - findLatestSession, - listSessionsForCwd, - openSession, - readMetadataFromFile, - resolveSessionRef, -} from "./harness-sessions"; -import { type ContextFile, discoverCuaSkills } from "./harness-skills"; -import { runPrint } from "./print"; - -const MODELS_HELP = `cua models — list selectable -m/--model values - -Usage: - cua models - cua models -p openai - cua models --provider anthropic - cua models --json - -Options: - -p, --provider Filter by provider id (any pi-ai provider; gemini/moonshot are aliases) - --json Output JSON - -h, --help Show this help -`; - -interface ModelsFlags { - provider?: string; - json: boolean; - help: boolean; -} - -function parseModelsArgs(argv: string[]): ModelsFlags { - const parsed = parseArgs({ - args: argv, - options: { - provider: { type: "string", short: "p" }, - json: { type: "boolean", default: false }, - help: { type: "boolean", short: "h", default: false }, - }, - allowPositionals: true, - strict: true, - }); - const positionalProvider = parsed.positionals[0]; - if (parsed.positionals.length > 1) { - throw new Error(`unexpected arguments: ${parsed.positionals.slice(1).join(" ")}`); - } - return { - provider: (parsed.values.provider as string | undefined) ?? positionalProvider, - json: !!parsed.values.json, - help: !!parsed.values.help, - }; -} - -/** `cua models` subcommand backed by cua-ai's `listCuaModels()`. */ -export async function runModelsSubcommand(argv: string[]): Promise { - let flags: ModelsFlags; - try { - flags = parseModelsArgs(argv); - } catch (err) { - stderr.write(`${(err as Error).message}\n\n${MODELS_HELP}`); - return 2; - } - if (flags.help) { - stdout.write(MODELS_HELP); - return 0; - } - let models; - try { - models = listSupportedModels(flags.provider); - } catch (err) { - stderr.write(`${(err as Error).message}\n`); - return 2; - } - if (flags.json) { - stdout.write(`${JSON.stringify(models, null, 2)}\n`); - return 0; - } - stdout.write(formatModelsTable(models)); - return 0; -} - -function formatModelsTable(models: ReturnType): string { - const rows = models.map((entry) => ({ - ref: entry.ref, - provider: entry.provider, - model: entry.model, - default: entry.ref === DEFAULT_CUA_MODEL_REF ? "yes" : "", - native: entry.nativeSurfaces.join(","), - name: entry.name, - })); - const headers = { ref: "REF", provider: "PROVIDER", model: "MODEL", default: "DEFAULT", native: "NATIVE", name: "NAME" }; - const widths = { - ref: columnWidth(headers.ref, rows.map((r) => r.ref)), - provider: columnWidth(headers.provider, rows.map((r) => r.provider)), - model: columnWidth(headers.model, rows.map((r) => r.model)), - default: columnWidth(headers.default, rows.map((r) => r.default)), - native: columnWidth(headers.native, rows.map((r) => r.native)), - name: columnWidth(headers.name, rows.map((r) => r.name)), - }; - const lines = [ - [ - headers.ref.padEnd(widths.ref), - headers.provider.padEnd(widths.provider), - headers.model.padEnd(widths.model), - headers.default.padEnd(widths.default), - headers.native.padEnd(widths.native), - headers.name, - ].join(" "), - [ - "-".repeat(widths.ref), - "-".repeat(widths.provider), - "-".repeat(widths.model), - "-".repeat(widths.default), - "-".repeat(widths.native), - "-".repeat(widths.name), - ].join(" "), - ]; - for (const row of rows) { - lines.push( - [ - row.ref.padEnd(widths.ref), - row.provider.padEnd(widths.provider), - row.model.padEnd(widths.model), - row.default.padEnd(widths.default), - row.native.padEnd(widths.native), - row.name, - ].join(" "), - ); - } - return `${lines.join("\n")}\n`; -} - -const TOOLS_HELP = `cua tools — list the tools CUA can offer for a model - -Usage: - cua tools - cua tools -m anthropic:claude-opus-5 - cua tools --json - -Options: - -m, --model Model to build the menu for (default: the CLI default model) - --json Output JSON - -h, --help Show this help - -Availability is decided by compiling the resulting catalog, so a tool listed as -available is one the selected model will accept. -`; - -interface ToolsFlags { - model?: string; - json: boolean; - help: boolean; -} - -function parseToolsArgs(argv: string[]): ToolsFlags { - const parsed = parseArgs({ - args: argv, - options: { - model: { type: "string", short: "m" }, - json: { type: "boolean", default: false }, - help: { type: "boolean", short: "h", default: false }, - }, - allowPositionals: true, - strict: true, - }); - if (parsed.positionals.length > 0) { - throw new Error(`unexpected arguments: ${parsed.positionals.join(" ")}`); - } - return { - model: parsed.values.model as string | undefined, - json: !!parsed.values.json, - help: !!parsed.values.help, - }; -} - -/** `cua tools` subcommand: the model-derived tool menu, as `cua models` is to the catalog. */ -export async function runToolsSubcommand(argv: string[]): Promise { - let flags: ToolsFlags; - try { - flags = parseToolsArgs(argv); - } catch (err) { - stderr.write(`${(err as Error).message}\n\n${TOOLS_HELP}`); - return 2; - } - if (flags.help) { - stdout.write(TOOLS_HELP); - return 0; - } - let menu: CuaToolMenuEntry[]; - let modelRef: CuaModelRef; - try { - modelRef = resolveCuaModelRef(flags.model); - menu = cuaToolMenu(modelRef, defaultInteractionTools(modelRef).filter(isCuaToolSpec)); - } catch (err) { - stderr.write(`${(err as Error).message}\n`); - return 2; - } - if (flags.json) { - stdout.write(`${JSON.stringify({ model: modelRef, tools: menu.map(toJsonEntry) }, null, 2)}\n`); - return 0; - } - stdout.write(formatToolsTable(modelRef, menu)); - return 0; -} - -function toJsonEntry(entry: CuaToolMenuEntry) { - return { - key: entry.key, - label: entry.label, - group: entry.group, - selected: entry.selected, - available: entry.available, - ...(entry.unavailableReason ? { unavailable_reason: entry.unavailableReason } : {}), - ...(entry.description ? { description: entry.description } : {}), - }; -} - -function formatToolsTable(modelRef: CuaModelRef, menu: readonly CuaToolMenuEntry[]): string { - const rows = menu.map((entry) => ({ - tool: entry.label, - group: entry.group, - state: entry.available ? (entry.selected ? "default" : "available") : "unavailable", - note: entry.available ? entry.description ?? "" : entry.unavailableReason ?? "", - })); - const headers = { tool: "TOOL", group: "GROUP", state: "STATE", note: "NOTE" }; - const widths = { - tool: columnWidth(headers.tool, rows.map((r) => r.tool)), - group: columnWidth(headers.group, rows.map((r) => r.group)), - state: columnWidth(headers.state, rows.map((r) => r.state)), - }; - const lines = [ - `model: ${modelRef}`, - "", - [headers.tool.padEnd(widths.tool), headers.group.padEnd(widths.group), headers.state.padEnd(widths.state), headers.note].join(" "), - ["-".repeat(widths.tool), "-".repeat(widths.group), "-".repeat(widths.state), "-".repeat(headers.note.length)].join(" "), - ]; - for (const row of rows) { - lines.push([row.tool.padEnd(widths.tool), row.group.padEnd(widths.group), row.state.padEnd(widths.state), row.note].join(" ")); - } - return `${lines.join("\n")}\n`; -} - -function columnWidth(header: string, values: string[]): number { - return Math.max(header.length, ...values.map((value) => value.length)); -} - -export interface HarnessCliFlags { - verbose: boolean; - profileSaveChanges: boolean; - continueLatest: boolean; - resumePicker: boolean; - noSession: boolean; - noSkills: boolean; - debugTui: boolean; - jsonlIncludeDeltas: boolean; - jsonlIncludeImages: boolean; - model?: string; - thinking?: string; - browserProfile?: string; - browserProxy?: string; - browserTimeout?: number; - maxSteps?: number; - out?: string; - output?: string; - filter?: string; - imageProtocol?: string; - namedSession?: string; - sessionRef?: string; - sessionDir?: string; - skillPaths: string[]; -} - -export interface KernelAuth { - kernelApiKey: string; - kernelBaseUrl?: string; -} - -interface ResolvedAuth extends KernelAuth { - modelRef: CuaModelRef; -} - -export function requireKernelApiKey(): { apiKey: string; baseUrl?: string } { - const apiKey = process.env.KERNEL_API_KEY?.trim(); - if (!apiKey) throw new Error("missing Kernel API key (set KERNEL_API_KEY)"); - const baseUrl = process.env.KERNEL_BASE_URL?.trim() || undefined; - return { apiKey, baseUrl }; -} - -function resolveAuth(flags: HarnessCliFlags): ResolvedAuth { - const { apiKey, baseUrl } = requireKernelApiKey(); - const modelRef = resolveCuaModelRef(flags.model); - const { provider } = parseCuaModelRef(modelRef); - // Preflight only where CUA documents the variable names; for any other - // pi-ai provider the credential is pi's to resolve when it streams, and - // failing here would refuse a model that works. - if (cuaApiKeyEnvVarsForProvider(provider).length > 0) requireCuaEnvApiKey(provider); - return { kernelApiKey: apiKey, kernelBaseUrl: baseUrl, modelRef }; -} - -export interface ProvisionedBrowser { - handle: CuaBrowserHandle; - named?: NamedSessionMetadata; -} - -export async function provisionForFlags(flags: HarnessCliFlags, auth: KernelAuth): Promise { - if (flags.namedSession) { - const { client, browser, meta } = await attachNamedSession({ - name: flags.namedSession, - apiKey: auth.kernelApiKey, - baseUrl: auth.kernelBaseUrl, - }); - if (flags.verbose) { - stderr.write(`[cua] attached named session "${meta.name}" (browser=${browser.session_id})\n`); - if (browser.browser_live_view_url) stderr.write(`[cua] live view=${browser.browser_live_view_url}\n`); - } - const handle: CuaBrowserHandle = { - client, - browser, - profileId: meta.profile_id, - async close(): Promise { - // no-op: named-session browsers are torn down via `cua session stop`. - }, - }; - return { handle, named: meta }; - } - if (flags.verbose) stderr.write("[cua] provisioning Kernel browser...\n"); - const handle = await provisionBrowser({ - apiKey: auth.kernelApiKey, - baseUrl: auth.kernelBaseUrl, - timeoutSeconds: flags.browserTimeout, - profileSelector: flags.browserProfile, - saveChanges: flags.profileSaveChanges, - proxySelector: flags.browserProxy, - }); - if (flags.verbose) { - stderr.write(`[cua] browser session=${handle.browser.session_id}\n`); - if (handle.browser.browser_live_view_url) { - stderr.write(`[cua] live view=${handle.browser.browser_live_view_url}\n`); - } - } - return { handle }; -} - -interface ResolvedSession { - session: Session; - transcriptPath: string; - resumed: boolean; -} - -async function resolveSession( - repo: JsonlSessionRepo, - cwd: string, - flags: HarnessCliFlags, - namedMeta?: NamedSessionMetadata, -): Promise { - if (flags.noSession) return undefined; - if (flags.sessionRef) { - const metadata = await resolveSessionRef(repo, cwd, flags.sessionRef); - return { session: await openSession(repo, metadata), transcriptPath: metadata.path, resumed: true }; - } - if (flags.continueLatest) { - const latest = await findLatestSession(repo, cwd); - if (!latest) { - stderr.write("[cua] no previous session for this cwd; starting fresh\n"); - const fresh = await createSession(repo, cwd); - const metadata = await fresh.getMetadata(); - return { session: fresh, transcriptPath: metadata.path, resumed: false }; - } - return { session: await openSession(repo, latest), transcriptPath: latest.path, resumed: true }; - } - if (flags.resumePicker) { - const sessions = await listSessionsForCwd(repo, cwd); - if (sessions.length === 0) { - stderr.write("[cua] no previous sessions for this cwd; starting fresh\n"); - const fresh = await createSession(repo, cwd); - const metadata = await fresh.getMetadata(); - return { session: fresh, transcriptPath: metadata.path, resumed: false }; - } - const picked = await pickSession(sessions); - if (!picked) { - const fresh = await createSession(repo, cwd); - const metadata = await fresh.getMetadata(); - return { session: fresh, transcriptPath: metadata.path, resumed: false }; - } - return { session: await openSession(repo, picked), transcriptPath: picked.path, resumed: true }; - } - if (namedMeta?.transcript_path) { - const direct = await readMetadataFromFile(namedMeta.transcript_path); - if (direct) { - return { session: await openSession(repo, direct), transcriptPath: direct.path, resumed: true }; - } - } - const fresh = await createSession(repo, cwd); - const metadata = await fresh.getMetadata(); - return { session: fresh, transcriptPath: metadata.path, resumed: false }; -} - -async function pickSession(sessions: JsonlSessionMetadata[]): Promise { - const sorted = [...sessions].sort((a, b) => b.createdAt.localeCompare(a.createdAt)); - stderr.write("\nResume which session?\n"); - const limit = Math.min(sorted.length, 20); - for (let i = 0; i < limit; i++) { - const s = sorted[i]!; - stderr.write(` [${i + 1}] ${s.id.slice(0, 8)} · ${s.createdAt}\n`); - } - if (sorted.length > limit) { - stderr.write(` (${sorted.length - limit} more not shown; use --session to select directly)\n`); - } - const { createInterface } = await import("node:readline/promises"); - const rl = createInterface({ input: process.stdin, output: process.stderr }); - try { - const answer = (await rl.question("Pick a number (or blank to skip): ")).trim(); - if (!answer) return undefined; - const n = Number(answer); - if (!Number.isFinite(n) || n < 1 || n > limit) { - stderr.write("[cua] invalid selection; starting fresh\n"); - return undefined; - } - return sorted[n - 1]; - } finally { - rl.close(); - } -} - -interface HarnessRuntime { - handle: CuaBrowserHandle; - resolved: ResolvedSession | undefined; - session: Session; - skills: Skill[]; - contextFiles: ContextFile[]; - applicationTools: ReturnType; - harness: CuaCliHarness; - catalog: CuaCliCatalog; - provider: string; - modelRef: CuaModelRef; -} - -export interface SetupHarnessRuntimeOptions { - /** - * When true, never create or open a JsonlSession; use an InMemorySession instead. - * One-shot action subcommands without -s/-c/-r/--session pass this so they - * don't pollute the on-disk transcript list. The print path always persists - * (so `-c` / `--session latest` keeps working). - */ - skipDiskSession?: boolean; -} - -/** Default -m from a named session's stored model when not passed explicitly. */ -export function applyNamedSessionDefaults(flags: HarnessCliFlags, meta: NamedSessionMetadata): HarnessCliFlags { - return { ...flags, model: flags.model ?? meta.model }; -} - -async function setupHarnessRuntime( - flags: HarnessCliFlags, - opts: SetupHarnessRuntimeOptions = {}, -): Promise { - if (flags.namedSession) { - const named = await readNamedSession(flags.namedSession); - if (named) flags = applyNamedSessionDefaults(flags, named); - } - const auth = resolveAuth(flags); - const cwd = process.cwd(); - const env = new NodeExecutionEnv({ cwd }); - const { skills, contextFiles } = await discoverCuaSkills({ - cwd, - env, - extraPaths: flags.skillPaths, - disabled: flags.noSkills, - }); - - const provisioned = await provisionForFlags(flags, auth); - try { - return await finishHarnessRuntime(flags, auth, provisioned, { cwd, skills, contextFiles, skipDisk: opts.skipDiskSession === true }); - } catch (err) { - await provisioned.handle.close().catch(() => {}); - throw err; - } -} - -interface FinishHarnessRuntimeContext { - cwd: string; - skills: Skill[]; - contextFiles: ContextFile[]; - skipDisk: boolean; -} - -async function finishHarnessRuntime( - flags: HarnessCliFlags, - auth: ResolvedAuth, - provisioned: ProvisionedBrowser, - context: FinishHarnessRuntimeContext, -): Promise { - const { cwd, skills, contextFiles } = context; - const repo = createSessionRepo(flags.sessionDir); - - const skipDisk = context.skipDisk && !hasExplicitSessionFlag(flags); - const resolved = skipDisk ? undefined : await resolveSession(repo, cwd, flags, provisioned.named); - - let inMemorySession: Session | undefined; - if (!resolved) { - const memRepo = new InMemorySessionRepo(); - inMemorySession = await memRepo.create(); - } - - const session = resolved?.session ?? inMemorySession!; - const { provider } = parseCuaModelRef(auth.modelRef); - - if (resolved) { - await appendBrowserEntry(session, { - sessionId: provisioned.handle.browser.session_id, - liveUrl: provisioned.handle.browser.browser_live_view_url, - profileId: provisioned.handle.profileId, - createdAt: Date.now(), - }); - if (provisioned.named) { - await recordTranscriptPath(provisioned.named.name, resolved.transcriptPath); - await recordSessionModel(provisioned.named.name, { model: auth.modelRef }); - } - if (flags.verbose) { - stderr.write(`[cua] session=${resolved.transcriptPath}\n`); - if (resolved.resumed) stderr.write("[cua] resumed prior session into fresh browser\n"); - } - } - - const thinkingLevel = mapThinkingLevel(flags.thinking); - const baseUrlOverride = providerBaseUrlOverride(provider); - const applicationTools = defaultApplicationTools(); - const { harness, catalog } = buildCuaHarness({ - cwd, - client: provisioned.handle.client, - browser: provisioned.handle.browser, - session, - model: auth.modelRef, - skills, - contextFiles, - thinkingLevel, - tools: [...defaultInteractionTools(auth.modelRef), ...applicationTools], - modelBaseUrl: baseUrlOverride, - }); - - return { - handle: provisioned.handle, - resolved, - session, - skills, - contextFiles, - applicationTools, - harness, - catalog, - provider, - modelRef: auth.modelRef, - }; -} - -function hasExplicitSessionFlag(flags: HarnessCliFlags): boolean { - return ( - !!flags.sessionRef || - flags.continueLatest || - flags.resumePicker || - !!flags.namedSession - ); -} - -function providerBaseUrlOverride(provider: string): string | undefined { - const envName = `${provider.toUpperCase()}_BASE_URL`; - const value = process.env[envName]?.trim(); - return value && value.length > 0 ? value : undefined; -} - -/** Map a `--thinking` flag value to pi's thinking level; unset/empty defaults to `"low"`, and invalid values throw. */ -export function mapThinkingLevel(raw: string | undefined): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" { - const v = (raw ?? "low").trim().toLowerCase(); - switch (v) { - case "off": - case "none": - return "off"; - case "minimal": - return "minimal"; - case "medium": - return "medium"; - case "high": - return "high"; - case "xhigh": - return "xhigh"; - case "max": - return "max"; - case "low": - case "": - return "low"; - default: - throw new Error( - `invalid --thinking value "${raw}"; expected one of: off | minimal | low | medium | high | xhigh | max`, - ); - } -} - -/** Run a single prompt through the new harness wiring (`--print`). */ -export async function runPrintCommand(prompt: string, flags: HarnessCliFlags): Promise { - const runtime = await setupHarnessRuntime(flags); - const jsonlMode = (flags.output ?? "text").toLowerCase() === "jsonl"; - try { - return await runPrint({ - harness: runtime.harness, - browserHandle: runtime.handle, - modelRef: runtime.modelRef, - provider: runtime.provider, - prompt, - skills: runtime.skills, - verbose: flags.verbose, - jsonlMode, - jsonlIncludeDeltas: flags.jsonlIncludeDeltas, - jsonlIncludeImages: flags.jsonlIncludeImages, - }); - } finally { - try { - await runtime.handle.close(); - } catch (err) { - stderr.write(`[cua] cleanup warning: ${(err as Error).message}\n`); - } - } -} - -/** Run the interactive TUI through the new harness wiring. */ -export async function runInteractiveCommand( - initialPrompt: string, - flags: HarnessCliFlags, -): Promise { - const runtime = await setupHarnessRuntime(flags); - const { runInteractive } = await import("./tui/main"); - try { - return await runInteractive({ - cwd: process.cwd(), - harness: runtime.harness, - catalog: runtime.catalog, - browserHandle: runtime.handle, - session: runtime.session, - skills: runtime.skills, - contextFiles: runtime.contextFiles, - modelRef: runtime.modelRef, - provider: runtime.provider, - applicationTools: runtime.applicationTools, - interactionToolsForModel: defaultInteractionTools, - initialPrompt: initialPrompt || undefined, - imageProtocol: flags.imageProtocol, - debugTui: flags.debugTui, - resumed: runtime.resolved?.resumed === true, - transcriptPath: runtime.resolved?.transcriptPath, - namedSession: flags.namedSession, - }); - } finally { - try { - await runtime.handle.close(); - } catch (err) { - stderr.write(`[cua] cleanup warning: ${(err as Error).message}\n`); - } - } -} - -/** Run a one-shot model-mediated action subcommand through the harness wiring. */ -export async function runActionCommand( - action: ModelActionType, - rest: string[], - flags: HarnessCliFlags, -): Promise { - const runtime = await setupHarnessRuntime(flags, { skipDiskSession: true }); - const req: ActionRequest = buildActionRequest(action, rest); - if (flags.maxSteps !== undefined) req.maxTurns = flags.maxSteps; - try { - const res = await runAction(req, { - harness: runtime.harness, - }); - return emitCompact(res); - } finally { - try { - await runtime.handle.close(); - } catch (err) { - stderr.write(`[cua] cleanup warning: ${(err as Error).message}\n`); - } - } -} - -function buildActionRequest(action: ModelActionType, rest: string[]): ActionRequest { - switch (action) { - case "click": - return { action, target: rest.join(" ") }; - case "type": - return { action, target: rest[0], text: rest[1] }; - case "observe": - return { action, text: rest.join(" ") }; - case "do": - return { action, text: rest.join(" ") }; - } -} - -/** Named-session subcommand handlers wired to the new SDK-backed implementation. */ -export async function runSessionSubcommand(args: string[], flags: HarnessCliFlags): Promise { - const sub = args[0]; - if (!sub || sub === "help" || sub === "--help" || sub === "-h") { - stdout.write(`${sessionHelp()}\n`); - return 0; - } - const auth = resolveAuthOrFail(); - switch (sub) { - case "start": { - const name = (args[1] ?? "").trim() || generateSessionSlug(); - validateSlug(name); - const { meta, metadataPath, browser } = await startNamedSession({ - name, - apiKey: auth.kernelApiKey, - baseUrl: auth.kernelBaseUrl, - browserTimeoutSeconds: flags.browserTimeout, - profileSelector: flags.browserProfile, - saveProfileChanges: flags.profileSaveChanges, - proxySelector: flags.browserProxy, - model: flags.model ? resolveCuaModelRef(flags.model) : undefined, - }); - stdout.write(`name=${meta.name}\n`); - stdout.write(`kernel_session_id=${browser.session_id}\n`); - if (browser.browser_live_view_url) stdout.write(`live_url=${browser.browser_live_view_url}\n`); - stdout.write(`metadata=${metadataPath}\n`); - stdout.write(`\nUse: cua -s ${meta.name} ...\n`); - return 0; - } - case "stop": { - const name = (args[1] ?? "").trim(); - if (!name) { - stderr.write("usage: cua session stop \n"); - return 2; - } - validateSlug(name); - const result = await stopNamedSession({ - name, - apiKey: auth.kernelApiKey, - baseUrl: auth.kernelBaseUrl, - }); - if (!result.existed) { - stderr.write(`no named session "${name}"\n`); - return 1; - } - stdout.write( - result.kernelDeleted - ? `stopped ${name} (kernel browser deleted)\n` - : `stopped ${name} (kernel browser was already gone)\n`, - ); - return 0; - } - case "list": { - const sessions = await listNamedSessions(); - if (sessions.length === 0) { - stdout.write("(no named sessions; run `cua session start [name]`)\n"); - return 0; - } - const header = ["NAME", "KERNEL_ID", "AGE", "LIVE_URL"].join("\t"); - stdout.write(`${header}\n`); - for (const s of sessions) { - stdout.write( - [ - s.name, - shortKernelId(s.kernel_session_id), - formatRelativeAge(s.created_at), - s.live_url ?? "-", - ].join("\t") + "\n", - ); - } - return 0; - } - case "show": { - const name = (args[1] ?? "").trim(); - if (!name) { - stderr.write("usage: cua session show \n"); - return 2; - } - validateSlug(name); - const sessions = await listNamedSessions(); - const meta = sessions.find((s) => s.name === name); - if (!meta) { - stderr.write(`no named session "${name}"\n`); - return 1; - } - stdout.write(`${JSON.stringify(meta, null, 2)}\n`); - return 0; - } - default: - stderr.write(`unknown session subcommand: ${sub}\n${sessionHelp()}\n`); - return 2; - } -} - -function resolveAuthOrFail(): { kernelApiKey: string; kernelBaseUrl?: string } { - const { apiKey, baseUrl } = requireKernelApiKey(); - return { kernelApiKey: apiKey, kernelBaseUrl: baseUrl }; -} - -function generateSessionSlug(): string { - const adjectives = ["calm", "brisk", "swift", "quiet", "bright", "sharp"]; - const nouns = ["fox", "owl", "lynx", "hawk", "wolf", "moth"]; - const adj = adjectives[Math.floor(Math.random() * adjectives.length)] ?? "calm"; - const noun = nouns[Math.floor(Math.random() * nouns.length)] ?? "fox"; - const stamp = Date.now().toString(36).slice(-4); - return `${adj}-${noun}-${stamp}`; -} - -function sessionHelp(): string { - return [ - "cua session start [name] Start a new named browser session.", - "cua session stop Tear down a named session.", - "cua session list List existing named sessions.", - "cua session show Print full metadata for a named session.", - "", - "Use `-s ` on any other command to reuse the named session's", - "browser (e.g. `cua -s login open https://...`).", - ].join("\n"); -} diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts deleted file mode 100644 index c3dd0dcd..00000000 --- a/packages/cli/src/cli.ts +++ /dev/null @@ -1,345 +0,0 @@ -#!/usr/bin/env node -import { stderr, stdout } from "node:process"; -import { parseArgs } from "node:util"; -import { type ModelActionType } from "./action/prompts"; -import { deterministicActionFor, runDeterministicCommand } from "./cli-executor"; -import { - runActionCommand, - runInteractiveCommand, - runModelsSubcommand as runModelsSubcommandHarness, - runToolsSubcommand as runToolsSubcommandHarness, - runPrintCommand, - runSessionSubcommand as runSessionSubcommandHarness, - type HarnessCliFlags, -} from "./cli-harness"; -import { DEFAULT_CUA_MODEL_REF } from "./harness-models"; - -const HELP = `cua — Kernel-cloud-browser computer-use agent - -Usage: - cua [options] [prompt...] - cua --print "go to news.ycombinator.com and summarize" - cua open - cua url - cua snapshot [--filter interactive] - cua act '' - cua find "" - cua text - cua fill "" - cua press [key...] - cua click | cua click - cua tabs - cua screenshot [--out file|-] - - cua click "" - cua type "" "" - cua observe [""] - cua do "" - cua models [-p provider] - cua tools [-m model] - cua session start [name] | stop | list | show - -Subcommands above the blank line are model-free: they run directly against -the browser (no LLM, no model API key; only KERNEL_API_KEY). \`click \` -with exactly two integer arguments clicks those viewport coordinates without -a model, \`click e12\` / \`fill e12 ...\` target an element ref minted by -\`snapshot\` or \`find\`, and \`act '{...}'\` runs a verified dependent browser -plan using those refs; any other \`click\` argument is a natural-language -description resolved by the model. With \`-s \`, refs span invocations -(re-snapshot on a stale-ref error). Exit codes: 0 ok, 1 not_found, 2 error. - -Options: - -p, --print Run a single prompt and exit - -m, --model Model ref (default: ${DEFAULT_CUA_MODEL_REF}) - Accepts \`provider:model\` refs or bare ids that - match exactly one entry in \`cua models\`. - Recommended: - openai: openai:gpt-5.6-sol - anthropic: anthropic:claude-opus-5 - google: google:gemini-3.6-flash - xai: xai:grok-4.5 - moonshot: moonshotai:kimi-k3 - --thinking Thinking level: off | minimal | low | medium | high | xhigh | max - (default: low; applies to providers that support it) - --profile Kernel browser profile to load - --proxy Kernel proxy to route the browser through - (must already exist; never auto-created) - --profile-no-save-changes Do not persist changes back to the profile - --browser-timeout Browser inactivity timeout in seconds (default 300) - --max-steps Max turns for action subcommands (default 3) - --out Output file for screenshot subcommand - --filter Restrict \`cua snapshot\` to interactive elements - -o, --output Output format for --print: text (default) | jsonl - --jsonl-include-deltas Include assistant_text_delta events (default off) - --jsonl-include-images Include base64 screenshots (default off, only sizes) - --image-protocol

Force terminal image protocol: \`kitty\` | \`iterm2\` | \`none\` | \`auto\` - (Ghostty / WezTerm are auto-detected as \`kitty\`.) - Also via CUA_IMAGE_PROTOCOL env var. - -s, --session-name Reuse a named browser session (see \`cua session start\`) - -c, --continue Resume the most recent session for cwd (fresh browser) - -r, --resume Pick a previous session to resume from a list - --session Resume a specific session: path | partial id | latest - --session-dir

Override the sessions directory - --no-session Don't persist this session to disk - --skill Load an extra skill file or directory (repeatable). - Skills also load from ~/.agents/skills/, - /.agents/skills/, the pi agent dir - (~/.pi/agent/), and pi-installed packages. - -ns, --no-skills Disable skill discovery entirely - --debug-tui Enable TUI render diagnostics for manual repros - -v, --verbose Verbose progress output to stderr - -h, --help Show this help - -Environment: - KERNEL_API_KEY Kernel API key (required) - OPENAI_API_KEY OpenAI API key (required when -m openai:…) - ANTHROPIC_API_KEY Anthropic API key (required when -m anthropic:…) - GOOGLE_API_KEY Google API key (required when -m google:…) - GEMINI_API_KEY Alias for GOOGLE_API_KEY - XAI_API_KEY xAI API key (required when -m xai:…) - MOONSHOT_API_KEY Moonshot AI API key (required when -m moonshotai:…) - OPENROUTER_API_KEY OpenRouter API key (required when -m openrouter:…) - KERNEL_BASE_URL Override Kernel base URL - OPENAI_BASE_URL Override OpenAI base URL - ANTHROPIC_BASE_URL Override Anthropic base URL - GOOGLE_BASE_URL Override Google base URL - META_BASE_URL Override Meta Model API base URL - XAI_BASE_URL Override xAI API base URL - MOONSHOTAI_BASE_URL Override Moonshot AI base URL - XDG_DATA_HOME Sessions are stored under \$XDG_DATA_HOME/cua/sessions - (defaults to ~/.local/share/cua/sessions) - CUA_IMAGE_PROTOCOL Force inline image protocol (\`kitty\`|\`iterm2\`|\`none\`|\`auto\`) -`; - -interface CliFlags { - help: boolean; - print: boolean; - verbose: boolean; - profileSaveChanges: boolean; - continueLatest: boolean; - resumePicker: boolean; - noSession: boolean; - noSkills: boolean; - debugTui: boolean; - jsonlIncludeDeltas: boolean; - jsonlIncludeImages: boolean; - model?: string; - thinking?: string; - browserProfile?: string; - browserProxy?: string; - browserTimeout?: number; - maxSteps?: number; - out?: string; - output?: string; - filter?: string; - imageProtocol?: string; - namedSession?: string; - sessionRef?: string; - sessionDir?: string; - skillPaths: string[]; - positionals: string[]; -} - -function parseCliArgs(argv: string[]): CliFlags { - // Pre-process: expand `-ns` → `--no-skills` (multi-char short flag pi-coding-agent supports; - // node:util's parseArgs only allows single-char shorts). - const preprocessed = argv.map((arg) => (arg === "-ns" ? "--no-skills" : arg)); - - let parsed; - try { - parsed = parseArgs({ - args: preprocessed, - options: { - help: { type: "boolean", short: "h", default: false }, - print: { type: "boolean", short: "p", default: false }, - verbose: { type: "boolean", short: "v", default: false }, - model: { type: "string", short: "m" }, - thinking: { type: "string" }, - profile: { type: "string" }, - proxy: { type: "string" }, - "profile-no-save-changes": { type: "boolean", default: false }, - "browser-timeout": { type: "string" }, - "max-steps": { type: "string" }, - out: { type: "string" }, - filter: { type: "string" }, - "image-protocol": { type: "string" }, - "session-name": { type: "string", short: "s" }, - continue: { type: "boolean", short: "c", default: false }, - resume: { type: "boolean", short: "r", default: false }, - session: { type: "string" }, - "session-dir": { type: "string" }, - "no-session": { type: "boolean", default: false }, - skill: { type: "string", multiple: true, default: [] }, - "no-skills": { type: "boolean", default: false }, - "debug-tui": { type: "boolean", default: false }, - output: { type: "string", short: "o" }, - "jsonl-include-deltas": { type: "boolean", default: false }, - "jsonl-include-images": { type: "boolean", default: false }, - }, - allowPositionals: true, - strict: true, - }); - } catch (err) { - throw new Error(`invalid arguments: ${(err as Error).message}`); - } - - const browserTimeoutRaw = parsed.values["browser-timeout"]; - const browserTimeout = browserTimeoutRaw ? Number(browserTimeoutRaw) : undefined; - const maxStepsRaw = parsed.values["max-steps"]; - const maxSteps = maxStepsRaw ? Number(maxStepsRaw) : undefined; - const thinkingRaw = parsed.values.thinking as string | undefined; - if (thinkingRaw !== undefined) { - const allowed = new Set(["off", "none", "minimal", "low", "medium", "high", "xhigh", "max"]); - if (!allowed.has(thinkingRaw.trim().toLowerCase())) { - throw new Error( - `invalid --thinking value "${thinkingRaw}"; expected one of: off | minimal | low | medium | high | xhigh | max`, - ); - } - } - return { - help: !!parsed.values.help, - print: !!parsed.values.print, - verbose: !!parsed.values.verbose, - profileSaveChanges: !parsed.values["profile-no-save-changes"], - continueLatest: !!parsed.values.continue, - resumePicker: !!parsed.values.resume, - noSession: !!parsed.values["no-session"], - noSkills: !!parsed.values["no-skills"], - debugTui: !!parsed.values["debug-tui"], - model: parsed.values.model as string | undefined, - thinking: parsed.values.thinking as string | undefined, - browserProfile: parsed.values.profile as string | undefined, - browserProxy: parsed.values.proxy as string | undefined, - browserTimeout: Number.isFinite(browserTimeout) ? browserTimeout : undefined, - maxSteps: Number.isFinite(maxSteps) ? maxSteps : undefined, - out: parsed.values.out as string | undefined, - filter: parsed.values.filter as string | undefined, - imageProtocol: parsed.values["image-protocol"] as string | undefined, - namedSession: parsed.values["session-name"] as string | undefined, - sessionRef: parsed.values.session as string | undefined, - sessionDir: parsed.values["session-dir"] as string | undefined, - skillPaths: ((parsed.values.skill as string[] | undefined) ?? []).filter((p) => p && p.trim().length > 0), - output: parsed.values.output as string | undefined, - jsonlIncludeDeltas: !!parsed.values["jsonl-include-deltas"], - jsonlIncludeImages: !!parsed.values["jsonl-include-images"], - positionals: parsed.positionals, - }; -} - -function toHarnessFlags(flags: CliFlags): HarnessCliFlags { - return { - verbose: flags.verbose, - profileSaveChanges: flags.profileSaveChanges, - continueLatest: flags.continueLatest, - resumePicker: flags.resumePicker, - noSession: flags.noSession, - noSkills: flags.noSkills, - debugTui: flags.debugTui, - jsonlIncludeDeltas: flags.jsonlIncludeDeltas, - jsonlIncludeImages: flags.jsonlIncludeImages, - model: flags.model, - thinking: flags.thinking, - browserProfile: flags.browserProfile, - browserProxy: flags.browserProxy, - browserTimeout: flags.browserTimeout, - maxSteps: flags.maxSteps, - out: flags.out, - output: flags.output, - filter: flags.filter, - imageProtocol: flags.imageProtocol, - namedSession: flags.namedSession, - sessionRef: flags.sessionRef, - sessionDir: flags.sessionDir, - skillPaths: flags.skillPaths, - }; -} - -const MODEL_SUBCOMMANDS = new Set(["click", "type", "observe", "do"]); - -export async function main(argv: string[]): Promise { - if (argv[0] === "models") { - return await runModelsSubcommandHarness(argv.slice(1)); - } - - if (argv[0] === "tools") { - return await runToolsSubcommandHarness(argv.slice(1)); - } - - let flags: CliFlags; - try { - flags = parseCliArgs(argv); - } catch (err) { - stderr.write(`${(err as Error).message}\n\n${HELP}`); - return 2; - } - - if (flags.help) { - stdout.write(HELP); - return 0; - } - - const positionals = flags.positionals; - const first = positionals[0]; - - if (first === "session") { - try { - return await runSessionSubcommandHarness(positionals.slice(1), toHarnessFlags(flags)); - } catch (err) { - stderr.write(`session error: ${(err as Error).message}\n`); - return 2; - } - } - - const rest = positionals.slice(1); - - const deterministic = deterministicActionFor(first, rest); - if (deterministic) { - try { - return await runDeterministicCommand(deterministic, rest, toHarnessFlags(flags)); - } catch (err) { - stderr.write(`error: ${(err as Error).message}\n`); - return 2; - } - } - - if (first && MODEL_SUBCOMMANDS.has(first)) { - try { - return await runActionCommand(first as ModelActionType, rest, toHarnessFlags(flags)); - } catch (err) { - stderr.write(`error: ${(err as Error).message}\n`); - return 2; - } - } - - const prompt = positionals.join(" ").trim(); - - if (flags.print) { - if (!prompt) { - stderr.write("error: --print requires a prompt\n"); - return 2; - } - try { - return await runPrintCommand(prompt, toHarnessFlags(flags)); - } catch (err) { - stderr.write(`error: ${(err as Error).message}\n`); - return 1; - } - } - - try { - return await runInteractiveCommand(prompt, toHarnessFlags(flags)); - } catch (err) { - stderr.write(`error: ${(err as Error).message}\n`); - return 1; - } -} - -main(process.argv.slice(2)).then( - (code) => { - process.exit(code); - }, - (err) => { - stderr.write(`fatal: ${(err as Error).message}\n`); - process.exit(1); - }, -); diff --git a/packages/cli/src/harness-browser.ts b/packages/cli/src/harness-browser.ts deleted file mode 100644 index 8674457d..00000000 --- a/packages/cli/src/harness-browser.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { KernelBrowser } from "@onkernel/cua-agent"; -import Kernel, { NotFoundError } from "@onkernel/sdk"; - -/** Plain SDK-backed Kernel browser handle for the new harness wiring. */ -export interface CuaBrowserHandle { - client: Kernel; - browser: KernelBrowser; - /** Resolved Kernel profile id when --profile was used, otherwise undefined. */ - profileId?: string; - close(): Promise; -} - -export interface ProvisionBrowserOptions { - apiKey: string; - baseUrl?: string; - timeoutSeconds?: number; - /** Profile id or name. If a name is supplied that does not exist, it is created. */ - profileSelector?: string; - /** Explicit profile id (skips lookup). */ - profileId?: string; - /** Persist changes back to the profile when the session ends. Defaults to false. */ - saveChanges?: boolean; - /** Proxy id or name. Must already exist; never auto-created. */ - proxySelector?: string; -} - -const CUID2_LENGTH = 24; -const CUID2_PATTERN = /^[a-z][a-z0-9]{23}$/; - -function looksLikeProfileId(selector: string): boolean { - const trimmed = selector.trim(); - return trimmed.length === CUID2_LENGTH && CUID2_PATTERN.test(trimmed); -} - -/** - * Resolve a `--profile ` selector to a concrete profile id. - * Looks up by id first; if the API reports not-found and the selector does - * not look like a CUID2 id, the profile is created with that name. - */ -export async function resolveProfileId(client: Kernel, selector: string): Promise { - const trimmed = selector.trim(); - if (!trimmed) throw new Error("profile selector is empty"); - try { - const existing = await client.profiles.retrieve(trimmed); - return existing.id; - } catch (err) { - if (!(err instanceof NotFoundError)) { - throw new Error(`looking up browser profile "${trimmed}": ${(err as Error).message}`, { cause: err }); - } - if (looksLikeProfileId(trimmed)) { - throw new Error(`browser profile "${trimmed}" was not found`); - } - const created = await client.profiles.create({ name: trimmed }); - return created.id; - } -} - -/** - * Resolve a proxy selector (id or name) to a proxy id. Unlike profiles, - * proxies are never auto-created — creating one requires type/location/ - * credential configuration that does not fit a bare name. - */ -export async function resolveProxyId(client: Kernel, selector: string): Promise { - const trimmed = selector.trim(); - if (!trimmed) throw new Error("proxy selector is empty"); - try { - const existing = await client.proxies.retrieve(trimmed); - if (existing.id) return existing.id; - } catch (err) { - if (!(err instanceof NotFoundError)) { - throw new Error(`looking up proxy "${trimmed}": ${(err as Error).message}`, { cause: err }); - } - } - const proxies = await client.proxies.list(); - const matches = proxies.filter((proxy) => proxy.name === trimmed && proxy.id); - if (matches.length === 1) return matches[0]!.id!; - if (matches.length > 1) throw new Error(`proxy name "${trimmed}" is ambiguous (${matches.length} proxies); pass the proxy id`); - throw new Error(`proxy "${trimmed}" was not found; create one first (e.g. kernel proxies create)`); -} - -/** Create a Kernel SDK client with the supplied auth. */ -export function createKernelClient(apiKey: string, baseUrl?: string): Kernel { - return new Kernel({ apiKey, ...(baseUrl ? { baseURL: baseUrl } : {}) }); -} - -/** Provision a fresh Kernel cloud browser session and return a handle. */ -export async function provisionBrowser(opts: ProvisionBrowserOptions): Promise { - const client = createKernelClient(opts.apiKey, opts.baseUrl); - const timeoutSeconds = opts.timeoutSeconds && opts.timeoutSeconds > 0 ? opts.timeoutSeconds : 300; - - let profileId = (opts.profileId ?? "").trim(); - if (!profileId && opts.profileSelector && opts.profileSelector.trim()) { - profileId = await resolveProfileId(client, opts.profileSelector); - } - - const params: Parameters[0] = { - stealth: true, - timeout_seconds: timeoutSeconds, - }; - if (profileId) { - params.profile = { id: profileId, save_changes: opts.saveChanges ?? false }; - } - if (opts.proxySelector && opts.proxySelector.trim()) { - params.proxy_id = await resolveProxyId(client, opts.proxySelector); - } - - const browser = await client.browsers.create(params); - return { - client, - browser, - profileId: profileId || undefined, - async close(): Promise { - await client.browsers.deleteByID(browser.session_id); - }, - }; -} - -/** - * Capture a screenshot through the SDK. Falls back to undefined when the - * call fails — first-prompt images are best-effort. - */ -export async function captureScreenshot(client: Kernel, sessionId: string): Promise { - try { - const response = await client.browsers.computer.captureScreenshot(sessionId); - const arrayBuffer = await response.arrayBuffer(); - return Buffer.from(arrayBuffer); - } catch { - return undefined; - } -} diff --git a/packages/cli/src/harness-models.ts b/packages/cli/src/harness-models.ts deleted file mode 100644 index 28ecec21..00000000 --- a/packages/cli/src/harness-models.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { - type CuaModelInfo, - type CuaModelRef, - type CuaProvider, - formatCuaModelRef, - getCuaModel, - cuaProviders, - listCuaModels, - parseCuaModelRef, -} from "@onkernel/cua-ai"; - -/** Default model used by the CLI harness. */ -export const DEFAULT_CUA_MODEL_REF: CuaModelRef = "openai:gpt-5.6-sol"; - -/** - * Providers preferred when a bare model id is carried by several of them, in - * order. Gateways and aggregators resell the same ids as the provider that - * trained the model, so `-m gpt-5.5` should mean OpenAI's. - * - * This is a disambiguation preference for bare ids only. It never decides - * whether a model may run, and any provider is still reachable by passing a - * qualified `provider:model` ref. - */ -const BARE_ID_PROVIDER_PREFERENCE: readonly string[] = ["openai", "anthropic", "google", "xai", "moonshotai", "openrouter"]; - -/** - * Resolve a model ref from CLI input. Accepts a provider-qualified - * `provider:model` ref, or a bare model id when exactly one provider carries it - * or one of the preferred providers does. Throws when a bare id is unknown, or - * ambiguous among providers none of which is preferred. - */ -export function resolveCuaModelRef(input: string | undefined): CuaModelRef { - if (!input || !input.trim()) return DEFAULT_CUA_MODEL_REF; - const value = input.trim(); - if (value.includes(":")) { - const { provider, model } = parseCuaModelRef(value); - const ref = formatCuaModelRef(provider, model); - // Validate the ref resolves to a concrete model so failures surface early. - getCuaModel(ref); - return ref; - } - const matches = listCuaModels().filter((m) => m.model === value); - if (matches.length === 0) { - throw new Error(`unknown model "${value}" (run \`cua models\` to list supported -m/--model values)`); - } - if (matches.length > 1) { - const preferred = BARE_ID_PROVIDER_PREFERENCE.map((provider) => matches.find((m) => m.provider === provider)).find(Boolean); - if (preferred) return preferred.ref; - const refs = matches.map((m) => m.ref).join(", "); - throw new Error(`ambiguous model "${value}" (matches: ${refs}); pass a provider-qualified ref`); - } - return matches[0]!.ref; -} - -/** - * List selectable models, optionally filtered to a provider. Accepts any - * provider pi-ai carries, plus the CLI-friendly `"gemini"`/`"moonshot"` - * aliases. - */ -export function listSupportedModels(provider?: string): CuaModelInfo[] { - if (!provider) return listCuaModels(); - const normalized = provider === "gemini" ? "google" : provider === "moonshot" ? "moonshotai" : provider; - if (!cuaProviders().includes(normalized)) { - throw new Error(`unknown provider "${provider}" (pi-ai carries: ${cuaProviders().join(", ")})`); - } - return listCuaModels(normalized); -} diff --git a/packages/cli/src/harness-named-sessions.ts b/packages/cli/src/harness-named-sessions.ts deleted file mode 100644 index bf17e2e1..00000000 --- a/packages/cli/src/harness-named-sessions.ts +++ /dev/null @@ -1,307 +0,0 @@ -import type { BrowserRefState, KernelBrowser } from "@onkernel/cua-agent"; -import Kernel from "@onkernel/sdk"; -import { mkdir, readdir, readFile, stat, unlink, writeFile } from "node:fs/promises"; -import { homedir } from "node:os"; -import { join } from "node:path"; -import { createKernelClient, resolveProfileId, resolveProxyId } from "./harness-browser"; - -/** - * Named sessions: durable, slug-keyed pointers to a Kernel cloud browser - * session that can be reused across `cua` invocations (e.g. `cua -s login - * open ...` then `cua -s login click ...`). The metadata file lives under - * `$XDG_DATA_HOME/cua/named-sessions/.json`; the browser itself is - * server-side on Kernel. - */ - -export interface NamedSessionMetadata { - name: string; - kernel_session_id: string; - live_url?: string; - profile_id?: string; - proxy_id?: string; - transcript_path?: string; - /** Model ref last used with this session; chained invocations without -m default to it. */ - model?: string; - created_at: number; -} - -const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/; - -export function namedSessionsDir(): string { - const xdg = process.env.XDG_DATA_HOME; - if (xdg) return join(xdg, "cua", "named-sessions"); - return join(homedir(), ".local", "share", "cua", "named-sessions"); -} - -function sessionFilePath(name: string): string { - return join(namedSessionsDir(), `${name}.json`); -} - -export function validateSlug(name: string): void { - if (!SLUG_PATTERN.test(name)) { - throw new Error( - `invalid session name "${name}": must match ${SLUG_PATTERN} (lowercase a-z, 0-9, hyphens; 1-63 chars; cannot start with a hyphen)`, - ); - } -} - -async function fileExists(path: string): Promise { - try { - await stat(path); - return true; - } catch { - return false; - } -} - -export async function readNamedSession(name: string): Promise { - const path = sessionFilePath(name); - if (!(await fileExists(path))) return undefined; - const raw = await readFile(path, "utf8"); - return JSON.parse(raw) as NamedSessionMetadata; -} - -export async function writeNamedSession(meta: NamedSessionMetadata): Promise { - validateSlug(meta.name); - const path = sessionFilePath(meta.name); - await mkdir(namedSessionsDir(), { recursive: true }); - await writeFile(path, JSON.stringify(meta, null, 2) + "\n", { mode: 0o600 }); - return path; -} - -export async function deleteNamedSession(name: string): Promise { - const path = sessionFilePath(name); - await unlink(refsFilePath(name)).catch(() => {}); - if (!(await fileExists(path))) return false; - await unlink(path); - return true; -} - -function refsFilePath(name: string): string { - return join(namedSessionsDir(), `${name}.refs.json`); -} - -/** - * Element refs minted by one invocation (snapshot/find) survive to the next - * via this per-session file, so `cua -s x snapshot` then `cua -s x click e12` - * works across processes. Scoped to the named session's browser; stale state - * is caught by the executor's generation/self-heal machinery. - */ -export async function readNamedSessionRefs(name: string): Promise { - try { - return JSON.parse(await readFile(refsFilePath(name), "utf8")) as BrowserRefState; - } catch { - return undefined; - } -} - -export async function writeNamedSessionRefs(name: string, state: BrowserRefState): Promise { - await mkdir(namedSessionsDir(), { recursive: true }); - await writeFile(refsFilePath(name), JSON.stringify(state) + "\n", { mode: 0o600 }); -} - -export async function listNamedSessions(): Promise { - const dir = namedSessionsDir(); - if (!(await fileExists(dir))) return []; - const entries = await readdir(dir); - const out: NamedSessionMetadata[] = []; - for (const entry of entries) { - if (!entry.endsWith(".json") || entry.endsWith(".refs.json")) continue; - try { - const raw = await readFile(join(dir, entry), "utf8"); - const meta = JSON.parse(raw) as NamedSessionMetadata; - if (typeof meta.name !== "string" || typeof meta.kernel_session_id !== "string" || typeof meta.created_at !== "number") continue; - out.push(meta); - } catch { - // skip unreadable / malformed entries - } - } - out.sort((a, b) => b.created_at - a.created_at); - return out; -} - -export interface StartNamedSessionOptions { - name: string; - apiKey: string; - baseUrl?: string; - browserTimeoutSeconds?: number; - /** Profile id or name (created if missing). Same semantics as `--profile`. */ - profileSelector?: string; - saveProfileChanges?: boolean; - /** Proxy id or name (must already exist). Same semantics as `--proxy`. */ - proxySelector?: string; - /** Canonical model ref to seed the session with (same semantics as `-m`). */ - model?: string; -} - -export interface StartNamedSessionResult { - meta: NamedSessionMetadata; - metadataPath: string; - client: Kernel; - browser: KernelBrowser; -} - -/** Provision a fresh Kernel browser and persist a named-session metadata file. */ -export async function startNamedSession(opts: StartNamedSessionOptions): Promise { - validateSlug(opts.name); - const existing = await readNamedSession(opts.name); - if (existing) { - throw new Error( - `named session "${opts.name}" already exists (kernel_session_id=${existing.kernel_session_id}). Run \`cua session stop ${opts.name}\` first.`, - ); - } - - const client = createKernelClient(opts.apiKey, opts.baseUrl); - const timeoutSeconds = opts.browserTimeoutSeconds && opts.browserTimeoutSeconds > 0 ? opts.browserTimeoutSeconds : 300; - let profileId: string | undefined; - if (opts.profileSelector && opts.profileSelector.trim()) { - profileId = await resolveProfileId(client, opts.profileSelector); - } - const params: Parameters[0] = { - stealth: true, - timeout_seconds: timeoutSeconds, - }; - if (profileId) { - params.profile = { id: profileId, save_changes: opts.saveProfileChanges ?? false }; - } - let proxyId: string | undefined; - if (opts.proxySelector && opts.proxySelector.trim()) { - proxyId = await resolveProxyId(client, opts.proxySelector); - params.proxy_id = proxyId; - } - const browser = await client.browsers.create(params); - - const meta: NamedSessionMetadata = { - name: opts.name, - kernel_session_id: browser.session_id, - live_url: browser.browser_live_view_url, - profile_id: profileId, - proxy_id: proxyId, - model: opts.model, - created_at: Date.now(), - }; - const metadataPath = await writeNamedSession(meta); - return { meta, metadataPath, client, browser }; -} - -export interface AttachNamedSessionOptions { - name: string; - apiKey: string; - baseUrl?: string; -} - -export interface AttachNamedSessionResult { - meta: NamedSessionMetadata; - client: Kernel; - browser: KernelBrowser; -} - -/** - * Attach to a previously-started named session. Performs a liveness check - * via `client.browsers.retrieve` so the caller can fail fast when the - * server-side session has timed out or been deleted. - */ -export async function attachNamedSession(opts: AttachNamedSessionOptions): Promise { - const meta = await readNamedSession(opts.name); - if (!meta) { - throw new Error( - `unknown named session "${opts.name}". Run \`cua session list\` to see available sessions, or \`cua session start ${opts.name}\` to create one.`, - ); - } - const client = createKernelClient(opts.apiKey, opts.baseUrl); - let browser: KernelBrowser; - try { - browser = await client.browsers.retrieve(meta.kernel_session_id); - } catch (err) { - const status = (err as { status?: unknown }).status; - if (status === 404) { - throw new Error( - `named session "${opts.name}" is no longer alive on Kernel (browser timed out or was deleted). Run \`cua session stop ${opts.name} && cua session start ${opts.name}\` to provision a fresh one.`, - ); - } - throw new Error(`liveness check for named session "${opts.name}" failed: ${(err as Error).message}`, { cause: err }); - } - const deletedAt = (browser as { deleted_at?: unknown }).deleted_at; - if (deletedAt) { - throw new Error( - `named session "${opts.name}" is no longer alive on Kernel (browser timed out or was deleted). Run \`cua session stop ${opts.name} && cua session start ${opts.name}\` to provision a fresh one.`, - ); - } - return { meta, client, browser }; -} - -export interface StopNamedSessionOptions { - name: string; - apiKey: string; - baseUrl?: string; -} - -export interface StopNamedSessionResult { - existed: boolean; - kernelDeleted: boolean; -} - -/** Tear down a named session: delete the Kernel browser and remove the metadata file. */ -export async function stopNamedSession(opts: StopNamedSessionOptions): Promise { - const meta = await readNamedSession(opts.name); - if (!meta) return { existed: false, kernelDeleted: false }; - const client = createKernelClient(opts.apiKey, opts.baseUrl); - let kernelDeleted = false; - try { - await client.browsers.deleteByID(meta.kernel_session_id); - kernelDeleted = true; - } catch (err) { - const status = (err as { status?: unknown }).status; - if (status !== 404) { - throw new Error( - `failed to delete Kernel browser ${meta.kernel_session_id} for named session "${opts.name}": ${(err as Error).message}`, - { cause: err }, - ); - } - } - await deleteNamedSession(opts.name); - return { existed: true, kernelDeleted }; -} - -/** Update the persisted `transcript_path` on a named session. */ -export async function recordTranscriptPath(name: string, transcriptPath: string): Promise { - const meta = await readNamedSession(name); - if (!meta) return; - if (meta.transcript_path === transcriptPath) return; - meta.transcript_path = transcriptPath; - await writeNamedSession(meta); -} - -/** Persist the model used with a named session so chained invocations reuse it. */ -export async function recordSessionModel(name: string, runtime: { model: string }): Promise { - const meta = await readNamedSession(name); - if (!meta || meta.model === runtime.model) return; - meta.model = runtime.model; - await writeNamedSession(meta); -} - -/** Patch the named session model after a TUI /model switch. */ -export async function updateNamedSessionRuntime(name: string, patch: { model?: string }): Promise { - const meta = await readNamedSession(name); - if (!meta) return; - const model = patch.model ?? meta.model; - if (meta.model === model) return; - meta.model = model; - await writeNamedSession(meta); -} - -export function shortKernelId(id: string): string { - return id.length > 10 ? `${id.slice(0, 8)}…` : id; -} - -export function formatRelativeAge(createdAt: number): string { - const diff = Date.now() - createdAt; - const sec = Math.max(0, Math.floor(diff / 1000)); - if (sec < 60) return `${sec}s`; - const min = Math.floor(sec / 60); - if (min < 60) return `${min}m`; - const hr = Math.floor(min / 60); - if (hr < 24) return `${hr}h`; - const d = Math.floor(hr / 24); - return `${d}d`; -} diff --git a/packages/cli/src/harness-sessions.ts b/packages/cli/src/harness-sessions.ts deleted file mode 100644 index d326a860..00000000 --- a/packages/cli/src/harness-sessions.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { - type JsonlSessionMetadata, - JsonlSessionRepo, - NodeExecutionEnv, - type Session, -} from "@onkernel/cua-agent"; -import { readFile, stat } from "node:fs/promises"; -import { homedir } from "node:os"; -import { isAbsolute, resolve as resolvePath, join } from "node:path"; - -/** - * Resolve the default sessions directory: `$XDG_DATA_HOME/cua/sessions` - * (or `~/.local/share/cua/sessions`). - */ -export function defaultSessionsRoot(): string { - const xdg = process.env.XDG_DATA_HOME; - if (xdg) return join(xdg, "cua", "sessions"); - return join(homedir(), ".local", "share", "cua", "sessions"); -} - -/** Build a `JsonlSessionRepo` rooted at the resolved sessions directory. */ -export function createSessionRepo(sessionsRoot?: string): JsonlSessionRepo { - const root = sessionsRoot ?? defaultSessionsRoot(); - return new JsonlSessionRepo({ - fs: new NodeExecutionEnv({ cwd: process.cwd() }), - sessionsRoot: root, - }); -} - -export interface SessionInfo { - metadata: JsonlSessionMetadata; - mtimeMs?: number; -} - -/** List sessions for a cwd; legacy / malformed files are skipped. */ -export async function listSessionsForCwd( - repo: JsonlSessionRepo, - cwd: string, -): Promise { - const all = await repo.list({ cwd }); - return all; -} - -/** - * Find the most recent session metadata for cwd. The pi `JsonlSessionRepo` - * already orders by `createdAt` descending, but legacy `-c` semantics - * resumed by last *modified* time so a session that was reopened and - * appended to comes back first. We stat each file and prefer the newest - * mtime; results that fail to stat fall back to `createdAt`. - */ -export async function findLatestSession( - repo: JsonlSessionRepo, - cwd: string, -): Promise { - const sessions = await listSessionsForCwd(repo, cwd); - if (sessions.length === 0) return undefined; - const ranked = await Promise.all( - sessions.map(async (meta) => { - try { - const s = await stat(meta.path); - return { meta, mtime: s.mtimeMs }; - } catch { - return { meta, mtime: Number.NaN }; - } - }), - ); - ranked.sort((a, b) => { - const am = Number.isFinite(a.mtime) ? a.mtime : -Infinity; - const bm = Number.isFinite(b.mtime) ? b.mtime : -Infinity; - if (am !== bm) return bm - am; - return b.meta.createdAt.localeCompare(a.meta.createdAt); - }); - return ranked[0]?.meta; -} - -/** - * Resolve a `--session ` argument. Accepts: - * - an absolute or relative path to an existing session file - * - `latest` for the most recent session for cwd - * - any other string as a prefix matched against session ids - */ -export async function resolveSessionRef( - repo: JsonlSessionRepo, - cwd: string, - ref: string, -): Promise { - const trimmed = ref.trim(); - if (!trimmed) throw new Error("session reference is empty"); - if (trimmed.includes("/") || trimmed.endsWith(".jsonl")) { - const absolute = isAbsolute(trimmed) ? trimmed : resolvePath(cwd, trimmed); - const direct = await readMetadataFromFile(absolute); - if (direct) return direct; - // Best-effort scan of the repo (no cwd filter) in case the path was - // re-encoded somewhere (e.g. symlinks) and only matches a known session. - const sessions = await repo.list(); - const match = sessions.find((m) => m.path === absolute); - if (match) return match; - throw new Error(`no session at "${trimmed}"`); - } - if (trimmed === "latest") { - const latest = await findLatestSession(repo, cwd); - if (!latest) throw new Error("no sessions found"); - return latest; - } - const sessions = await listSessionsForCwd(repo, cwd); - const matches = sessions.filter((s) => s.id.startsWith(trimmed)); - if (matches.length === 0) throw new Error(`no session matches "${trimmed}"`); - if (matches.length > 1) throw new Error(`ambiguous session prefix "${trimmed}" (${matches.length} matches)`); - return matches[0]!; -} - -/** - * Load the header line of a jsonl session file from disk and return its - * metadata, or undefined when the file is missing/empty/legacy. Used to - * resolve `--session ` and named transcript_path entries that may - * have been created from a different cwd (so the repo's per-cwd listing - * wouldn't see them). - */ -export async function readMetadataFromFile( - absolutePath: string, -): Promise { - try { - const raw = await readFile(absolutePath, "utf8"); - const firstLine = raw.split("\n", 1)[0]?.trim(); - if (!firstLine) return undefined; - const header = JSON.parse(firstLine) as { - type?: string; - version?: unknown; - id?: unknown; - timestamp?: unknown; - cwd?: unknown; - parentSession?: unknown; - }; - if (header.type !== "session") return undefined; - if (typeof header.id !== "string" || typeof header.timestamp !== "string" || typeof header.cwd !== "string") { - return undefined; - } - return { - id: header.id, - createdAt: header.timestamp, - cwd: header.cwd, - path: absolutePath, - ...(typeof header.parentSession === "string" ? { parentSessionPath: header.parentSession } : {}), - }; - } catch { - return undefined; - } -} - -/** Open (resume) a session by metadata. */ -export function openSession(repo: JsonlSessionRepo, metadata: JsonlSessionMetadata): Promise> { - return repo.open(metadata); -} - -/** Create a brand-new session for cwd. */ -export function createSession(repo: JsonlSessionRepo, cwd: string): Promise> { - return repo.create({ cwd }); -} - -/** Custom entry type used to record the Kernel browser the session ran against. */ -export const CUA_BROWSER_ENTRY = "cua-browser"; - -export interface CuaBrowserEntryData { - sessionId: string; - liveUrl?: string; - profileId?: string; - createdAt: number; -} - -/** Append a browser-metadata custom entry to the session. */ -export async function appendBrowserEntry( - session: Session, - data: CuaBrowserEntryData, -): Promise { - try { - await session.appendCustomEntry(CUA_BROWSER_ENTRY, data); - } catch { - // best-effort; never block a run on bookkeeping - } -} diff --git a/packages/cli/src/harness-skills.ts b/packages/cli/src/harness-skills.ts deleted file mode 100644 index 531681ce..00000000 --- a/packages/cli/src/harness-skills.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { type ExecutionEnv, loadSkills, type Skill, type SkillDiagnostic } from "@onkernel/cua-agent"; -import { - DefaultResourceLoader, - getAgentDir, - SettingsManager, -} from "@earendil-works/pi-coding-agent"; -import { dirname, join } from "node:path"; - -export interface DiscoverSkillsOptions { - cwd: string; - env: ExecutionEnv; - /** Extra explicit skill paths (files or directories) from `--skill` flags. */ - extraPaths?: string[]; - /** Disable all skill discovery. */ - disabled?: boolean; - /** pi agent dir to resolve installed packages from. Defaults to `getAgentDir()`. */ - agentDir?: string; -} - -export interface ContextFile { - path: string; - content: string; -} - -export interface DiscoverSkillsResult { - skills: Skill[]; - contextFiles: ContextFile[]; - diagnostics: SkillDiagnostic[]; -} - -/** - * Discover skills and context files via pi's `DefaultResourceLoader`, the same - * loader pi's own TUI uses. This resolves skills from installed pi packages - * (`pi install …` writes them under the agent dir and records them in - * settings.json) in addition to `~/.agents/skills/`, `/.agents/skills/`, - * `~/.pi/agent/skills/`, and explicit `--skill` paths. - * - * Startup never blocks on an interactive prompt: project settings start - * untrusted (no trust prompt), and `PI_OFFLINE` keeps a configured-but-not- - * installed package from triggering a network install — it is skipped instead. - * - * pi extensions are not loaded (`noExtensions`): cua's harness drives the - * lower-level `AgentHarness` directly and cannot bind pi `AgentSession` - * extensions. - */ -export async function discoverCuaSkills(opts: DiscoverSkillsOptions): Promise { - const extras = (opts.extraPaths ?? []).filter((p) => p && p.trim().length > 0); - const agentDir = opts.agentDir ?? getAgentDir(); - const settingsManager = SettingsManager.create(opts.cwd, agentDir, { projectTrusted: false }); - // Project-local `/.agents/skills` is loaded explicitly rather than via - // pi's trusted project scan. That scan only runs when the project is trusted - // (which would mean prompting the user and binding untrusted `.pi/` - // extensions); `additionalSkillPaths` loads the directory unconditionally and - // never binds extensions, so project skills work without a trust prompt. - const projectSkillDir = join(opts.cwd, ".agents", "skills"); - const additionalSkillPaths = [...extras, projectSkillDir]; - const loader = new DefaultResourceLoader({ - cwd: opts.cwd, - agentDir, - settingsManager, - additionalSkillPaths, - noSkills: opts.disabled === true, - noExtensions: true, - noPromptTemplates: true, - noThemes: true, - }); - - const restoreOffline = forceOfflinePackageResolution(); - try { - await loader.reload(); - } finally { - restoreOffline(); - } - - const piSkills = loader.getSkills().skills; - const contextFiles = loader.getAgentsFiles().agentsFiles; - - // pi's loader resolves the skill *file paths* — the superset that includes - // package skills — but its skill objects don't carry the file body. cua's - // harness needs the full instructions, so re-read the discovered skills - // through cua-agent's `loadSkills`, which produces the `{ content }` shape - // the harness and `/skill:` expansion consume. Scan each skill's root - // directory, then keep only the files pi actually enumerated (so a skills - // root holding both a loose `.md` and a nested `SKILL.md` doesn't load the - // nested skill twice). - const discoveredPaths = new Set(piSkills.map((s) => s.filePath)); - const skillDirs = [...new Set(piSkills.map((s) => dirname(s.filePath)))]; - if (skillDirs.length === 0) { - return { skills: [], contextFiles, diagnostics: [] }; - } - const loaded = await loadSkills(opts.env, skillDirs); - const skills = dedupeByFilePath(loaded.skills.filter((s) => discoveredPaths.has(s.filePath))); - return { skills, contextFiles, diagnostics: loaded.diagnostics }; -} - -function dedupeByFilePath(skills: Skill[]): Skill[] { - const seen = new Set(); - const result: Skill[] = []; - for (const skill of skills) { - if (seen.has(skill.filePath)) continue; - seen.add(skill.filePath); - result.push(skill); - } - return result; -} - -/** - * `DefaultResourceLoader.reload()` resolves packages without an `onMissing` - * callback, which would auto-install a configured-but-missing package over the - * network. `PI_OFFLINE` makes that resolution skip missing packages instead, so - * startup can never hang on an install. Restores any prior value afterward. - */ -function forceOfflinePackageResolution(): () => void { - const previous = process.env.PI_OFFLINE; - if (previous !== undefined) return () => {}; - process.env.PI_OFFLINE = "1"; - return () => { - delete process.env.PI_OFFLINE; - }; -} - -/** - * Resolve a `/skill:` invocation. Returns the matched skill (so the - * caller can use `harness.skill(name)`) plus any remainder text the user - * typed after the skill name, which the caller can append as an additional - * instruction. - */ -export function parseSkillInvocation( - text: string, - skills: Skill[], -): { skill?: Skill; remainder: string } | undefined { - const trimmed = text.trim(); - const match = trimmed.match(/^\/skill:([A-Za-z0-9_\-.]+)\s*(.*)$/); - if (!match) return undefined; - const [, name, rest] = match; - const skill = skills.find((s) => s.name === name); - return { skill, remainder: (rest ?? "").trim() }; -} diff --git a/packages/cli/src/harness.ts b/packages/cli/src/harness.ts deleted file mode 100644 index 78e4a3cc..00000000 --- a/packages/cli/src/harness.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { - AgentHarness, - attach, - type CuaAttachOptions, - type CuaBrowserHandle, - type CuaHarnessTool, - type CuaModelInput, - formatSkillsForSystemPrompt, - type KernelBrowser, - type Session, - type Skill, - type ThinkingLevel, -} from "@onkernel/cua-agent"; -import { - type Api, - cua, - cuaModelCapabilities, - cuaNativeSurfaces, - type CuaModelRef, - getCuaModel, - type Model, - type Models, - parseCuaModelRef, -} from "@onkernel/cua-ai"; -import type Kernel from "@onkernel/sdk"; -import { - type AgentHarnessTool, - createBashTool, - createEditTool, - createReadTool, - createWriteTool, - type ExecutionToolContext, - type PromptTemplate, -} from "@earendil-works/pi-agent-core"; -import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node"; -import type { ContextFile } from "./harness-skills"; - -/** One tool in the CLI harness's caller-owned list. */ -export type CuaCliTool = CuaHarnessTool; - -/** - * The CLI's harness: stock pi, with no CUA class wrapping it. Its tool type is - * pi's own — {@link CuaCliTool} is the caller-owned input the catalog compiles - * from, and a CUA spec is not executable until the handle materializes it. - */ -export type CuaCliHarness = AgentHarness>; - -/** - * The live (model, tools) selection, and the compile-then-swap that changes it. - * - * `attach()` hands back an immutable compiled pair, so the current selection - * lives with whoever can change it — here, `/model` and `/tools`. Both steps - * fail safe: `compile()` throws before anything mutates, and `apply()` restores - * the previous pair if pi rejects the new one, so a rejected selection leaves - * the session exactly as it was. - */ -export class CuaCliCatalog { - private selection: CuaModelInput; - private requested: readonly CuaCliTool[]; - - constructor( - private readonly handle: CuaBrowserHandle, - private readonly harness: CuaCliHarness, - selection: CuaModelInput, - requested: readonly CuaCliTool[], - ) { - this.selection = selection; - this.requested = [...requested]; - } - - /** The caller-owned tool list, as selected — CUA specs, not materialized pi tools. */ - getTools(): readonly CuaCliTool[] { - return [...this.requested]; - } - - setTools(tools: readonly CuaCliTool[]): Promise { - return this.swap(this.selection, tools); - } - - setModel(model: CuaModelInput): Promise { - return this.swap(model, this.requested); - } - - /** - * Select a model and its tool list in one compile. Staging the two in - * sequence would compile an intermediate catalog whose derived transport - * differs from both the old and the new one. - */ - setModelAndTools(model: CuaModelInput, tools: readonly CuaCliTool[]): Promise { - return this.swap(model, tools); - } - - private async swap(model: CuaModelInput, tools: readonly CuaCliTool[]): Promise { - const compiled = this.handle.compile({ model, tools }); - await compiled.apply(this.harness); - this.selection = model; - this.requested = [...tools]; - } -} - -/** A CLI session: stock pi driving the agent, a CUA handle owning the browser. */ -export interface CuaCliSession { - readonly harness: CuaCliHarness; - readonly catalog: CuaCliCatalog; -} - -export interface BuildCuaHarnessOptions { - cwd: string; - client: Kernel; - browser: KernelBrowser; - session: Session; - model: CuaModelRef; - skills?: Skill[]; - contextFiles?: ContextFile[]; - thinkingLevel?: ThinkingLevel; - /** Override the CLI's explicit interaction + coding tool list. */ - tools?: CuaCliTool[]; - models?: Models; - toolResultImageReplayLimit?: CuaAttachOptions["toolResultImageReplayLimit"]; - responseThreading?: CuaAttachOptions["responseThreading"]; - retry?: CuaAttachOptions["retry"]; - modelBaseUrl?: string; -} - -/** Build the CLI session with one explicit tool list and a caller-owned prompt. */ -export function buildCuaHarness(opts: BuildCuaHarnessOptions): CuaCliSession { - const skills = opts.skills ?? []; - const contextFiles = opts.contextFiles ?? []; - const model: CuaModelRef | Model = opts.modelBaseUrl - ? { ...getCuaModel(opts.model), baseUrl: opts.modelBaseUrl } - : opts.model; - const tools = opts.tools ?? [ - ...defaultInteractionTools(opts.model), - ...defaultApplicationTools(), - ]; - const handle = attach({ - browser: opts.browser, - client: opts.client, - models: opts.models, - toolResultImageReplayLimit: opts.toolResultImageReplayLimit, - responseThreading: opts.responseThreading, - retry: opts.retry, - }); - const compiled = handle.compile({ model, tools }); - const harness: CuaCliHarness = new AgentHarness({ - session: opts.session, - model: compiled.model, - models: compiled.models, - tools: [...compiled.tools], - activeToolNames: compiled.tools.map((tool) => tool.name), - toolContext: { env: new NodeExecutionEnv({ cwd: opts.cwd }) }, - resources: { skills }, - thinkingLevel: opts.thinkingLevel, - systemPrompt: ({ resources }) => composeSystemPrompt(resources.skills ?? [], contextFiles), - }); - compiled.activate(harness); - return { harness, catalog: new CuaCliCatalog(handle, harness, model, tools) }; -} - -/** Coding tools owned by the CLI application rather than inferred from a compiled catalog. */ -export function defaultApplicationTools(): CuaCliTool[] { - return [createReadTool(), createBashTool(), createEditTool(), createWriteTool()]; -} - -/** - * CLI structured-browser policy. `browser_act` remains outside the reusable - * base toolset, so the application opts into semantic verified plans explicitly. - */ -function structuredBrowserTools(): CuaCliTool[] { - return [...cua.toolsets.browser(), cua.tools.browser.act()]; -} - -/** - * CLI interaction policy, asked of the model rather than switched on its - * provider: a model with a provider-native browser surface gets that surface, - * and everything else gets CUA's CDP browser tools, with `browser_act` included - * only where the model accepts its schema. - * - * OpenAI's native computer tool is deliberately not a default: it is a distinct - * interaction style callers opt into through `--tools` or the `/tools` picker. - */ -export function defaultInteractionTools(model: CuaModelRef): CuaCliTool[] { - const { provider } = parseCuaModelRef(model); - const resolved = getCuaModel(model); - if (cuaNativeSurfaces(resolved).includes("browser")) { - if (provider === "anthropic") { - return [cua.providers.anthropic.tools.browser({ version: "20260701", javascript: true })]; - } - if (provider === "google") return cua.providers.google.toolsets.browser(); - } - return cuaModelCapabilities(resolved).acceptsLargeSchemas - ? structuredBrowserTools() - : cua.toolsets.browser(); -} - -function composeSystemPrompt(skills: Skill[], contextFiles: ContextFile[]): string { - const sections: string[] = []; - const skillBlock = formatSkillsForSystemPrompt(skills).trim(); - if (skillBlock) sections.push(skillBlock); - const contextBlock = formatContextFiles(contextFiles); - if (contextBlock) sections.push(contextBlock); - return sections.length ? `${sections.join("\n\n")}\n` : ""; -} - -function formatContextFiles(contextFiles: ContextFile[]): string { - const blocks = contextFiles - .filter((file) => file.content.trim().length > 0) - .map((file) => `## ${file.path}\n\n${file.content.trim()}`); - if (blocks.length === 0) return ""; - return `# Context\n\n${blocks.join("\n\n")}`; -} diff --git a/packages/cli/src/output/harness-jsonl.ts b/packages/cli/src/output/harness-jsonl.ts deleted file mode 100644 index f4ab623c..00000000 --- a/packages/cli/src/output/harness-jsonl.ts +++ /dev/null @@ -1,193 +0,0 @@ -import type { - AgentHarnessEvent, - KernelBrowser, -} from "@onkernel/cua-agent"; -import type { Usage } from "@onkernel/cua-ai"; -import type { CuaCliHarness } from "../harness"; - -/** - * Schema version stamped on every `session_created` event. Bump when the - * jsonl shape changes in a way external consumers need to detect. - */ -export const CUA_JSONL_SCHEMA_VERSION = 2; - -export interface JsonlSinkOptions { - harness: CuaCliHarness; - browser: KernelBrowser; - modelRef: string; - provider: string; - /** Kernel profile id used to provision the browser, when --profile was set. */ - profileId?: string; - /** Where to write each line. Defaults to process.stdout. */ - write?: (line: string) => void; - /** When true, emit `assistant_text_delta` events. Default: false. */ - includeDeltas?: boolean; - /** When true, include base64 screenshot bytes in `tool_result` events. Default: false. */ - includeImages?: boolean; -} - -interface JsonlEventBase { - type: string; - ts: number; -} - -/** - * Subscribe to a harness and emit one JSON object per line for downstream - * tooling. The event schema mirrors the legacy `output/jsonl.ts`: only the - * source of each field changes. - */ -export function attachHarnessJsonlSink(opts: JsonlSinkOptions): () => void { - const write = opts.write ?? ((line: string) => process.stdout.write(line + "\n")); - const emit = (obj: JsonlEventBase & Record): void => { - try { - write(JSON.stringify(obj)); - } catch { - write( - JSON.stringify({ - type: "error", - code: "serialize_failed", - message: "could not serialize event", - ts: Date.now(), - }), - ); - } - }; - - emit({ - type: "session_created", - schema_version: CUA_JSONL_SCHEMA_VERSION, - model: opts.modelRef, - provider: opts.provider, - ts: Date.now(), - }); - emit({ - type: "browser_created", - browser_session_id: opts.browser.session_id, - live_url: opts.browser.browser_live_view_url, - ...(opts.profileId ? { profile_id: opts.profileId } : {}), - ts: Date.now(), - }); - - let turn = 0; - const includeDeltas = opts.includeDeltas === true; - const includeImages = opts.includeImages === true; - - return opts.harness.subscribe((event: AgentHarnessEvent) => { - switch (event.type) { - case "turn_start": - turn += 1; - return; - case "turn_end": - emit({ type: "turn_done", turn, ts: Date.now() }); - return; - case "agent_end": - emit({ type: "run_complete", turns: turn, ts: Date.now() }); - return; - case "message_end": { - const msg = event.message; - if (msg.role === "user") { - const text = textOf(msg.content); - emit({ type: "user_message", text, ts: Date.now() }); - } else if (msg.role === "assistant") { - const text = textOf(msg.content); - if (text) emit({ type: "assistant_text_done", text, ts: Date.now() }); - emit({ type: "assistant_usage", turn, model: msg.model, api: msg.api, ...usageFields(msg.usage), ts: Date.now() }); - } - return; - } - case "message_update": { - if (!includeDeltas) return; - if (event.assistantMessageEvent.type === "text_delta") { - emit({ - type: "assistant_text_delta", - delta: event.assistantMessageEvent.delta, - ts: Date.now(), - }); - } - return; - } - case "tool_execution_start": - emit({ - type: "tool_call", - tool_name: event.toolName, - call_id: event.toolCallId, - args: event.args, - ts: Date.now(), - }); - return; - case "tool_execution_end": { - const result = event.result as - | { - content?: Array<{ type?: string; text?: string; data?: string; mimeType?: string }>; - details?: unknown; - } - | undefined; - const ok = !event.isError; - let contentText: string | undefined; - let screenshotBytes: number | undefined; - const screenshotsB64: string[] = []; - if (result?.content) { - const textParts: string[] = []; - for (const c of result.content) { - if (c?.type === "text" && typeof c.text === "string") textParts.push(c.text); - if (c?.type === "image" && typeof c.data === "string") { - const len = c.data.length; - screenshotBytes = (screenshotBytes ?? 0) + len; - if (includeImages) screenshotsB64.push(c.data); - } - } - contentText = textParts.join("\n").trim() || undefined; - } - emit({ - type: "tool_result", - tool_name: event.toolName, - call_id: event.toolCallId, - ok, - content_text: contentText, - screenshot_bytes: screenshotBytes, - ...(includeImages && screenshotsB64.length ? { screenshots_b64: screenshotsB64 } : {}), - details: result?.details, - ts: Date.now(), - }); - return; - } - default: - return; - } - }); -} - -/** - * OpenAI's `input_tokens` (and pi-ai's `Usage.input`) already excludes cached - * and cache-write tokens, so the billed prompt is `input + cacheRead + - * cacheWrite` and the cache hit ratio is `cacheRead` over that total. - */ -function usageFields(usage: Usage): Record { - const billedPrompt = usage.input + usage.cacheRead + usage.cacheWrite; - return { - input: usage.input, - output: usage.output, - cache_read: usage.cacheRead, - cache_write: usage.cacheWrite, - reasoning: usage.reasoning, - total_tokens: usage.totalTokens, - cache_hit_ratio: billedPrompt > 0 ? usage.cacheRead / billedPrompt : 0, - }; -} - -function textOf(content: unknown): string { - if (typeof content === "string") return content; - if (!Array.isArray(content)) return ""; - const parts: string[] = []; - for (const c of content) { - if ( - c && - typeof c === "object" && - (c as { type?: unknown }).type === "text" && - typeof (c as { text?: unknown }).text === "string" - ) { - parts.push((c as { text: string }).text); - } - } - return parts.join("\n"); -} diff --git a/packages/cli/src/print.ts b/packages/cli/src/print.ts deleted file mode 100644 index e25a973b..00000000 --- a/packages/cli/src/print.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { AgentHarnessEvent, Skill } from "@onkernel/cua-agent"; -import type { CuaCliHarness } from "./harness"; -import { stderr, stdout } from "node:process"; -import type { CuaBrowserHandle } from "./harness-browser"; -import { attachHarnessJsonlSink } from "./output/harness-jsonl"; -import { parseSkillInvocation } from "./harness-skills"; - -export interface RunPrintOptions { - harness: CuaCliHarness; - browserHandle: CuaBrowserHandle; - modelRef: string; - provider: string; - prompt: string; - skills?: Skill[]; - verbose?: boolean; - jsonlMode?: boolean; - jsonlIncludeDeltas?: boolean; - jsonlIncludeImages?: boolean; -} - -/** - * Run a single prompt through the harness and stream output to stdout - * (text mode) or as jsonl events. Returns the process exit code (0 ok, - * 1 on failure). - */ -export async function runPrint(opts: RunPrintOptions): Promise { - const jsonlMode = opts.jsonlMode === true; - let unsubscribeJsonl: (() => void) | undefined; - if (jsonlMode) { - unsubscribeJsonl = attachHarnessJsonlSink({ - harness: opts.harness, - browser: opts.browserHandle.browser, - profileId: opts.browserHandle.profileId, - modelRef: opts.modelRef, - provider: opts.provider, - includeDeltas: opts.jsonlIncludeDeltas, - includeImages: opts.jsonlIncludeImages, - }); - } - - const unsubscribeText = opts.harness.subscribe((event: AgentHarnessEvent) => { - if (jsonlMode) return; - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { - stdout.write(event.assistantMessageEvent.delta); - return; - } - if (opts.verbose && event.type === "tool_execution_start") { - stderr.write(`\n[cua] tool ${event.toolName} ${JSON.stringify(event.args)}\n`); - } - if (opts.verbose && event.type === "tool_execution_end") { - stderr.write(`[cua] tool ${event.toolName} done\n`); - } - }); - - let exitCode = 0; - try { - const invocation = parseSkillInvocation(opts.prompt, opts.skills ?? []); - let assistant; - if (invocation?.skill) { - if (opts.verbose) stderr.write(`[cua] expanded /skill:${invocation.skill.name}\n`); - assistant = await opts.harness.skill(invocation.skill.name, invocation.remainder || undefined); - } else { - assistant = await opts.harness.prompt(opts.prompt); - } - if (assistant.stopReason === "error" || assistant.stopReason === "aborted") { - throw new Error(assistant.errorMessage ?? `agent stopped with ${assistant.stopReason}`); - } - if (!jsonlMode) stdout.write("\n"); - } catch (err) { - if (jsonlMode) { - stdout.write( - JSON.stringify({ - type: "error", - code: "run_failed", - message: (err as Error).message, - ts: Date.now(), - }) + "\n", - ); - } else { - stderr.write(`\n[cua] error: ${(err as Error).message}\n`); - } - exitCode = 1; - } finally { - unsubscribeText(); - unsubscribeJsonl?.(); - } - return exitCode; -} diff --git a/packages/cli/src/tui/debug-log.ts b/packages/cli/src/tui/debug-log.ts deleted file mode 100644 index 26b7550e..00000000 --- a/packages/cli/src/tui/debug-log.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { - appendFileSync, - copyFileSync, - existsSync, - mkdirSync, - readFileSync, - readdirSync, - statSync, - writeFileSync, -} from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { stderr } from "node:process"; - -const PI_RENDER_DIR = "/tmp/tui"; -const PI_REDRAW_LOG = path.join(os.homedir(), ".pi", "agent", "pi-debug.log"); - -interface EnvSnapshot { - PI_TUI_DEBUG?: string; - PI_DEBUG_REDRAW?: string; - PI_TUI_WRITE_LOG?: string; -} - -export interface TuiDebugLog { - readonly dir: string; - log(event: string, data?: Record): void; - close(data?: Record): void; -} - -export function openTuiDebugLog(): TuiDebugLog { - const stamp = new Date().toISOString().replaceAll(":", "-"); - const dir = path.join(os.tmpdir(), `cua-tui-debug-${stamp}-${process.pid}`); - const piRendersDir = path.join(dir, "pi-renders"); - const terminalWriteLog = path.join(dir, "terminal-output.log"); - const eventsPath = path.join(dir, "events.jsonl"); - - mkdirSync(piRendersDir, { recursive: true }); - writeFileSync( - path.join(dir, "README.txt"), - [ - "cua --debug-tui artifacts", - "", - "events.jsonl app-level event timeline", - "terminal-output.log raw terminal bytes written by pi-tui", - "pi-debug-redraw.log full redraw reasons from PI_DEBUG_REDRAW", - "pi-renders/ per-render pi-tui debug snapshots", - "", - "These artifacts are meant to be captured during a manual TUI repro.", - ].join("\n"), - ); - - const previousEnv: EnvSnapshot = { - PI_TUI_DEBUG: process.env.PI_TUI_DEBUG, - PI_DEBUG_REDRAW: process.env.PI_DEBUG_REDRAW, - PI_TUI_WRITE_LOG: process.env.PI_TUI_WRITE_LOG, - }; - - const initialPiRenderFiles = snapshotFiles(PI_RENDER_DIR); - const redrawLogSize = fileSize(PI_REDRAW_LOG); - - process.env.PI_TUI_DEBUG = "1"; - process.env.PI_DEBUG_REDRAW = "1"; - process.env.PI_TUI_WRITE_LOG = terminalWriteLog; - - stderr.write(`[cua] TUI debug logs: ${dir}\n`); - - const writeEvent = (event: string, data: Record = {}): void => { - appendFileSync( - eventsPath, - JSON.stringify({ - ts: new Date().toISOString(), - pid: process.pid, - event, - ...data, - }) + "\n", - ); - }; - - writeEvent("debug_open", { dir }); - - let closed = false; - - return { - dir, - log(event: string, data: Record = {}): void { - writeEvent(event, data); - }, - close(data: Record = {}): void { - if (closed) return; - closed = true; - writeEvent("debug_close", data); - copyNewFiles(PI_RENDER_DIR, initialPiRenderFiles, piRendersDir); - copyRedrawLogDelta(PI_REDRAW_LOG, redrawLogSize, path.join(dir, "pi-debug-redraw.log")); - restoreEnv(previousEnv); - }, - }; -} - -function snapshotFiles(dir: string): Set { - if (!existsSync(dir)) return new Set(); - return new Set(readdirSync(dir)); -} - -function fileSize(file: string): number { - if (!existsSync(file)) return 0; - return statSync(file).size; -} - -function copyNewFiles(sourceDir: string, before: Set, targetDir: string): void { - if (!existsSync(sourceDir)) return; - for (const entry of readdirSync(sourceDir)) { - if (before.has(entry)) continue; - copyFileSync(path.join(sourceDir, entry), path.join(targetDir, entry)); - } -} - -function copyRedrawLogDelta(sourceFile: string, startSize: number, targetFile: string): void { - if (!existsSync(sourceFile)) return; - const content = readFileSync(sourceFile); - const start = Math.min(startSize, content.length); - if (start >= content.length) return; - writeFileSync(targetFile, content.subarray(start)); -} - -function restoreEnv(previous: EnvSnapshot): void { - restoreVar("PI_TUI_DEBUG", previous.PI_TUI_DEBUG); - restoreVar("PI_DEBUG_REDRAW", previous.PI_DEBUG_REDRAW); - restoreVar("PI_TUI_WRITE_LOG", previous.PI_TUI_WRITE_LOG); -} - -function restoreVar(name: keyof EnvSnapshot, value?: string): void { - if (value === undefined) { - delete process.env[name]; - return; - } - process.env[name] = value; -} diff --git a/packages/cli/src/tui/diagnostics.ts b/packages/cli/src/tui/diagnostics.ts deleted file mode 100644 index f648dd2b..00000000 --- a/packages/cli/src/tui/diagnostics.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - type ImageProtocol, - type TerminalCapabilities, - detectCapabilities, - getCapabilities, - setCapabilities, -} from "@earendil-works/pi-tui"; - -export type ImageProtocolOverride = "kitty" | "iterm2" | "none" | "auto"; - -/** - * Resolve image protocol with explicit override > env var > pi-tui detection. - * Mutates pi-tui's cached capabilities so the {@link Image} component uses - * our resolved value on render. - */ -export function resolveImageProtocol(flag?: string): TerminalCapabilities { - const override = normalize(flag) ?? normalize(process.env.CUA_IMAGE_PROTOCOL); - const detected = detectCapabilities(); - - let images: ImageProtocol; - if (override === "auto" || override === undefined) { - images = detected.images; - } else if (override === "none") { - images = null; - } else { - images = override; - } - - const caps: TerminalCapabilities = { - images, - trueColor: detected.trueColor, - hyperlinks: detected.hyperlinks, - }; - setCapabilities(caps); - return caps; -} - -function normalize(value?: string): ImageProtocolOverride | undefined { - if (!value) return undefined; - const v = value.trim().toLowerCase(); - if (v === "kitty" || v === "iterm2" || v === "none" || v === "auto") return v; - return undefined; -} - -/** - * One-line summary of the resolved terminal capabilities, suitable for - * the TUI header so users can see at a glance whether inline images will - * work and how to override. - */ -export function summarizeCapabilities(applied: TerminalCapabilities, source: "auto" | "override"): string { - const parts: string[] = []; - const tag = source === "override" ? " (override)" : ""; - parts.push(`images=${applied.images ?? "none"}${tag}`); - if (applied.trueColor) parts.push("trueColor"); - if (applied.hyperlinks) parts.push("hyperlinks"); - return parts.join(" · "); -} - -export function applyAndSummarizeImageProtocol(flag?: string): { - caps: TerminalCapabilities; - summary: string; - overridden: boolean; -} { - const overridden = !!normalize(flag) || !!normalize(process.env.CUA_IMAGE_PROTOCOL); - const caps = resolveImageProtocol(flag); - return { - caps, - summary: summarizeCapabilities(caps, overridden ? "override" : "auto"), - overridden, - }; -} - -export function currentCapabilities(): TerminalCapabilities { - return getCapabilities(); -} diff --git a/packages/cli/src/tui/keybindings.ts b/packages/cli/src/tui/keybindings.ts deleted file mode 100644 index ef219903..00000000 --- a/packages/cli/src/tui/keybindings.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { getKeybindings, type Keybinding, KeybindingsManager, setKeybindings, TUI_KEYBINDINGS } from "@earendil-works/pi-tui"; - -/** - * cua-specific keybinding ids, merged into pi-tui's global registry via - * declaration merging (the mechanism pi-tui documents for downstream packages). - * Without this augmentation `getKeybindings().matches(data, "cua.tools.…")` - * would not type-check, and without {@link installCuaKeybindings} it would - * silently return false because the lazily-built default manager only knows - * {@link TUI_KEYBINDINGS}. - */ -declare module "@earendil-works/pi-tui" { - interface Keybindings { - "cua.tools.enableAll": true; - "cua.tools.clearAll": true; - "cua.tools.reset": true; - "cua.tools.apply": true; - } -} - -/** - * pi-tui's base bindings plus the bulk actions the `/tools` picker needs. - * `ctrl+a` / `ctrl+x` / `ctrl+s` deliberately match pi's own - * `app.models.enableAll` / `clearAll` / `save` defaults so the two selectors - * feel identical; `ctrl+r` is cua-specific. - */ -export const CUA_TUI_KEYBINDINGS = { - ...TUI_KEYBINDINGS, - "cua.tools.enableAll": { defaultKeys: "ctrl+a", description: "Enable all listed tools" }, - "cua.tools.clearAll": { defaultKeys: "ctrl+x", description: "Disable all listed tools" }, - "cua.tools.reset": { defaultKeys: "ctrl+r", description: "Reset tools to the model defaults" }, - "cua.tools.apply": { defaultKeys: "ctrl+s", description: "Apply the staged tool selection" }, -} as const; - -/** - * Publish {@link CUA_TUI_KEYBINDINGS} as the process-wide manager. Must run - * before any component calls `getKeybindings()`, i.e. at TUI startup. - */ -export function installCuaKeybindings(): void { - setKeybindings(new KeybindingsManager(CUA_TUI_KEYBINDINGS)); -} - -/** - * Render the keys bound to `id` for a hint line. - * - * pi-coding-agent's own `keyText()` is not usable here: it reads the keybinding - * registry through its own module instance of pi-tui, which is not the instance - * {@link installCuaKeybindings} writes to, so cua-specific ids resolve to an - * empty string there. Reading through the same import we register with keeps - * the hints consistent with what {@link getKeybindings} actually matches. - */ -export function cuaKeyText(id: Keybinding): string { - const bound = getKeybindings().getKeys(id); - const keys = bound.length > 0 ? bound : normalizeDefaultKeys(id); - return keys.join("/"); -} - -function normalizeDefaultKeys(id: Keybinding): string[] { - const definition = (CUA_TUI_KEYBINDINGS as Record)[id]; - if (!definition) return []; - return Array.isArray(definition.defaultKeys) ? [...definition.defaultKeys] : [definition.defaultKeys as string]; -} diff --git a/packages/cli/src/tui/main.ts b/packages/cli/src/tui/main.ts deleted file mode 100644 index 568534f5..00000000 --- a/packages/cli/src/tui/main.ts +++ /dev/null @@ -1,872 +0,0 @@ -import { - type AgentHarnessEvent, - type AgentMessage, - estimateContextTokens, - type Session, - type Skill, - type ThinkingLevel, -} from "@onkernel/cua-agent"; -import { - type Component, - Container, - Editor, - hyperlink, - matchesKey, - ProcessTerminal, - Spacer, - Text, - TUI, -} from "@earendil-works/pi-tui"; -import { initTheme } from "@earendil-works/pi-coding-agent"; -import { homedir } from "node:os"; -import { type CuaModelRef, listCuaModels, type Model } from "@onkernel/cua-ai"; -import { type CuaCliCatalog, type CuaCliHarness, type CuaCliTool } from "../harness"; -import type { CuaBrowserHandle } from "../harness-browser"; -import { resolveCuaModelRef } from "../harness-models"; -import { updateNamedSessionRuntime } from "../harness-named-sessions"; -import type { ContextFile } from "../harness-skills"; -import { openTuiDebugLog } from "./debug-log"; -import { applyAndSummarizeImageProtocol } from "./diagnostics"; -import { installCuaKeybindings } from "./keybindings"; -import { type AssistantBuffer, MessageList } from "./message-list"; -import { createMutationQueue } from "./mutation-queue"; -import { fitMaxVisible, ModelPickerComponent } from "./model-picker"; -import { ScreenshotWidget } from "./screenshot-widget"; -import { buildAutocompleteProvider, parseSlashCommand } from "./slash-commands"; -import { StatusLine } from "./status-line"; -import { TelemetryFooter } from "./telemetry-footer"; -import { colors, getEditorTheme } from "./themes"; -import { describeMenu, selectedKeys, toolKey, toolsForSelection, type ToolSelectionItem } from "./tool-selection"; -import { ToolsPickerComponent } from "./tools-picker"; -import { cuaVersion } from "./version"; - -export interface InteractiveOptions { - cwd: string; - harness: CuaCliHarness; - /** The live (model, tools) selection `/model` and `/tools` change. */ - catalog: CuaCliCatalog; - browserHandle: CuaBrowserHandle; - session: Session; - skills?: Skill[]; - /** Loaded context files (AGENTS.md, …) shown in the `[Context]` section. */ - contextFiles?: ContextFile[]; - /** CUA model ref currently active. Used for the status line and `/model` default. */ - modelRef: string; - provider: string; - /** Coding tools explicitly owned by the CLI and retained across /model switches. */ - applicationTools: readonly CuaCliTool[]; - /** Optional CLI application policy for replacing interaction tools on /model. */ - interactionToolsForModel?: (model: CuaModelRef) => readonly CuaCliTool[]; - initialPrompt?: string; - /** Image protocol override: kitty | iterm2 | none | auto (default: auto). */ - imageProtocol?: string; - /** True when seeding the agent from a previously persisted session. */ - resumed?: boolean; - /** Display path of the on-disk transcript, when one exists. */ - transcriptPath?: string; - /** Named session (-s) backing this TUI; /model switches persist to it. */ - namedSession?: string; - /** Enable extra TUI render diagnostics for manual repros. */ - debugTui?: boolean; -} - -/** - * Run the interactive cua TUI: pi-tui differential renderer with header, - * message list, sticky screenshot widget, editor (autocomplete-backed slash - * commands), status line, and telemetry footer. Drives a {@link CuaCliHarness} - * directly via `harness.subscribe()`. - */ -export async function runInteractive(opts: InteractiveOptions): Promise { - // pi's `theme` singleton throws until initialized; do this before any - // component or theme helper runs. - initTheme(); - // Apply image protocol override BEFORE constructing TUI components so - // the Image component sees the resolved capabilities on its first render. - const { summary: capsSummary, overridden } = applyAndSummarizeImageProtocol(opts.imageProtocol); - const debug = opts.debugTui ? openTuiDebugLog() : undefined; - const initialModel = opts.harness.getModel(); - const initialThinking = opts.harness.getThinkingLevel(); - const initialContextWindow = initialModel.contextWindow ?? undefined; - debug?.log("interactive_init", { - model: opts.modelRef, - browserSession: opts.browserHandle.browser.session_id, - liveUrl: opts.browserHandle.browser.browser_live_view_url, - capsSummary, - imageProtocol: opts.imageProtocol ?? "auto", - overridden, - }); - - const terminal = new ProcessTerminal(); - const tui = new TUI(terminal); - const requestRender = (reason: string, force = false, data: Record = {}): void => { - debug?.log("request_render", { - reason, - force, - columns: terminal.columns, - rows: terminal.rows, - fullRedraws: tui.fullRedraws, - ...data, - }); - tui.requestRender(force); - }; - - // Publishes cua's `cua.tools.*` ids alongside pi's base bindings. Must run - // before any component calls getKeybindings(). - installCuaKeybindings(); - - const editor = new Editor(tui, getEditorTheme()); - editor.setAutocompleteProvider(buildAutocompleteProvider(opts.cwd, opts.skills ?? [])); - const messages = new MessageList(); - const screenshot = new ScreenshotWidget(); - const liveUrl = opts.browserHandle.browser.browser_live_view_url; - const status = new StatusLine({ - model: modelLabel(initialModel), - browserSession: opts.browserHandle.browser.session_id, - liveUrl, - }); - const footer = new TelemetryFooter({ - provider: opts.provider, - model: modelLabel(initialModel), - thinkingLevel: initialThinking, - contextWindow: initialContextWindow, - contextTokens: 0, - }); - - const header = new Container(); - const logo = colors.bold(colors.accent("cua")) + colors.dim(` v${cuaVersion()}`); - header.addChild(new Text(logo, 0, 0)); - header.addChild(new Text(keyHintRow(), 0, 0)); - const capsHint = overridden - ? colors.dim(capsSummary) - : colors.dim(capsSummary + " · set CUA_IMAGE_PROTOCOL=kitty|iterm2 to force inline images"); - header.addChild(new Text(capsHint, 0, 0)); - if (liveUrl) { - header.addChild(new Text(colors.dim("live ") + hyperlink(liveUrl, liveUrl), 0, 0)); - } - header.addChild(new Text("", 0, 0)); - - const contextSection = buildContextSection(opts.contextFiles ?? []); - const skillSection = buildSkillSection(opts.skills ?? []); - tui.addChild(header); - if (contextSection) { - tui.addChild(contextSection); - tui.addChild(new Spacer(1)); - } - if (skillSection) { - tui.addChild(skillSection); - tui.addChild(new Spacer(1)); - } - tui.addChild(messages); - tui.addChild(new Spacer(1)); - tui.addChild(screenshot); - tui.addChild(new Spacer(1)); - // Pickers swap into the editor's slot (pi's `showSelector` pattern) so the - // status line and telemetry footer stay visible beneath them. - const editorContainer = new Container(); - editorContainer.addChild(editor); - tui.addChild(editorContainer); - tui.addChild(status); - tui.addChild(footer); - tui.setFocus(editor); - tui.onDebug = () => { - debug?.log("pi_tui_debug_key", { - columns: terminal.columns, - rows: terminal.rows, - fullRedraws: tui.fullRedraws, - }); - }; - - if (opts.resumed) { - const transcript = opts.transcriptPath ? ` ${opts.transcriptPath}` : ""; - messages.addNotice(`resumed${transcript} · fresh browser`); - } - - let assistantBuffer: AssistantBuffer | undefined; - let inflight = 0; - let promptRunning = 0; - let turnRevision = 0; - let interruptState: { queued: string[]; cancelled: boolean } | undefined; - let lastDisplayedError: string | undefined; - - const isTurnRunning = (): boolean => inflight > 0 || promptRunning > 0; - - // Ref of the live model, kept in sync by switchModel so the picker can mark - // it with a ✓. Undefined when opts.modelRef is not a catalog ref. - let currentModelRef: CuaModelRef | undefined = tryResolveModelRef(opts.modelRef); - // The list the application composed for the active model: the picker's - // "defaults", restored by ctrl+r. A selection is no longer confined to it — - // the picker offers the model's whole menu — but every staged change is - // still compiled by `harness.setTools()` before it can land. - let baselineTools: readonly CuaCliTool[] = composeBaselineTools(opts, currentModelRef); - let toolSelectionCustomized = false; - // Serializes every catalog mutation (`/tools` applies and `/model` switches); - // see mutation-queue.ts for why they must not interleave. - const catalogQueue = createMutationQueue(); - // Non-null while a picker owns the editor slot and all keyboard input. - let activeSelector: Component | null = null; - - /** - * Swap a picker into the editor's slot and restore the editor when it is - * done. Mirrors pi's `showSelector` in interactive-mode. - */ - const showSelector = ( - create: (done: () => void) => { component: Component; focus: Component }, - ): void => { - if (activeSelector) return; - const done = (): void => { - activeSelector = null; - editorContainer.clear(); - editorContainer.addChild(editor); - tui.setFocus(editor); - requestRender("selector_closed"); - }; - const { component, focus } = create(done); - activeSelector = component; - editorContainer.clear(); - editorContainer.addChild(component); - tui.setFocus(focus); - requestRender("selector_opened"); - }; - - /** - * Refuse to open a picker mid-turn. Recompiling the catalog while a request - * is streaming is unsafe, and nothing downstream stops it: the agent's - * execution-scope guard only rejects mutation from inside a tool's execute, - * and a TUI-initiated mutation carries no such scope. This check is the only - * protection, so it refuses up front rather than failing on apply. - */ - const refuseWhileBusy = (command: string): boolean => { - if (!isTurnRunning() && !interruptState) return false; - messages.addError(`${command} is unavailable while a turn is running`); - requestRender("selector_busy", false, { command }); - return true; - }; - - const displayAgentError = (error: unknown, reason: string): void => { - if (typeof error !== "string" || error.trim().length === 0) return; - if (error === lastDisplayedError) return; - lastDisplayedError = error; - messages.addError(error); - status.update({ working: undefined }); - debug?.log("agent_error", { reason, message: error }); - requestRender("agent_error", false, { reason }); - }; - - const unsubscribe = opts.harness.subscribe((event: AgentHarnessEvent) => { - switch (event.type) { - case "agent_start": { - inflight += 1; - status.update({ working: "thinking…" }); - debug?.log("agent_start", { inflight }); - requestRender("agent_start", false, { inflight }); - return; - } - case "agent_end": { - inflight -= 1; - if (inflight <= 0) status.update({ working: undefined }); - const finalError = lastErrorMessage(event.messages); - displayAgentError(finalError, "agent_end"); - debug?.log("agent_end", { inflight }); - requestRender("agent_end", false, { inflight }); - return; - } - case "message_start": { - if (event.message.role === "assistant") { - assistantBuffer = messages.addAssistantStart(); - debug?.log("assistant_message_start"); - requestRender("assistant_message_start"); - } - return; - } - case "message_update": { - if (event.assistantMessageEvent.type === "text_delta") { - assistantBuffer?.append(event.assistantMessageEvent.delta); - requestRender("assistant_text_delta", false, { - deltaLength: event.assistantMessageEvent.delta.length, - }); - } - return; - } - case "message_end": { - if (event.message.role === "assistant") { - if (event.message.usage) { - footer.update({ contextTokens: event.message.usage.input }); - } - assistantBuffer?.end(); - assistantBuffer = undefined; - displayAgentError(event.message.errorMessage, "assistant_message_end"); - debug?.log("assistant_message_end"); - requestRender("assistant_message_end"); - } - return; - } - case "tool_execution_start": { - messages.addToolCall(event.toolName, event.args); - status.update({ working: event.toolName }); - debug?.log("tool_execution_start", { toolName: event.toolName }); - requestRender("tool_execution_start", false, { toolName: event.toolName }); - return; - } - case "tool_execution_end": { - const result = event.result as - | { - content?: Array<{ type?: string; data?: string; mimeType?: string }>; - details?: { error?: string }; - } - | undefined; - const isError = !!event.isError; - let summary = isError ? colors.error("error") : colors.success("ok"); - if (!isError && result?.content) { - const imgs = result.content.filter((c) => c?.type === "image"); - if (imgs.length > 0) summary += colors.dim(` · ${imgs.length} screenshot${imgs.length > 1 ? "s" : ""}`); - const lastImg = imgs[imgs.length - 1]; - if (lastImg?.data) screenshot.update(lastImg.data, lastImg.mimeType ?? "image/png"); - } - if (isError && result?.details?.error) summary = colors.error(result.details.error); - messages.addToolResult(event.toolName, !isError, summary); - debug?.log("tool_execution_end", { - toolName: event.toolName, - isError, - hasImage: !!result?.content?.some((c) => c?.type === "image"), - }); - requestRender("tool_execution_end", false, { - toolName: event.toolName, - isError, - }); - return; - } - case "model_update": { - footer.update({ - provider: event.model.provider, - model: modelLabel(event.model), - contextWindow: event.model.contextWindow, - }); - status.update({ model: modelLabel(event.model) }); - requestRender("model_update"); - return; - } - case "thinking_level_update": { - footer.update({ thinkingLevel: event.level }); - requestRender("thinking_level_update"); - return; - } - case "session_compact": { - messages.addNotice(`compacted ${event.compactionEntry.tokensBefore} tokens`); - void refreshContextTokens(opts.session).then((tokens) => { - footer.update({ contextTokens: tokens }); - requestRender("session_compact"); - }); - return; - } - default: - return; - } - }); - - const pendingPrompt = opts.initialPrompt?.trim() || ""; - let exitRequested = false; - - /** - * Apply a model switch. The picker and `/model ` share this one path. - * A failed switch needs no rollback here: the harness compiles before it - * mutates and restores its own state if the mutation fails. - */ - const applySwitchModel = async (resolved: CuaModelRef): Promise => { - // The exact list installed by this switch, kept so it can become the new - // `/tools` baseline. Undefined when the caller supplies no interaction - // policy, in which case the switch never touches the tool list at all. - let installedTools: readonly CuaCliTool[] | undefined; - if (opts.interactionToolsForModel) { - installedTools = [...opts.interactionToolsForModel(resolved), ...opts.applicationTools]; - // Native catalogs are incompatible across providers, and the selected - // tools decide the transport, so the new model and its interaction - // catalog have to compile as one pair rather than in sequence. - await opts.catalog.setModelAndTools(resolved, installedTools); - } else { - await opts.catalog.setModel(resolved); - } - const model = opts.harness.getModel(); - footer.update({ - provider: model.provider, - model: modelLabel(model), - contextWindow: model.contextWindow, - }); - status.update({ model: modelLabel(model) }); - messages.addNotice(`model → ${resolved}`); - currentModelRef = resolved; - // Only an interaction policy rebuilds the tool list; without one the switch - // never touched setTools, so any customization legitimately survives and - // announcing a reset would be a lie. Rebasing the baseline on the live list - // there would also shrink it permanently. - if (installedTools) { - // Adopt the very list just installed as the new baseline, so baseline keys - // and the live catalog can never disagree. Tool identities are - // provider-specific, so carrying a previous selection over would silently - // substitute tools; report the reset instead. - baselineTools = installedTools; - if (toolSelectionCustomized) { - messages.addNotice("tool selection reset to the new model's defaults"); - toolSelectionCustomized = false; - } - } - await persistNamedSessionRuntime(opts, messages, { model: resolved }); - }; - - /** - * Serialized entry point for a model switch. Queued behind any in-flight - * `/tools` apply so the apply's `setTools` cannot land mid-switch and compile - * its tool subset against the other model. Rejects with the underlying - * failure; the harness has already rolled back by then. - */ - const switchModel = (resolved: CuaModelRef): Promise => catalogQueue.run(() => applySwitchModel(resolved)); - - const openModelPicker = (initialSearch?: string): void => { - if (refuseWhileBusy("/model")) return; - showSelector((done) => { - const picker = new ModelPickerComponent({ - tui, - currentRef: currentModelRef, - items: listCuaModels(), - initialSearch, - // Frame overhead: borders, hint, search, detail lines, plus the - // header/status/footer chrome the picker sits between. - maxVisible: fitMaxVisible(terminal.rows, 22), - onSelect: (ref) => { - // Close first (pi does the same) so a failing switch surfaces in - // the message list with the editor already restored. - done(); - void switchModel(ref).catch((err: unknown) => { - messages.addError((err as Error).message); - requestRender("model_switch_error"); - }); - }, - onCancel: done, - }); - return { component: picker, focus: picker }; - }); - }; - - /** - * Apply a staged selection of the model's tool menu, in menu order. - * `harness.setTools` compiles and validates before mutating, so a rejected - * selection leaves the live catalog untouched. - */ - const applyToolSelection = (items: readonly ToolSelectionItem[], enabledKeys: ReadonlySet): Promise => - catalogQueue.run(async () => { - const next = toolsForSelection(items, enabledKeys); - try { - await opts.catalog.setTools(next); - toolSelectionCustomized = !sameToolList(next, baselineTools); - messages.addNotice(`tools → ${next.length} enabled`); - debug?.log("tools_applied", { enabled: next.length, baseline: baselineTools.length }); - } catch (err) { - messages.addError(`tool selection rejected (tools unchanged): ${(err as Error).message}`); - debug?.log("tools_apply_error", { message: (err as Error).message }); - } - requestRender("tools_apply"); - }); - - const openToolsPicker = (): void => { - if (refuseWhileBusy("/tools")) return; - const modelRef = currentModelRef; - if (!modelRef) { - messages.addError("the tool menu needs a catalog model ref; this session was started with a model object"); - requestRender("tools_no_ref"); - return; - } - const live = opts.catalog.getTools(); - // Availability is pairwise, so the menu is rebuilt against each staged - // selection rather than computed once when the picker opens. - const menuFor = (selected: readonly CuaCliTool[]) => describeMenu(modelRef, opts.applicationTools, selected); - const items = menuFor(live); - if (items.length === 0) { - messages.addError("no model-callable tools are available for this model"); - requestRender("tools_empty"); - return; - } - showSelector((done) => { - const picker = new ToolsPickerComponent({ - tui, - items, - enabledKeys: selectedKeys(items, live), - defaultKeys: selectedKeys(items, baselineTools), - maxVisible: fitMaxVisible(terminal.rows, 25), - restage: (staged: ReadonlySet) => menuFor(toolsForSelection(items, staged)), - onApply: (enabled) => { - done(); - void applyToolSelection(items, enabled); - }, - onCancel: done, - }); - return { component: picker, focus: picker }; - }); - }; - - const sameToolList = (a: readonly CuaCliTool[], b: readonly CuaCliTool[]): boolean => - a.length === b.length && a.every((tool, index) => toolKey(tool) === toolKey(b[index]!)); - - const promptAgent = async (text: string): Promise => { - promptRunning += 1; - try { - await opts.harness.prompt(text); - } finally { - promptRunning -= 1; - } - }; - - const runPrompt = async (text: string): Promise => { - debug?.log("run_prompt_start", { length: text.length }); - try { - const parsed = parseSlashCommand(text); - if (parsed && refuseWhileBusy(`/${parsed.command}`)) return; - if (parsed?.command === "model") { - const argument = parsed.argument.trim(); - if (!argument) { - openModelPicker(); - return; - } - let resolved: CuaModelRef; - try { - resolved = resolveCuaModelRef(argument); - } catch (err) { - // Keep the diagnostic, then offer the picker prefilled with the - // unresolved text (pi's behavior for an unmatched /model arg). - messages.addError((err as Error).message); - openModelPicker(argument); - return; - } - if (refuseWhileBusy("/model")) return; - await switchModel(resolved); - return; - } - if (parsed?.command === "tools") { - if (parsed.argument) { - messages.addNotice("/tools takes no argument; opening the picker"); - } - openToolsPicker(); - return; - } - if (parsed?.command === "thinking") { - await applyThinkingCommand(opts, footer, messages, parsed.argument); - return; - } - if (parsed?.command === "compact") { - await applyCompactCommand(opts, messages); - return; - } - if (parsed?.command === "skill") { - const skill = (opts.skills ?? []).find((s) => s.name === parsed.name); - if (!skill) { - messages.addError(`unknown skill "${parsed.name}"`); - requestRender("skill_unknown"); - return; - } - messages.addNotice(`invoking /skill:${skill.name}`); - requestRender("skill_invocation"); - const skillRemainder = parsed.remainder || undefined; - await opts.harness.skill(skill.name, skillRemainder); - return; - } - if (interruptState) { - interruptState.queued.push(text); - messages.addNotice(interruptState.cancelled ? "queued for after abort" : "queued for the interrupted turn"); - requestRender("prompt_queued_during_interrupt"); - return; - } - if (isTurnRunning()) { - const revision = turnRevision; - await opts.harness.steer(text); - if (revision !== turnRevision) return; - messages.addNotice("queued for the next available turn"); - requestRender("prompt_queued_for_steer"); - return; - } - await promptAgent(text); - } catch (err) { - messages.addError((err as Error).message); - debug?.log("run_prompt_error", { message: (err as Error).message }); - requestRender("run_prompt_error", false, { message: (err as Error).message }); - return; - } - debug?.log("run_prompt_end"); - }; - - editor.onSubmit = (text: string) => { - const trimmed = text.trim(); - if (!trimmed) return; - editor.setText(""); - editor.addToHistory(trimmed); - messages.addUser(trimmed); - debug?.log("editor_submit", { length: trimmed.length }); - void runPrompt(trimmed); - }; - - const startQueuedPrompt = (queued: string[], notice: string): void => { - messages.addNotice(`${notice}; sending ${queued.length} queued message${queued.length === 1 ? "" : "s"}`); - requestRender("queued_prompt_start", false, { queued: queued.length }); - void promptAgent(queued.join("\n\n")).catch((err: unknown) => { - messages.addError((err as Error).message); - debug?.log("queued_prompt_error", { message: (err as Error).message }); - requestRender("queued_prompt_error"); - }); - }; - - const interruptTurn = async (): Promise => { - if (interruptState) return; - const state: { queued: string[]; cancelled: boolean } = { queued: [], cancelled: false }; - interruptState = state; - turnRevision += 1; - messages.addNotice("interrupting…"); - requestRender("input_interrupt_start", false, { key: "escape" }); - try { - const { clearedSteer, clearedFollowUp } = await opts.harness.abort(); - if (state.cancelled) { - const queued = state.queued; - state.queued = []; - if (queued.length > 0) { - interruptState = undefined; - startQueuedPrompt(queued, "abort complete"); - } - return; - } - const queued = [ - ...clearedSteer.map(userMessageText).filter((text): text is string => !!text), - ...clearedFollowUp.map(userMessageText).filter((text): text is string => !!text), - ...state.queued, - ]; - state.queued = []; - if (queued.length === 0) { - messages.addNotice("turn aborted"); - requestRender("input_abort_stream", false, { key: "escape" }); - return; - } - - interruptState = undefined; - startQueuedPrompt(queued, "turn interrupted"); - } catch (err) { - state.queued = []; - messages.addError((err as Error).message); - debug?.log("input_interrupt_error", { message: (err as Error).message }); - requestRender("input_interrupt_error"); - } finally { - if (interruptState === state) interruptState = undefined; - } - }; - - const removeListener = tui.addInputListener((data) => { - // Input listeners run before the focused component, so an open picker has - // to own every key: otherwise ctrl+c / ctrl+d here would quit the app - // instead of cancelling the picker. - if (activeSelector) return undefined; - if (matchesKey(data, "ctrl+c")) { - if (interruptState) { - interruptState.cancelled = true; - interruptState.queued = []; - messages.addNotice("aborted"); - debug?.log("input_cancel_interrupt_replay", { key: "ctrl+c" }); - requestRender("input_cancel_interrupt_replay", false, { key: "ctrl+c" }); - return { consume: true }; - } - if (isTurnRunning()) { - turnRevision += 1; - void opts.harness.abort(); - messages.addNotice("aborted"); - debug?.log("input_abort_stream", { key: "ctrl+c" }); - requestRender("input_abort_stream", false, { key: "ctrl+c" }); - return { consume: true }; - } - exitRequested = true; - debug?.log("input_exit_request", { key: "ctrl+c" }); - requestRender("input_exit_request", false, { key: "ctrl+c" }); - return { consume: true }; - } - if (matchesKey(data, "ctrl+d")) { - exitRequested = true; - debug?.log("input_exit_request", { key: "ctrl+d" }); - return { consume: true }; - } - if (matchesKey(data, "escape") && (isTurnRunning() || interruptState)) { - void interruptTurn(); - debug?.log("input_interrupt_stream", { key: "escape" }); - return { consume: true }; - } - return undefined; - }); - - tui.start(); - debug?.log("tui_started", { - columns: terminal.columns, - rows: terminal.rows, - fullRedraws: tui.fullRedraws, - }); - - try { - if (pendingPrompt) { - messages.addUser(pendingPrompt); - void runPrompt(pendingPrompt); - } - - await waitForExit( - () => exitRequested, - () => isTurnRunning() || !!interruptState, - ); - - return 0; - } finally { - removeListener(); - unsubscribe(); - tui.stop(); - debug?.close({ - fullRedraws: tui.fullRedraws, - columns: terminal.columns, - rows: terminal.rows, - }); - } -} - -async function waitForExit(shouldExit: () => boolean, isBusy: () => boolean): Promise { - while (true) { - if (shouldExit() && !isBusy()) return; - await new Promise((resolve) => setTimeout(resolve, 100)); - } -} - -function modelLabel(model: Model | undefined): string { - if (!model) return ""; - return model.id; -} - -function userMessageText(message: AgentMessage): string | undefined { - if (message.role !== "user") return undefined; - if (typeof message.content === "string") return message.content.trim() || undefined; - const text = message.content - .filter((content) => content.type === "text") - .map((content) => content.text) - .join(""); - return text.trim() || undefined; -} - -function lastErrorMessage(messages: AgentMessage[]): string | undefined { - for (let i = messages.length - 1; i >= 0; i -= 1) { - const m = messages[i]; - if (m && m.role === "assistant" && typeof m.errorMessage === "string") { - return m.errorMessage; - } - } - return undefined; -} - -/** Resolve the startup ref for picker bookkeeping; undefined when not a catalog ref. */ -function tryResolveModelRef(input: string | undefined): CuaModelRef | undefined { - try { - return resolveCuaModelRef(input); - } catch { - return undefined; - } -} - -/** - * The startup baseline: exactly the list `cli-harness` assembled for the initial - * model. Only used once — a later switch adopts the list it installed instead, - * so the baseline is never rebased on `harness.getTools()`, which a `/tools` - * customization would have shrunk. - */ -function composeBaselineTools(opts: InteractiveOptions, ref: CuaModelRef | undefined): readonly CuaCliTool[] { - if (!opts.interactionToolsForModel || !ref) return opts.catalog.getTools(); - return [...opts.interactionToolsForModel(ref), ...opts.applicationTools]; -} - -// Persistence is best-effort: the live switch already happened, so a failed -// metadata write must not masquerade as a failed switch — warn that resume -// will restore the previous value instead. -async function persistNamedSessionRuntime( - opts: InteractiveOptions, - messages: MessageList, - patch: { model?: string }, -): Promise { - if (!opts.namedSession) return; - try { - await updateNamedSessionRuntime(opts.namedSession, patch); - } catch (err) { - messages.addError( - `switched, but failed to persist to session "${opts.namedSession}" (resume will restore the previous value): ${(err as Error).message}`, - ); - } -} - -async function applyThinkingCommand( - opts: InteractiveOptions, - footer: TelemetryFooter, - messages: MessageList, - argument: string, -): Promise { - const value = argument.trim().toLowerCase(); - if (!isThinkingLevel(value)) { - messages.addError("usage: /thinking "); - return; - } - try { - await opts.harness.setThinkingLevel(value); - footer.update({ thinkingLevel: value }); - messages.addNotice(`thinking → ${value}`); - } catch (err) { - messages.addError((err as Error).message); - } -} - -function isThinkingLevel(value: string): value is ThinkingLevel { - return ["off", "minimal", "low", "medium", "high", "xhigh"].includes(value); -} - -async function applyCompactCommand(opts: InteractiveOptions, messages: MessageList): Promise { - messages.addNotice("compacting…"); - try { - // The `session_compact` harness event posts the final - // "compacted N tokens" notice; emitting it here too would duplicate. - await opts.harness.compact(); - } catch (err) { - messages.addError((err as Error).message); - } -} - -async function refreshContextTokens(session: Session): Promise { - const context = await session.buildContext(); - return estimateContextTokens(context.messages).tokens; -} - -function keyHintRow(): string { - const hint = (keys: string, label: string) => colors.bold(keys) + colors.dim(` ${label}`); - return [ - hint("esc/ctrl+c", "to interrupt"), - hint("ctrl+c/ctrl+d", "to exit"), - hint("/", "for commands"), - ].join(colors.muted(" · ")); -} - -function sectionLabel(name: string): string { - return colors.heading(`[${name}]`); -} - -function buildContextSection(contextFiles: ContextFile[]): Container | undefined { - if (contextFiles.length === 0) return undefined; - const paths = contextFiles.map((file) => displayPath(file.path)).join(", "); - const container = new Container(); - container.addChild(new Text(sectionLabel("Context") + "\n" + colors.dim(` ${paths}`), 0, 0)); - return container; -} - -function buildSkillSection(skills: Skill[]): Container | undefined { - if (skills.length === 0) return undefined; - const names = skills - .map((s) => s.name) - .sort((a, b) => a.localeCompare(b)) - .join(", "); - const container = new Container(); - container.addChild(new Text(sectionLabel("Skills") + "\n" + colors.dim(` ${names}`), 0, 0)); - return container; -} - -function displayPath(path: string): string { - const home = homedir(); - return path.startsWith(home) ? `~${path.slice(home.length)}` : path; -} diff --git a/packages/cli/src/tui/message-list.ts b/packages/cli/src/tui/message-list.ts deleted file mode 100644 index 29df67a0..00000000 --- a/packages/cli/src/tui/message-list.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { Container, Markdown, Text } from "@earendil-works/pi-tui"; -import { colors, getMarkdownTheme } from "./themes"; - -/** - * Append-only chat log of user prompts, assistant text, tool-call summaries, - * and inline error notes. Assistant blocks render through pi-tui's - * {@link Markdown}; everything else uses plain styled {@link Text}. - */ -export class MessageList extends Container { - addUser(text: string): void { - this.appendBlock([colors.bold("you ") + colors.dim("›") + " " + text]); - } - - addAssistantStart(): AssistantBuffer { - const buffer = new AssistantBuffer(); - this.addChild(buffer); - this.invalidate(); - return buffer; - } - - addToolCall(name: string, args: unknown): void { - const summary = formatToolCall(name, args); - this.appendBlock([colors.accent("· ") + colors.dim(name) + " " + summary]); - } - - addToolResult(name: string, ok: boolean, summary: string): void { - const icon = ok ? colors.success("✓") : colors.error("✗"); - this.appendBlock([` ${icon} ${colors.dim(name)} ${summary}`]); - } - - addNotice(text: string): void { - this.appendBlock([colors.warning("· ") + colors.dim(text)]); - } - - addError(text: string): void { - this.appendBlock([colors.error("error ") + text]); - } - - private appendBlock(lines: string[]): void { - for (const line of lines) { - this.addChild(new Text(line, 0, 0)); - } - this.invalidate(); - } -} - -/** Live-updating buffer for the in-flight assistant message. */ -export class AssistantBuffer extends Container { - private text = ""; - private readonly body: Markdown; - - constructor() { - super(); - this.addChild(new Text(colors.success("assistant"), 0, 0)); - this.body = new Markdown("", 0, 0, getMarkdownTheme()); - this.addChild(this.body); - } - - append(delta: string): void { - this.text += delta; - this.body.setText(this.text); - this.invalidate(); - } - - end(): void { - if (!this.text.trim()) { - this.children = []; - } - this.invalidate(); - } -} - -function formatToolCall(name: string, args: unknown): string { - if (!args || typeof args !== "object") return ""; - const obj = args as Record; - switch (name) { - case "computer_batch": { - const actions = Array.isArray(obj.actions) ? obj.actions : []; - if (actions.length === 0) return "(empty)"; - const parts = (actions as Array>).slice(0, 4).map(describeAction); - const more = actions.length > 4 ? colors.dim(` +${actions.length - 4} more`) : ""; - return parts.join(colors.dim(" → ")) + more; - } - case "playwright_execute": - return colors.dim(typeof obj.code === "string" ? truncate(obj.code.replace(/\s+/g, " ").trim(), 80) : ""); - case "bash": - return colors.dim(typeof obj.command === "string" ? truncate(obj.command, 80) : ""); - case "read": - case "write": - case "edit": - return colors.dim(typeof obj.path === "string" ? obj.path : ""); - default: - return describeAction(obj); - } -} - -function truncate(text: string, max: number): string { - if (text.length <= max) return text; - return text.slice(0, max - 1) + "…"; -} - -function describeAction(action: Record): string { - const t = typeof action.action === "string" ? action.action : typeof action.type === "string" ? action.type : ""; - const num = (v: unknown) => (typeof v === "number" ? Math.trunc(v) : 0); - switch (t) { - case "click": - return `click(${num(action.x)},${num(action.y)})`; - case "double_click": - return `dblclick(${num(action.x)},${num(action.y)})`; - case "triple_click": - return `triple(${num(action.x)},${num(action.y)})`; - case "type": - return `type(${truncate(JSON.stringify(action.text ?? ""), 24)})`; - case "keypress": - return `key(${(action.keys as string[] | undefined)?.join("+") ?? ""})`; - case "scroll": - return `scroll(${num(action.x)},${num(action.y)})`; - case "move": - return `move(${num(action.x)},${num(action.y)})`; - case "drag": - return `drag(...)`; - case "wait": - return `wait(${typeof action.ms === "number" ? action.ms : 1000}ms)`; - case "goto": - return `goto(${typeof action.url === "string" ? action.url : ""})`; - case "back": - return "back"; - case "forward": - return "forward"; - case "url": - return "url"; - case "screenshot": - return "screenshot"; - default: - return t || colors.dim(truncate(JSON.stringify(action), 80)); - } -} diff --git a/packages/cli/src/tui/model-picker.ts b/packages/cli/src/tui/model-picker.ts deleted file mode 100644 index be5ccd96..00000000 --- a/packages/cli/src/tui/model-picker.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { - Container, - type Focusable, - fuzzyFilter, - getKeybindings, - Input, - Spacer, - Text, - type TUI, -} from "@earendil-works/pi-tui"; -import { DynamicBorder } from "@earendil-works/pi-coding-agent"; -import type { CuaModelInfo, CuaModelRef } from "@onkernel/cua-ai"; -import { colors } from "./themes"; - -/** Rows shown at once in a picker list, matching pi's model selector. */ -export const PICKER_MAX_VISIBLE = 10; - -/** - * Fit a picker's list to the viewport. A picker replaces the editor, so a tall - * frame on a short terminal would push its own footer off-screen; cap the list - * instead of letting the frame overflow. - */ -export function fitMaxVisible(terminalRows: number, reservedRows: number, cap = PICKER_MAX_VISIBLE): number { - return Math.max(3, Math.min(cap, terminalRows - reservedRows)); -} - -/** - * cua-ref analogue of pi's `getModelSelectorSearchText`. Provider is repeated - * and the bare model id kept out of leading position for the same reason pi - * does it: a provider-qualified query should outrank an incidental substring - * match on another provider's id. - */ -export function modelSearchText(item: CuaModelInfo): string { - return `${item.provider} ${item.ref} ${item.provider} ${item.model}${item.name ? ` ${item.name}` : ""}`; -} - -/** - * Current model first, then {@link listCuaModels}' own order (provider order - * from `CUA_PROVIDERS`, then model id). pi sorts by provider name; keeping - * cua's catalog order instead means the picker lists models exactly like - * `cua models` does. - */ -export function sortModelsForPicker(items: readonly CuaModelInfo[], currentRef: string | undefined): CuaModelInfo[] { - const current = items.filter((item) => item.ref === currentRef); - const rest = items.filter((item) => item.ref !== currentRef); - return [...current, ...rest]; -} - -/** Fuzzy-filter over {@link modelSearchText}; an empty query keeps every row. */ -export function filterModelsForPicker(items: readonly CuaModelInfo[], query: string): CuaModelInfo[] { - return query ? fuzzyFilter([...items], query, modelSearchText) : [...items]; -} - -/** Wrap-around cursor movement; a no-op on an empty list. */ -export function moveSelection(index: number, delta: 1 | -1, length: number): number { - if (length === 0) return 0; - if (delta === -1) return index === 0 ? length - 1 : index - 1; - return index === length - 1 ? 0 : index + 1; -} - -/** Keep the cursor inside a list that just shrank. */ -export function clampSelection(index: number, length: number): number { - return Math.max(0, Math.min(index, length - 1)); -} - -/** Centred-cursor scroll window, matching pi's model selector arithmetic. */ -export function visibleWindow( - selectedIndex: number, - length: number, - maxVisible: number = PICKER_MAX_VISIBLE, -): { start: number; end: number } { - const start = Math.max(0, Math.min(selectedIndex - Math.floor(maxVisible / 2), length - maxVisible)); - return { start, end: Math.min(start + maxVisible, length) }; -} - -export interface ModelPickerConfig { - tui: TUI; - /** Active ref, rendered with a ✓ and sorted first. Undefined when unresolvable. */ - currentRef: CuaModelRef | undefined; - items: readonly CuaModelInfo[]; - onSelect: (ref: CuaModelRef) => void; - onCancel: () => void; - /** Prefills the search box, as pi does for an unmatched `/model `. */ - initialSearch?: string; - /** List height; defaults to pi's 10. See {@link fitMaxVisible}. */ - maxVisible?: number; -} - -/** - * Searchable model picker modelled on pi's `ModelSelectorComponent`: same - * frame, same bare search input, same 10-row centred scroll window, same - * `→ `/`[provider]`/` ✓` row format, same wrap-around navigation and - * single-press cancel. - * - * Deliberate differences from pi: cua's catalog ({@link listCuaModels}) is a - * static synchronous table, so there is no background refresh, no abort - * controller and no refresh status line; and selecting a model never writes - * pi's global `settings.json` — persistence is the caller's business. - */ -export class ModelPickerComponent extends Container implements Focusable { - private readonly tui: TUI; - private readonly currentRef: CuaModelRef | undefined; - private readonly allModels: readonly CuaModelInfo[]; - private readonly onSelectCallback: (ref: CuaModelRef) => void; - private readonly onCancelCallback: () => void; - private readonly searchInput: Input; - private readonly listContainer: Container; - private readonly hintText: Text; - private filtered: CuaModelInfo[]; - private selectedIndex = 0; - private readonly maxVisible: number; - - // Focusable: propagate to the search input so the hardware cursor (and IME - // composition) lands in the search box, per pi's container-with-input pattern. - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - this.searchInput.focused = value; - } - - constructor(config: ModelPickerConfig) { - super(); - this.tui = config.tui; - this.currentRef = config.currentRef; - this.allModels = sortModelsForPicker(config.items, config.currentRef); - this.onSelectCallback = config.onSelect; - this.onCancelCallback = config.onCancel; - this.maxVisible = config.maxVisible ?? PICKER_MAX_VISIBLE; - - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); - this.hintText = new Text(this.getHintText(), 0, 0); - this.addChild(this.hintText); - this.addChild(new Spacer(1)); - - this.searchInput = new Input(); - if (config.initialSearch) this.searchInput.setValue(config.initialSearch); - this.searchInput.onSubmit = () => { - const item = this.filtered[this.selectedIndex]; - if (item) this.handleSelect(item); - }; - this.addChild(this.searchInput); - this.addChild(new Spacer(1)); - - this.listContainer = new Container(); - this.addChild(this.listContainer); - this.addChild(new Spacer(1)); - this.addChild(new DynamicBorder()); - - this.filtered = filterModelsForPicker(this.allModels, config.initialSearch ?? ""); - const currentIndex = this.filtered.findIndex((item) => item.ref === this.currentRef); - this.selectedIndex = currentIndex >= 0 ? currentIndex : clampSelection(0, this.filtered.length); - this.updateList(); - } - - /** - * Themed strings are baked into child `Text` nodes, so a theme change has - * to rebuild them. pi's own model selector omits this; cua does it so the - * picker repaints correctly. - */ - override invalidate(): void { - super.invalidate(); - this.hintText.setText(this.getHintText()); - this.updateList(); - } - - private getHintText(): string { - return colors.warning( - "Showing every CUA-capable model. The provider's API key must be set; run `cua models` for the catalog.", - ); - } - - private filterModels(query: string): void { - this.filtered = filterModelsForPicker(this.allModels, query); - this.selectedIndex = clampSelection(this.selectedIndex, this.filtered.length); - this.updateList(); - } - - private updateList(): void { - this.listContainer.clear(); - const { start, end } = visibleWindow(this.selectedIndex, this.filtered.length, this.maxVisible); - for (let i = start; i < end; i += 1) { - const item = this.filtered[i]; - if (!item) continue; - const isSelected = i === this.selectedIndex; - const badge = colors.muted(`[${item.provider}]`); - const check = item.ref === this.currentRef ? colors.success(" ✓") : ""; - const label = isSelected ? colors.accent("→ ") + colors.accent(item.model) : ` ${item.model}`; - this.listContainer.addChild(new Text(`${label} ${badge}${check}`, 0, 0)); - } - if (start > 0 || end < this.filtered.length) { - this.listContainer.addChild( - new Text(colors.muted(` (${this.selectedIndex + 1}/${this.filtered.length})`), 0, 0), - ); - } - if (this.filtered.length === 0) { - this.listContainer.addChild(new Text(colors.muted(" No matching models"), 0, 0)); - } else { - const selected = this.filtered[this.selectedIndex]; - this.listContainer.addChild(new Spacer(1)); - this.listContainer.addChild(new Text(colors.muted(` Model Name: ${selected?.name ?? ""}`), 0, 0)); - this.listContainer.addChild(new Text(colors.muted(` Ref: ${selected?.ref ?? ""}`), 0, 0)); - } - } - - handleInput(data: string): void { - const kb = getKeybindings(); - if (kb.matches(data, "tui.select.up")) { - if (this.filtered.length === 0) return; - this.selectedIndex = moveSelection(this.selectedIndex, -1, this.filtered.length); - this.updateList(); - } else if (kb.matches(data, "tui.select.down")) { - if (this.filtered.length === 0) return; - this.selectedIndex = moveSelection(this.selectedIndex, 1, this.filtered.length); - this.updateList(); - } else if (kb.matches(data, "tui.select.confirm")) { - const item = this.filtered[this.selectedIndex]; - if (item) this.handleSelect(item); - return; - } else if (kb.matches(data, "tui.select.cancel")) { - this.onCancelCallback(); - return; - } else { - this.searchInput.handleInput(data); - this.filterModels(this.searchInput.getValue()); - } - this.tui.requestRender(); - } - - private handleSelect(item: CuaModelInfo): void { - this.onSelectCallback(item.ref); - } -} diff --git a/packages/cli/src/tui/mutation-queue.ts b/packages/cli/src/tui/mutation-queue.ts deleted file mode 100644 index 2f22a264..00000000 --- a/packages/cli/src/tui/mutation-queue.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Serializes catalog mutations initiated from the TUI. - * - * Both mutations a selector can trigger — a `/tools` apply and a `/model` - * switch — suspend across several `setTools()`/`setModel()` calls. Run - * concurrently, an apply's `setTools()` can land between a switch's - * `setModel()` and its final `setTools()` and fail to compile against the wrong - * provider, aborting an otherwise valid switch. Funnelling both through one - * queue removes the interleaving entirely. - */ -export interface MutationQueue { - /** - * Run `fn` only after every previously queued mutation has settled. The - * returned promise mirrors `fn`'s outcome, so callers keep their own error - * handling; the queue itself never rejects, so one failure cannot wedge it. - */ - run(fn: () => Promise): Promise; - /** Resolves once the queue is idle. Test and shutdown aid. */ - drain(): Promise; -} - -export function createMutationQueue(): MutationQueue { - let chain: Promise = Promise.resolve(); - return { - run(fn: () => Promise): Promise { - const result = chain.then(fn); - chain = result.then( - () => undefined, - () => undefined, - ); - return result; - }, - drain(): Promise { - return chain; - }, - }; -} diff --git a/packages/cli/src/tui/screenshot-widget.ts b/packages/cli/src/tui/screenshot-widget.ts deleted file mode 100644 index 34cfac6a..00000000 --- a/packages/cli/src/tui/screenshot-widget.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Container, Image, allocateImageId } from "@earendil-works/pi-tui"; -import { imageTheme } from "./themes"; - -const MAX_WIDTH_CELLS = 60; - -/** - * Sticky screenshot widget that re-renders the latest tool screenshot - * inline using pi-tui's terminal-image (Kitty / iTerm2). Falls back to - * a compact text card on terminals without inline image support. - */ -export class ScreenshotWidget extends Container { - private readonly imageId = allocateImageId(); - - clear(): void { - this.children = []; - this.invalidate(); - } - - update(pngBase64: string, mimeType = "image/png"): void { - const image = new Image(pngBase64, mimeType, imageTheme, { - maxWidthCells: MAX_WIDTH_CELLS, - imageId: this.imageId, - }); - this.children = [image]; - this.invalidate(); - } -} diff --git a/packages/cli/src/tui/slash-commands.ts b/packages/cli/src/tui/slash-commands.ts deleted file mode 100644 index c40a8472..00000000 --- a/packages/cli/src/tui/slash-commands.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { - type AutocompleteItem, - CombinedAutocompleteProvider, - type SlashCommand, -} from "@earendil-works/pi-tui"; -import type { Skill } from "@onkernel/cua-agent"; -import { listCuaModels } from "@onkernel/cua-ai"; - -/** - * Build an autocomplete provider for the TUI editor with the slash commands - * the interactive app supports: `/model`, `/tools`, `/thinking`, `/compact`, - * plus a `/skill:` entry per loaded skill. - * - * Model and thinking values are exposed as `getArgumentCompletions` so - * users can tab through CUA refs and reasoning levels. - */ -export function buildAutocompleteProvider( - cwd: string, - skills: Skill[], -): CombinedAutocompleteProvider { - const commands: SlashCommand[] = []; - - commands.push({ - name: "model", - description: "Switch the active CUA model (no argument opens the picker)", - argumentHint: "", - getArgumentCompletions: (prefix: string) => modelCompletions(prefix), - }); - - commands.push({ - name: "tools", - description: "Enable/disable model-callable tools for this session", - }); - - commands.push({ - name: "thinking", - description: "Set the reasoning level for future turns", - argumentHint: "", - getArgumentCompletions: (prefix: string) => thinkingCompletions(prefix), - }); - - commands.push({ - name: "compact", - description: "Summarize older turns to free context budget", - }); - - for (const skill of skills) { - commands.push({ - name: `skill:${skill.name}`, - description: skill.description, - }); - } - - return new CombinedAutocompleteProvider(commands, cwd); -} - -function modelCompletions(prefix: string): AutocompleteItem[] { - const all = listCuaModels(); - const trimmed = prefix.trim().toLowerCase(); - const filtered = trimmed - ? all.filter((m) => m.ref.toLowerCase().includes(trimmed) || m.model.toLowerCase().includes(trimmed)) - : all; - return filtered.map((m) => ({ value: m.ref, label: m.ref, description: m.name })); -} - -const THINKING_LEVELS: ReadonlyArray<{ value: string; description: string }> = [ - { value: "off", description: "Disable reasoning" }, - { value: "minimal", description: "Minimal reasoning" }, - { value: "low", description: "Low reasoning (default)" }, - { value: "medium", description: "Medium reasoning" }, - { value: "high", description: "High reasoning" }, - { value: "xhigh", description: "Maximum reasoning (selected models only)" }, -]; - -function thinkingCompletions(prefix: string): AutocompleteItem[] { - const trimmed = prefix.trim().toLowerCase(); - const filtered = trimmed ? THINKING_LEVELS.filter((t) => t.value.startsWith(trimmed)) : THINKING_LEVELS; - return filtered.map((t) => ({ value: t.value, label: t.value, description: t.description })); -} - -export type ParsedSlashCommand = - | { command: "model"; argument: string } - | { command: "tools"; argument: string } - | { command: "thinking"; argument: string } - | { command: "compact"; argument: string } - | { command: "skill"; name: string; remainder: string }; - -/** - * Recognize the supported slash-command forms. Returns undefined when the - * text is a regular user prompt. - */ -export function parseSlashCommand(text: string): ParsedSlashCommand | undefined { - const trimmed = text.trim(); - if (!trimmed.startsWith("/")) return undefined; - const skillMatch = trimmed.match(/^\/skill:([A-Za-z0-9_\-.]+)\s*(.*)$/); - if (skillMatch) { - const [, name, rest] = skillMatch; - return { command: "skill", name: name ?? "", remainder: (rest ?? "").trim() }; - } - const builtinMatch = trimmed.match(/^\/(model|tools|thinking|compact)\s*(.*)$/); - if (builtinMatch) { - const [, name, rest] = builtinMatch; - return { - command: name as "model" | "tools" | "thinking" | "compact", - argument: (rest ?? "").trim(), - }; - } - return undefined; -} diff --git a/packages/cli/src/tui/status-line.ts b/packages/cli/src/tui/status-line.ts deleted file mode 100644 index 7867f418..00000000 --- a/packages/cli/src/tui/status-line.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Text, hyperlink } from "@earendil-works/pi-tui"; -import { colors } from "./themes"; - -export interface StatusLineState { - model: string; - browserSession?: string; - liveUrl?: string; - currentUrl?: string; - cost?: number; - tokens?: number; - working?: string; -} - -export class StatusLine extends Text { - private state: StatusLineState; - - constructor(initial: StatusLineState) { - super("", 0, 0); - this.state = initial; - this.refresh(); - } - - update(patch: Partial): void { - this.state = { ...this.state, ...patch }; - this.refresh(); - } - - private refresh(): void { - const sep = colors.dim(" · "); - const parts: string[] = [colors.bold("cua")]; - if (this.state.liveUrl) { - parts.push(colors.dim("browser ") + hyperlink(this.state.liveUrl, this.state.liveUrl)); - } else if (this.state.browserSession) { - parts.push(colors.dim("browser ") + this.state.browserSession.slice(0, 6) + "…"); - } - if (this.state.currentUrl) parts.push(colors.dim("url ") + truncate(this.state.currentUrl, 50)); - if (this.state.tokens !== undefined) parts.push(colors.dim("tokens ") + this.state.tokens.toLocaleString()); - if (this.state.cost !== undefined) parts.push(colors.dim("$") + this.state.cost.toFixed(3)); - if (this.state.working) parts.push(colors.warning(`⏳ ${this.state.working}`)); - this.setText(parts.join(sep)); - } -} - -function truncate(text: string, max: number): string { - if (text.length <= max) return text; - return text.slice(0, max - 1) + "…"; -} diff --git a/packages/cli/src/tui/telemetry-footer.ts b/packages/cli/src/tui/telemetry-footer.ts deleted file mode 100644 index aedc8f1e..00000000 --- a/packages/cli/src/tui/telemetry-footer.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; -import { colors } from "./themes"; - -export interface TelemetryFooterState { - provider?: string; - model?: string; - thinkingLevel?: string; - contextTokens?: number; - contextWindow?: number; -} - -export class TelemetryFooter implements Component { - private state: TelemetryFooterState; - - constructor(initial: TelemetryFooterState) { - this.state = initial; - } - - update(patch: Partial): void { - this.state = { ...this.state, ...patch }; - } - - invalidate(): void {} - - render(width: number): string[] { - const left = this.renderContextUsage(); - const right = this.renderModelInfo(); - - if (!left && !right) return [" ".repeat(width)]; - if (!left) return [padToWidth(truncateToWidth(right, width), width)]; - if (!right) return [padToWidth(truncateToWidth(left, width), width)]; - - const rightWidth = visibleWidth(right); - if (rightWidth >= width) { - return [padToWidth(truncateToWidth(right, width), width)]; - } - - const gap = 1; - const leftMaxWidth = Math.max(1, width - rightWidth - gap); - const leftText = truncateToWidth(left, leftMaxWidth); - const spaces = " ".repeat(Math.max(gap, width - visibleWidth(leftText) - rightWidth)); - return [padToWidth(leftText + spaces + right, width)]; - } - - private renderContextUsage(): string { - if (!this.state.contextWindow || this.state.contextWindow <= 0) { - return ""; - } - - const used = Math.max(0, this.state.contextTokens ?? 0); - const percent = this.state.contextWindow > 0 ? ((used / this.state.contextWindow) * 100).toFixed(1) : "?"; - return colors.dim(`${percent}%/${formatTokens(this.state.contextWindow)}`); - } - - private renderModelInfo(): string { - const modelLabel = - this.state.provider && this.state.model ? `${this.state.provider}/${this.state.model}` : this.state.model ?? ""; - if (!modelLabel) return ""; - - const thinking = - this.state.thinkingLevel && this.state.thinkingLevel.length > 0 - ? this.state.thinkingLevel === "off" - ? "thinking off" - : this.state.thinkingLevel - : ""; - if (!thinking) { - return colors.dim(modelLabel); - } - return colors.dim(modelLabel) + colors.dim(" • ") + colors.dim(thinking); - } -} - -function formatTokens(tokens: number): string { - if (tokens >= 1_000_000) { - const millions = tokens / 1_000_000; - return `${trimFraction(millions)}M`; - } - if (tokens >= 1_000) { - const thousands = tokens / 1_000; - return `${trimFraction(thousands)}K`; - } - return Math.round(tokens).toString(); -} - -function trimFraction(value: number): string { - const rounded = value >= 10 ? value.toFixed(0) : value.toFixed(1); - return rounded.endsWith(".0") ? rounded.slice(0, -2) : rounded; -} - -function padToWidth(text: string, width: number): string { - const pad = Math.max(0, width - visibleWidth(text)); - return text + " ".repeat(pad); -} diff --git a/packages/cli/src/tui/themes.ts b/packages/cli/src/tui/themes.ts deleted file mode 100644 index 1f648bb7..00000000 --- a/packages/cli/src/tui/themes.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { EditorTheme, ImageTheme } from "@earendil-works/pi-tui"; -import { getMarkdownTheme, getSelectListTheme, Theme } from "@earendil-works/pi-coding-agent"; - -/** - * cua's TUI styling rides on pi's theme system so it matches pi's own TUI. - * `initTheme()` must run once at TUI startup (see `tui/main.ts`) before any of - * these helpers are used. - * - * pi exports the `Theme` class and the markdown/select-list theme getters, but - * not the live theme instance behind `theme.fg(...)`. That instance is published - * on a `Symbol.for` global key (pi's cross-realm contract for its own `theme` - * proxy), so we read it back here to colorize text with the active palette. - */ -const THEME_KEY = Symbol.for("@earendil-works/pi-coding-agent:theme"); - -function activeTheme(): Theme { - const instance = (globalThis as Record)[THEME_KEY]; - if (!(instance instanceof Theme)) { - throw new Error("pi theme not initialized; call initTheme() before rendering the TUI"); - } - return instance; -} - -/** - * The small palette cua's components reach for, mapped onto pi theme colors so - * existing call sites keep working while picking up pi's palette. - */ -export const colors = { - dim: (text: string) => activeTheme().fg("dim", text), - bold: (text: string) => activeTheme().bold(text), - accent: (text: string) => activeTheme().fg("accent", text), - muted: (text: string) => activeTheme().fg("muted", text), - heading: (text: string) => activeTheme().fg("mdHeading", text), - success: (text: string) => activeTheme().fg("success", text), - error: (text: string) => activeTheme().fg("error", text), - warning: (text: string) => activeTheme().fg("warning", text), -}; - -export { getMarkdownTheme }; - -/** pi has no exported editor theme; compose one from its select-list theme. */ -export function getEditorTheme(): EditorTheme { - return { - borderColor: (text) => activeTheme().fg("borderAccent", text), - selectList: getSelectListTheme(), - }; -} - -/** pi has no image theme; only the text fallback color is cua-specific. */ -export const imageTheme: ImageTheme = { - fallbackColor: (text) => activeTheme().fg("dim", text), -}; diff --git a/packages/cli/src/tui/tool-selection.ts b/packages/cli/src/tui/tool-selection.ts deleted file mode 100644 index 58d4d076..00000000 --- a/packages/cli/src/tui/tool-selection.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { callerToolIdentity, cuaToolMenu, isCuaToolSpec, type CuaModelRef, type CuaToolSpec } from "@onkernel/cua-ai"; -import type { CuaCliTool } from "../harness"; - -/** Where a tool came from, used purely as a display badge. */ -export type ToolGroup = "native" | "browser" | "computer" | "playwright" | "application"; - -/** One row in the `/tools` picker, derived from a caller-owned tool. */ -export interface ToolSelectionItem { - /** - * Stable key matching the catalog compiler's identity scheme in - * `@onkernel/cua-ai`: a spec's own `identity`, or `callerToolIdentity(name)` - * for a plain pi `AgentTool`. - */ - key: string; - /** Model-facing tool name. */ - label: string; - group: ToolGroup; - description?: string; - /** Whether selecting this row produces a catalog the model accepts. */ - available: boolean; - /** Why it cannot be selected, from the catalog compiler. */ - unavailableReason?: string; - /** The tools this row contributes when enabled. */ - tools: readonly CuaCliTool[]; -} - -/** Identity key for a caller-owned tool, using cua-ai's canonical identity helper. */ -export function toolKey(tool: CuaCliTool): string { - return isCuaToolSpec(tool) ? tool.identity : callerToolIdentity(tool.name); -} - - - -function toolDescription(tool: CuaCliTool): string | undefined { - const raw = isCuaToolSpec(tool) ? tool.declaration.description : tool.description; - if (typeof raw !== "string") return undefined; - const firstLine = raw.trim().split("\n")[0]?.trim(); - return firstLine || undefined; -} - -/** - * Describe everything selectable for a model: CUA's whole tool menu, then the - * application's own tools. - * - * Availability comes from `cuaToolMenu`, which decides it by compiling the - * resulting catalog, so a row shown as available is one `harness.setTools()` - * will accept. Some of those rules are pairwise — two providers' native - * surfaces cannot coexist — so this is rebuilt against each staged selection - * rather than computed once. - */ -export function describeMenu( - model: CuaModelRef, - applicationTools: readonly CuaCliTool[], - selectedTools: readonly CuaCliTool[], -): ToolSelectionItem[] { - const selectedSpecs = selectedTools.filter(isCuaToolSpec); - // A live spec may carry options the menu's freshly built one does not (the - // CLI enables `javascript` on Anthropic's native browser, for instance), so - // a row that is already installed contributes the exact object in use. - // Otherwise a no-op apply would quietly rebuild the catalog without them. - const live = new Map(selectedSpecs.map((tool) => [tool.identity, tool] as const)); - const items: ToolSelectionItem[] = cuaToolMenu(model, selectedSpecs).map((entry) => ({ - key: entry.key, - label: entry.label, - group: entry.group, - ...(entry.description ? { description: entry.description } : {}), - available: entry.available, - ...(entry.unavailableReason ? { unavailableReason: entry.unavailableReason } : {}), - tools: entry.tools.map((tool) => live.get(tool.identity) ?? tool) as readonly CuaCliTool[], - })); - for (const tool of applicationTools) { - const description = toolDescription(tool); - items.push({ - key: toolKey(tool), - label: tool.name, - group: "application", - ...(description ? { description } : {}), - available: true, - tools: [tool], - }); - } - return items; -} - -/** The exact tool list a staged selection produces, in menu order. */ -export function toolsForSelection(items: readonly ToolSelectionItem[], enabled: ReadonlySet): CuaCliTool[] { - return items.filter((item) => enabled.has(item.key)).flatMap((item) => [...item.tools]); -} - -/** Keys currently satisfied by a live tool list, for seeding the picker. */ -export function selectedKeys(items: readonly ToolSelectionItem[], tools: readonly CuaCliTool[]): Set { - const present = new Set(tools.map(toolKey)); - return new Set(items.filter((item) => item.tools.every((tool) => present.has(toolKey(tool)))).map((item) => item.key)); -} - -/** Search text for the `/tools` filter: name, group badge, and description. */ -export function toolSearchText(item: ToolSelectionItem): string { - return `${item.label} ${item.group} ${item.key}${item.description ? ` ${item.description}` : ""}`; -} - -/** Flip one row. */ -export function toggleTool(enabled: ReadonlySet, key: string): Set { - const next = new Set(enabled); - if (enabled.has(key)) next.delete(key); - else next.add(key); - return next; -} - -/** Enable `keys`. */ -export function enableTools(enabled: ReadonlySet, keys: readonly string[]): Set { - const next = new Set(enabled); - for (const key of keys) next.add(key); - return next; -} - -/** Disable `keys`. */ -export function disableTools(enabled: ReadonlySet, keys: readonly string[]): Set { - const next = new Set(enabled); - for (const key of keys) next.delete(key); - return next; -} - -/** True when both sets hold exactly the same keys. */ -export function sameSelection(a: ReadonlySet, b: ReadonlySet): boolean { - if (a.size !== b.size) return false; - for (const key of a) if (!b.has(key)) return false; - return true; -} diff --git a/packages/cli/src/tui/tools-picker.ts b/packages/cli/src/tui/tools-picker.ts deleted file mode 100644 index 9d412c32..00000000 --- a/packages/cli/src/tui/tools-picker.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { - Container, - type Focusable, - fuzzyFilter, - getKeybindings, - Input, - Key, - matchesKey, - Spacer, - Text, - type TUI, -} from "@earendil-works/pi-tui"; -import { DynamicBorder } from "@earendil-works/pi-coding-agent"; -import { cuaKeyText } from "./keybindings"; -import { clampSelection, moveSelection, PICKER_MAX_VISIBLE, visibleWindow } from "./model-picker"; -import { colors } from "./themes"; -import { disableTools, enableTools, sameSelection, toggleTool, toolSearchText, type ToolSelectionItem } from "./tool-selection"; - -export interface ToolsPickerConfig { - tui: TUI; - /** Baseline rows: every tool the application composed for this model. */ - items: readonly ToolSelectionItem[]; - /** Keys currently live on the harness. */ - enabledKeys: ReadonlySet; - /** The model defaults `ctrl+r` restores. */ - defaultKeys: ReadonlySet; - /** - * Rebuild the menu for a staged selection. Availability is pairwise — two - * providers' native surfaces cannot coexist, and a native surface pins the - * transport — so rows are re-evaluated after every toggle rather than fixed - * when the picker opens. - */ - restage?: (staged: ReadonlySet) => ToolSelectionItem[]; - /** Fired on apply only. Never called on cancel. */ - onApply: (enabled: ReadonlySet) => void; - onCancel: () => void; - /** List height; defaults to 10. See `fitMaxVisible`. */ - maxVisible?: number; -} - -/** - * Session-local tool enable/disable picker, shaped after pi's - * `ScopedModelsSelectorComponent` (the closest analogue: multi-select, - * session-only, ✓/✗ rows, bulk actions, an "N/M enabled" footer, and a - * `Focusable` search input that keeps the hardware cursor correct). - * - * Edits are staged: nothing reaches `harness.setTools()` until the user - * applies, and cancelling discards the staging set so live state is untouched. - */ -export class ToolsPickerComponent extends Container implements Focusable { - private readonly tui: TUI; - private items: readonly ToolSelectionItem[]; - private readonly liveKeys: ReadonlySet; - private readonly defaultKeys: ReadonlySet; - private readonly onApplyCallback: (enabled: ReadonlySet) => void; - private readonly restage?: (staged: ReadonlySet) => ToolSelectionItem[]; - private readonly onCancelCallback: () => void; - private readonly searchInput: Input; - private readonly listContainer: Container; - private readonly titleText: Text; - private readonly subtitleText: Text; - private readonly footerText: Text; - private staged: Set; - private filtered: readonly ToolSelectionItem[]; - private selectedIndex = 0; - private readonly maxVisible: number; - - private _focused = false; - get focused(): boolean { - return this._focused; - } - set focused(value: boolean) { - this._focused = value; - this.searchInput.focused = value; - } - - constructor(config: ToolsPickerConfig) { - super(); - this.tui = config.tui; - this.items = config.items; - this.restage = config.restage; - this.liveKeys = new Set(config.enabledKeys); - this.defaultKeys = new Set(config.defaultKeys); - this.onApplyCallback = config.onApply; - this.onCancelCallback = config.onCancel; - this.staged = new Set(config.enabledKeys); - this.filtered = this.items; - this.maxVisible = config.maxVisible ?? PICKER_MAX_VISIBLE; - - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); - this.titleText = new Text(this.getTitleText(), 0, 0); - this.addChild(this.titleText); - this.subtitleText = new Text(this.getSubtitleText(), 0, 0); - this.addChild(this.subtitleText); - this.addChild(new Spacer(1)); - - this.searchInput = new Input(); - this.addChild(this.searchInput); - this.addChild(new Spacer(1)); - - this.listContainer = new Container(); - this.addChild(this.listContainer); - this.addChild(new Spacer(1)); - this.footerText = new Text(this.getFooterText(), 0, 0); - this.addChild(this.footerText); - this.addChild(new DynamicBorder()); - - this.updateList(); - } - - /** Rebuild pre-baked themed strings when the theme changes. */ - override invalidate(): void { - super.invalidate(); - this.titleText.setText(this.getTitleText()); - this.subtitleText.setText(this.getSubtitleText()); - this.footerText.setText(this.getFooterText()); - this.updateList(); - } - - private getTitleText(): string { - return colors.accent(colors.bold("Tool Configuration")); - } - - private getSubtitleText(): string { - return colors.muted("Session-only. Model-callable tools for this session; resets on /model."); - } - - private isDirty(): boolean { - return !sameSelection(this.staged, this.liveKeys); - } - - private getFooterText(): string { - // `space` is only a toggle while the search box is empty; drop it from the - // hint once typing has claimed it. - const confirm = cuaKeyText("tui.select.confirm"); - const toggleKeys = this.searchInput.getValue() ? confirm : `${confirm}/space`; - const parts = [ - `${toggleKeys} toggle`, - `${cuaKeyText("cua.tools.enableAll")} all`, - `${cuaKeyText("cua.tools.clearAll")} none`, - `${cuaKeyText("cua.tools.reset")} defaults`, - `${cuaKeyText("cua.tools.apply")} apply`, - `${cuaKeyText("tui.select.cancel")} cancel`, - `${this.staged.size}/${this.items.filter((item) => item.available).length} enabled`, - ]; - const line = colors.dim(` ${parts.join(" · ")}`); - if (this.staged.size === 0) return `${line} ${colors.warning("(text-only agent)")}`; - return this.isDirty() ? `${line} ${colors.warning("(unapplied)")}` : line; - } - - private refresh(): void { - const query = this.searchInput.getValue(); - this.filtered = query ? fuzzyFilter([...this.items], query, toolSearchText) : this.items; - this.selectedIndex = clampSelection(this.selectedIndex, this.filtered.length); - this.updateList(); - this.footerText.setText(this.getFooterText()); - } - - private updateList(): void { - this.listContainer.clear(); - if (this.filtered.length === 0) { - this.listContainer.addChild(new Text(colors.muted(" No matching tools"), 0, 0)); - return; - } - const { start, end } = visibleWindow(this.selectedIndex, this.filtered.length, this.maxVisible); - for (let i = start; i < end; i += 1) { - const item = this.filtered[i]; - if (!item) continue; - const isSelected = i === this.selectedIndex; - const prefix = isSelected ? colors.accent("→ ") : " "; - const label = isSelected ? colors.accent(item.label) : item.label; - const badge = colors.muted(` [${item.group}]`); - const status = !item.available - ? colors.muted(" — unavailable") - : this.staged.has(item.key) - ? colors.success(" ✓ enabled") - : colors.dim(" ✗ disabled"); - this.listContainer.addChild(new Text(`${prefix}${label}${badge}${status}`, 0, 0)); - } - if (start > 0 || end < this.filtered.length) { - this.listContainer.addChild( - new Text(colors.muted(` (${this.selectedIndex + 1}/${this.filtered.length})`), 0, 0), - ); - } - const selected = this.filtered[this.selectedIndex]; - if (selected) { - this.listContainer.addChild(new Spacer(1)); - this.listContainer.addChild(new Text(colors.muted(` ${selected.key}`), 0, 0)); - if (selected.description) { - this.listContainer.addChild(new Text(colors.muted(` ${selected.description}`), 0, 0)); - } - if (!selected.available && selected.unavailableReason) { - this.listContainer.addChild(new Text(colors.warning(` ${selected.unavailableReason}`), 0, 0)); - } - } - } - - /** Bulk actions honour an active search filter, as pi's selector does. */ - private bulkTargets(): string[] { - const scope = this.searchInput.getValue() ? this.filtered : this.items; - return scope.filter((item) => item.available).map((item) => item.key); - } - - /** Re-evaluate availability against the staged selection, dropping rows that no longer compile. */ - private restageItems(): void { - if (!this.restage) return; - this.items = this.restage(this.staged); - const selectable = new Set(this.items.filter((item) => item.available).map((item) => item.key)); - this.staged = new Set([...this.staged].filter((key) => selectable.has(key))); - } - - handleInput(data: string): void { - const kb = getKeybindings(); - - if (kb.matches(data, "tui.select.up")) { - if (this.filtered.length === 0) return; - this.selectedIndex = moveSelection(this.selectedIndex, -1, this.filtered.length); - this.updateList(); - this.tui.requestRender(); - return; - } - if (kb.matches(data, "tui.select.down")) { - if (this.filtered.length === 0) return; - this.selectedIndex = moveSelection(this.selectedIndex, 1, this.filtered.length); - this.updateList(); - this.tui.requestRender(); - return; - } - // Space toggles only while the search box is empty, so a multi-word query - // (descriptions are searchable) stays typeable. - if (kb.matches(data, "tui.select.confirm") || (data === " " && !this.searchInput.getValue())) { - const item = this.filtered[this.selectedIndex]; - if (item && (item.available || this.staged.has(item.key))) { - this.staged = toggleTool(this.staged, item.key); - this.restageItems(); - this.refresh(); - this.tui.requestRender(); - } - return; - } - if (kb.matches(data, "cua.tools.enableAll")) { - this.staged = enableTools(this.staged, this.bulkTargets()); - this.restageItems(); - this.refresh(); - this.tui.requestRender(); - return; - } - if (kb.matches(data, "cua.tools.clearAll")) { - this.staged = disableTools(this.staged, this.bulkTargets()); - this.restageItems(); - this.refresh(); - this.tui.requestRender(); - return; - } - if (kb.matches(data, "cua.tools.reset")) { - this.staged = new Set(this.defaultKeys); - this.restageItems(); - this.refresh(); - this.tui.requestRender(); - return; - } - if (kb.matches(data, "cua.tools.apply")) { - this.onApplyCallback(new Set(this.staged)); - return; - } - // Honour whatever `tui.select.cancel` is bound to (default escape/ctrl+c) so - // the footer hint and the handler can never disagree. Within that binding, - // ctrl+c clears an active search before cancelling, like pi's selector; - // escape always cancels outright. - if (kb.matches(data, "tui.select.cancel")) { - if (matchesKey(data, Key.ctrl("c")) && this.searchInput.getValue()) { - this.searchInput.setValue(""); - this.refresh(); - this.tui.requestRender(); - } else { - this.onCancelCallback(); - } - return; - } - this.searchInput.handleInput(data); - this.refresh(); - this.tui.requestRender(); - } - - getSearchInput(): Input { - return this.searchInput; - } -} diff --git a/packages/cli/src/tui/version.ts b/packages/cli/src/tui/version.ts deleted file mode 100644 index 438af517..00000000 --- a/packages/cli/src/tui/version.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * cua's version, inlined by tsdown's `define` (see tsdown.config.ts) so the - * bundled bin never reads package.json from disk at runtime. When the source - * runs unbundled (e.g. tests via tsx), the define isn't applied and this falls - * back to "dev". - */ -declare const __CUA_VERSION__: string | undefined; - -export function cuaVersion(): string { - return typeof __CUA_VERSION__ === "string" ? __CUA_VERSION__ : "dev"; -} diff --git a/packages/cli/test/action-runner.test.ts b/packages/cli/test/action-runner.test.ts deleted file mode 100644 index 8d973250..00000000 --- a/packages/cli/test/action-runner.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { cua } from "@onkernel/cua-ai"; -import { describe, expect, it, vi } from "vitest"; -import { runAction } from "../src/action/harness-runner"; -import { buildTestHarness as buildDefaultTestHarness, type TestHarnessFixture } from "./fixtures/harness"; - -const buildTestHarness = (options: Parameters[0]) => - buildDefaultTestHarness({ ...options, tools: cua.toolsets.computer() }); - -let fixture: TestHarnessFixture | undefined; - -describe("action harness-runner", () => { - it("exits 0 with formatted result when a click action succeeds", async () => { - fixture = await buildTestHarness({ - turns: [ - { - steps: [ - { - type: "tool_call", - toolName: "computer_click", - args: { x: 123, y: 45 }, - }, - ], - }, - { - steps: [{ type: "text", text: "clicked" }], - }, - ], - }); - const res = await runAction( - { action: "click", target: "the button" }, - { harness: fixture.harness, maxTurns: 5 }, - ); - expect(res.exitCode).toBe(0); - expect(res.result.coordinates).toEqual([123, 45]); - expect(res.result.action).toBe("click"); - }); - - it("exits 1 when the model says NOT_FOUND", async () => { - fixture = await buildTestHarness({ - turns: [ - { - steps: [{ type: "text", text: "NOT_FOUND: no match" }], - }, - ], - }); - const res = await runAction( - { action: "click", target: "missing" }, - { harness: fixture.harness, maxTurns: 5 }, - ); - expect(res.exitCode).toBe(1); - expect(res.result.status).toBe("not_found"); - expect(res.result.text).toBe("no match"); - }); - - it("exits 2 when the provider returns an error", async () => { - fixture = await buildTestHarness({ - turns: [{ steps: [{ type: "error", message: "boom" }] }], - }); - const res = await runAction( - { action: "do", text: "fail" }, - { harness: fixture.harness, maxTurns: 3 }, - ); - expect(res.exitCode).toBe(2); - expect(res.result.status).toBe("error"); - expect(res.result.text).toContain("boom"); - }); - - it("retries a transient provider error before completing the action", async () => { - vi.useFakeTimers(); - try { - fixture = await buildTestHarness({ - turns: [ - { steps: [{ type: "tool_call", toolName: "computer_click", args: { x: 9, y: 9 } }] }, - { - steps: [ - { type: "text", text: "discarded" }, - { type: "error", message: "HTTP 429: Please retry in 10.367614288s" }, - ], - }, - { steps: [{ type: "text", text: "done" }] }, - ], - retry: { enabled: true }, - }); - const resultPromise = runAction( - { action: "do", text: "recover" }, - { harness: fixture.harness, maxTurns: 3 }, - ); - - await vi.advanceTimersByTimeAsync(1_999); - expect(fixture.provider.callCount()).toBe(2); - await vi.advanceTimersByTimeAsync(1); - const res = await resultPromise; - - expect(fixture.provider.callCount()).toBe(3); - expect(res.exitCode).toBe(0); - expect(res.result.status).toBe("ok"); - expect(fixture.kernel.batchCalls).toHaveLength(1); - expect(JSON.stringify(fixture.provider.lastContext())).toContain("toolResult"); - const messages = (await fixture.session.getBranch()).filter((entry) => entry.type === "message"); - expect(messages).toHaveLength(4); - expect(JSON.stringify(messages)).toContain("done"); - expect(JSON.stringify(messages)).not.toContain("discarded"); - } finally { - vi.useRealTimers(); - } - }); - - it("invokes harness.abort once the turn cap is reached", async () => { - const toolCall = { - steps: [ - { - type: "tool_call" as const, - toolName: "computer_click", - args: { x: 1, y: 1 }, - }, - ], - }; - fixture = await buildTestHarness({ - turns: Array.from({ length: 10 }, () => toolCall), - }); - // Spy on harness.abort so we don't depend on the scripted provider - // honouring the abort signal (it runs synchronously below the loop). - let abortCalls = 0; - const originalAbort = fixture.harness.abort.bind(fixture.harness); - fixture.harness.abort = async () => { - abortCalls += 1; - return originalAbort(); - }; - await runAction( - { action: "do", text: "loop" }, - { harness: fixture.harness, maxTurns: 2 }, - ); - expect(abortCalls).toBeGreaterThanOrEqual(1); - }); - - it("does not attach a screenshot to the first user message", async () => { - fixture = await buildTestHarness({ - turns: [{ steps: [{ type: "text", text: "ok" }] }], - }); - await runAction( - { action: "do", text: "look" }, - { harness: fixture.harness, maxTurns: 3 }, - ); - expect(fixture.kernel.screenshots).toBe(0); - const entries = await fixture.session.getBranch(); - const firstUser = entries.find((e) => e.type === "message" && e.message.role === "user"); - expect(firstUser).toBeDefined(); - const content = (firstUser as { message: { content: unknown[] } }).message.content; - expect(content.some((entry) => entry && typeof entry === "object" && (entry as { type?: unknown }).type === "image")).toBe(false); - }); -}); diff --git a/packages/cli/test/cli-executor.test.ts b/packages/cli/test/cli-executor.test.ts deleted file mode 100644 index 039860c8..00000000 --- a/packages/cli/test/cli-executor.test.ts +++ /dev/null @@ -1,522 +0,0 @@ -import type { - BrowserActResult, - BrowserExecutor, - BrowserFindCandidate, - BrowserRefState, - InternalComputerTranslator as Translator, -} from "@onkernel/cua-agent"; -import { InternalComputerTranslator } from "@onkernel/cua-agent"; -import type { CuaBrowserAction } from "@onkernel/cua-ai"; -import { mkdtempSync } from "node:fs"; -import { readFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { - deterministicActionFor, - isCoordinatePair, - parseDeterministicArgs, - runDeterministicCommand, - runDeterministicOnHandle, -} from "../src/cli-executor"; -import type { HarnessCliFlags } from "../src/cli-harness"; -import type { CuaBrowserHandle } from "../src/harness-browser"; -import { createFakeKernelEnvironment, type FakeKernelEnvironment } from "./fixtures/fake-kernel"; - -const PROVIDER_ENV_KEYS = [ - "OPENAI_API_KEY", - "ANTHROPIC_API_KEY", - "GOOGLE_API_KEY", - "GEMINI_API_KEY", - "META_API_KEY", - "XAI_API_KEY", - "MOONSHOT_API_KEY", -]; - -function baseFlags(overrides: Partial = {}): HarnessCliFlags { - return { - verbose: false, - profileSaveChanges: true, - continueLatest: false, - resumePicker: false, - noSession: false, - noSkills: false, - debugTui: false, - jsonlIncludeDeltas: false, - jsonlIncludeImages: false, - skillPaths: [], - ...overrides, - }; -} - -interface FakeExecutorState { - actions: CuaBrowserAction[]; - closed: number; - imported: BrowserRefState[]; - exported: number; -} - -interface FakeExecutorScript { - candidates?: BrowserFindCandidate[]; - url?: string; - texts?: Partial>; - actResult?: BrowserActResult; - failWith?: Error; -} - -const FAKE_REF_STATE: BrowserRefState = { - refCounter: 7, - generations: [["F0", 0]], - refs: [["e7", { backendNodeId: 42, targetId: "F0", frameId: "F0", generation: 0, role: "button", name: "Save", nth: 0, cohort: 1 }]], -}; - -function fakeExecutor(script: FakeExecutorScript = {}): { executor: BrowserExecutor; state: FakeExecutorState } { - const state: FakeExecutorState = { actions: [], closed: 0, imported: [], exported: 0 }; - const executor = { - async execute(action: CuaBrowserAction) { - if (script.failWith) throw script.failWith; - state.actions.push(action); - if (action.type === "browser_act" && script.actResult) { - return [{ type: "browser_act", result: script.actResult }]; - } - const text = script.texts?.[action.type]; - return text !== undefined ? [{ type: "browser_text", label: action.type, text }] : []; - }, - async findCandidates(_query: string, _tabId?: string, roles?: ReadonlySet) { - if (script.failWith) throw script.failWith; - const candidates = script.candidates ?? []; - return roles ? candidates.filter((c) => roles.has(c.role)) : candidates; - }, - async currentUrl() { - return script.url ?? ""; - }, - importRefState(refState: BrowserRefState) { - state.imported.push(refState); - }, - exportRefState(): BrowserRefState { - state.exported += 1; - return FAKE_REF_STATE; - }, - close() { - state.closed += 1; - }, - }; - return { executor: executor as unknown as BrowserExecutor, state }; -} - -interface TestSetup { - kernel: FakeKernelEnvironment; - handle: CuaBrowserHandle; - handleCloses: () => number; - createTranslator: (handle: CuaBrowserHandle) => Translator; - state: FakeExecutorState; -} - -function setup(script: FakeExecutorScript = {}): TestSetup { - const kernel = createFakeKernelEnvironment(); - let closes = 0; - const handle: CuaBrowserHandle = { - client: kernel.client, - browser: kernel.browser, - async close() { - closes += 1; - }, - }; - const { executor, state } = fakeExecutor(script); - const createTranslator = (h: CuaBrowserHandle) => - new InternalComputerTranslator({ browser: h.browser, client: h.client, createBrowserExecutor: () => executor }); - return { kernel, handle, handleCloses: () => closes, createTranslator, state }; -} - -let stdoutLines: string[] = []; -let originalWrite: typeof process.stdout.write; -let savedEnv: Record = {}; - -beforeEach(() => { - stdoutLines = []; - originalWrite = process.stdout.write.bind(process.stdout); - process.stdout.write = ((chunk: string | Uint8Array): boolean => { - stdoutLines.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("latin1")); - return true; - }) as typeof process.stdout.write; - savedEnv = {}; - for (const key of [...PROVIDER_ENV_KEYS, "KERNEL_API_KEY"]) { - savedEnv[key] = process.env[key]; - delete process.env[key]; - } -}); - -afterEach(() => { - process.stdout.write = originalWrite; - for (const [key, value] of Object.entries(savedEnv)) { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; - } -}); - -describe("deterministicActionFor", () => { - it("routes deterministic subcommands to the executor plane", () => { - expect(deterministicActionFor("url", [])).toBe("url"); - expect(deterministicActionFor("open", ["https://a"])).toBe("open"); - expect(deterministicActionFor("screenshot", [])).toBe("screenshot"); - expect(deterministicActionFor("act", ['{"steps":[{"type":"wait"}]}'])).toBe("act"); - expect(deterministicActionFor("click", ["10", "20"])).toBe("click"); - expect(deterministicActionFor("click", ["e12"])).toBe("click"); - }); - - it("leaves model-mediated and free-form argv alone", () => { - expect(deterministicActionFor("click", ["3", "dots", "menu"])).toBeUndefined(); - expect(deterministicActionFor("click", ["sign in button"])).toBeUndefined(); - expect(deterministicActionFor("click", ["e12x"])).toBeUndefined(); - expect(deterministicActionFor("click", ["e12", "e13"])).toBeUndefined(); - expect(deterministicActionFor("do", ["open hn"])).toBeUndefined(); - expect(deterministicActionFor("observe", [])).toBeUndefined(); - expect(deterministicActionFor("session", ["list"])).toBeUndefined(); - expect(deterministicActionFor(undefined, [])).toBeUndefined(); - }); -}); - -describe("isCoordinatePair", () => { - it("accepts exactly two integer tokens", () => { - expect(isCoordinatePair(["10", "20"])).toBe(true); - }); - - it("rejects descriptions, partial pairs, and non-integers", () => { - expect(isCoordinatePair(["3", "dots", "menu"])).toBe(false); - expect(isCoordinatePair(["10"])).toBe(false); - expect(isCoordinatePair(["10", "20px"])).toBe(false); - expect(isCoordinatePair([])).toBe(false); - }); -}); - -describe("parseDeterministicArgs", () => { - it("rejects invalid argv before any provisioning", () => { - expect(() => parseDeterministicArgs("open", [], baseFlags())).toThrow("usage: cua open"); - expect(() => parseDeterministicArgs("find", [], baseFlags())).toThrow("usage: cua find"); - expect(() => parseDeterministicArgs("fill", ["query"], baseFlags())).toThrow("usage: cua fill"); - expect(() => parseDeterministicArgs("press", [], baseFlags())).toThrow("usage: cua press"); - expect(() => parseDeterministicArgs("click", ["a", "b"], baseFlags())).toThrow("usage: cua click"); - expect(() => parseDeterministicArgs("act", [], baseFlags())).toThrow("usage: cua act"); - expect(() => parseDeterministicArgs("act", ["not-json"], baseFlags())).toThrow("invalid cua act JSON"); - expect(() => parseDeterministicArgs("act", ['{"steps":[]}'], baseFlags())).toThrow("invalid cua act input"); - expect(() => parseDeterministicArgs("snapshot", [], baseFlags({ filter: "everything" }))).toThrow( - "invalid --filter", - ); - }); - - it("rejects extra positionals on url, text, and tabs", () => { - expect(() => parseDeterministicArgs("url", ["extra"], baseFlags())).toThrow("usage: cua url"); - expect(() => parseDeterministicArgs("text", ["extra"], baseFlags())).toThrow("usage: cua text"); - expect(() => parseDeterministicArgs("tabs", ["extra"], baseFlags())).toThrow("usage: cua tabs"); - }); - - it("rejects --filter on subcommands other than snapshot", () => { - expect(() => parseDeterministicArgs("text", [], baseFlags({ filter: "interactive" }))).toThrow( - "--filter only applies to cua snapshot", - ); - }); - - it("accepts the documented forms", () => { - expect(parseDeterministicArgs("open", ["back"], baseFlags())).toEqual({ action: "open", url: "back" }); - expect(parseDeterministicArgs("snapshot", [], baseFlags({ filter: "interactive" }))).toEqual({ - action: "snapshot", - filter: "interactive", - }); - expect(parseDeterministicArgs("fill", ["email", "a@b.c"], baseFlags())).toEqual({ - action: "fill", - query: "email", - value: "a@b.c", - }); - expect(parseDeterministicArgs("click", ["10", "20"], baseFlags())).toEqual({ action: "click", x: 10, y: 20 }); - expect(parseDeterministicArgs("click", ["e12"], baseFlags())).toEqual({ action: "click", ref: "e12" }); - expect(parseDeterministicArgs("act", ['{"steps":[{"type":"click","ref":"e12"}]}'], baseFlags())).toEqual({ - action: "act", - input: { steps: [{ type: "click", ref: "e12" }] }, - }); - expect(parseDeterministicArgs("fill", ["e12", "a@b.c"], baseFlags())).toEqual({ - action: "fill", - ref: "e12", - value: "a@b.c", - }); - }); - - it("runDeterministicCommand surfaces argv errors before touching the Kernel API", async () => { - // KERNEL_API_KEY is unset in this suite: reaching provisioning would - // throw "missing Kernel API key" instead of the usage error. - await expect(runDeterministicCommand("open", [], baseFlags())).rejects.toThrow("usage: cua open"); - }); -}); - -describe("runDeterministicOnHandle", () => { - it("open navigates via CDP and prints ok (no provider keys in env)", async () => { - const t = setup({ texts: { browser_navigate: "Navigated to https://example.test/." } }); - const code = await runDeterministicOnHandle({ action: "open", url: "example.test" }, t.handle, t.createTranslator); - expect(code).toBe(0); - expect(stdoutLines.join("")).toBe("ok\n"); - expect(t.state.actions).toEqual([{ type: "browser_navigate", url: "example.test" }]); - expect(t.state.closed).toBe(1); - expect(t.handleCloses()).toBe(1); - }); - - it("url prints the current URL", async () => { - const t = setup({ url: "https://example.test/page" }); - const code = await runDeterministicOnHandle({ action: "url" }, t.handle, t.createTranslator); - expect(code).toBe(0); - expect(stdoutLines.join("")).toBe("https://example.test/page\n"); - }); - - it("snapshot passes --filter through and prints the tree", async () => { - const t = setup({ texts: { browser_snapshot: 'button "Go" [e1]' } }); - const code = await runDeterministicOnHandle( - { action: "snapshot", filter: "interactive" }, - t.handle, - t.createTranslator, - ); - expect(code).toBe(0); - expect(stdoutLines.join("")).toBe('button "Go" [e1]\n'); - expect(t.state.actions).toEqual([{ type: "browser_snapshot", filter: "interactive" }]); - }); - - it("act prints bounded semantic feedback and exits by causal outcome", async () => { - const worked: BrowserActResult = { - outcome: "worked", - steps: [{ index: 0, type: "click", outcome: "worked", diagnostics: ["action dispatched"] }], - final_expectation: { - status: "newly_verified", - before: "not_matched", - after: "matched", - diagnostics: ["url changed"], - }, - successor: { status: "unavailable", error: "test successor omitted" }, - }; - const success = setup({ actResult: worked }); - const request = { - action: "act" as const, - input: { steps: [{ type: "click" as const, ref: "e12" }], expect: { type: "url" as const, changed: true } }, - }; - expect(await runDeterministicOnHandle(request, success.handle, success.createTranslator)).toBe(0); - expect(stdoutLines.join("")).toContain("browser_act: worked"); - expect(success.state.actions).toEqual([{ type: "browser_act", ...request.input }]); - - stdoutLines = []; - const uncertain = setup({ actResult: { ...worked, outcome: "unknown", stop_reason: "control_flow" } }); - expect(await runDeterministicOnHandle(request, uncertain.handle, uncertain.createTranslator)).toBe(1); - expect(stdoutLines.join("")).toContain("browser_act: unknown"); - }); - - it("text prints the page text", async () => { - const t = setup({ texts: { browser_text: "hello world" } }); - const code = await runDeterministicOnHandle({ action: "text" }, t.handle, t.createTranslator); - expect(code).toBe(0); - expect(stdoutLines.join("")).toBe("hello world\n"); - }); - - it("tabs prints one line per tab", async () => { - const t = setup({ texts: { browser_list_tabs: 'tab_id AAAA: "One" (https://a)\ntab_id BBBB: "Two" (https://b)' } }); - const code = await runDeterministicOnHandle({ action: "tabs" }, t.handle, t.createTranslator); - expect(code).toBe(0); - expect(stdoutLines.join("")).toContain("tab_id AAAA"); - }); - - it("find exits 1 when no candidates match", async () => { - const t = setup({ candidates: [] }); - const code = await runDeterministicOnHandle({ action: "find", query: "missing thing" }, t.handle, t.createTranslator); - expect(code).toBe(1); - expect(stdoutLines.join("")).toBe('not_found no elements matched "missing thing"\n'); - }); - - it("find prints one candidate per line", async () => { - const t = setup({ - candidates: [ - { ref: "e1", role: "button", name: "Search", score: 2 }, - { ref: "e2", role: "link", name: "Search help", score: 1 }, - ], - }); - const code = await runDeterministicOnHandle({ action: "find", query: "search" }, t.handle, t.createTranslator); - expect(code).toBe(0); - expect(stdoutLines.join("")).toBe('button "Search" [e1]\nlink "Search help" [e2]\n'); - }); - - it("fill exits 1 when nothing fillable matches", async () => { - const t = setup({ candidates: [{ ref: "e1", role: "button", name: "Email us", score: 1 }] }); - const code = await runDeterministicOnHandle( - { action: "fill", query: "email", value: "a@b.c" }, - t.handle, - t.createTranslator, - ); - expect(code).toBe(1); - expect(stdoutLines.join("")).toBe('not_found no fillable element matched "email"\n'); - expect(t.state.actions).toEqual([]); - }); - - it("fill exits 1 on a tied top score and lists the matches", async () => { - const t = setup({ - candidates: [ - { ref: "e1", role: "textbox", name: "Email", score: 1 }, - { ref: "e2", role: "textbox", name: "Email confirmation", score: 1 }, - ], - }); - const code = await runDeterministicOnHandle( - { action: "fill", query: "email", value: "a@b.c" }, - t.handle, - t.createTranslator, - ); - expect(code).toBe(1); - expect(stdoutLines.join("")).toBe( - 'not_found ambiguous query "email" (2 matches): textbox "Email", textbox "Email confirmation"\n', - ); - expect(t.state.actions).toEqual([]); - }); - - it("fill fills the unique best fillable match by ref", async () => { - const t = setup({ - candidates: [ - { ref: "e1", role: "button", name: "Email us", score: 3 }, - { ref: "e2", role: "textbox", name: "Email", score: 2 }, - { ref: "e3", role: "textbox", name: "Name", score: 1 }, - ], - }); - const code = await runDeterministicOnHandle( - { action: "fill", query: "email", value: "a@b.c" }, - t.handle, - t.createTranslator, - ); - expect(code).toBe(0); - expect(stdoutLines.join("")).toBe('ok filled textbox "Email"\n'); - expect(t.state.actions).toEqual([{ type: "browser_fill", ref: "e2", value: "a@b.c" }]); - }); - - it("fill maps checkbox values to a checked state", async () => { - const t = setup({ candidates: [{ ref: "e1", role: "checkbox", name: "Subscribe", score: 2 }] }); - const code = await runDeterministicOnHandle( - { action: "fill", query: "subscribe", value: "false" }, - t.handle, - t.createTranslator, - ); - expect(code).toBe(0); - expect(stdoutLines.join("")).toBe('ok filled checkbox "Subscribe"\n'); - expect(t.state.actions).toEqual([{ type: "browser_fill", ref: "e1", value: false }]); - }); - - it("fill exits 2 on an unrecognized checkbox value", async () => { - const t = setup({ candidates: [{ ref: "e1", role: "checkbox", name: "Subscribe", score: 2 }] }); - const code = await runDeterministicOnHandle( - { action: "fill", query: "subscribe", value: "maybe" }, - t.handle, - t.createTranslator, - ); - expect(code).toBe(2); - expect(stdoutLines.join("")).toContain("error checkbox/radio value must be"); - expect(t.state.actions).toEqual([]); - }); - - it("press dispatches one key chord through the computer batch API", async () => { - const t = setup(); - const code = await runDeterministicOnHandle({ action: "press", keys: ["ctrl", "l"] }, t.handle, t.createTranslator); - expect(code).toBe(0); - expect(stdoutLines.join("")).toBe("ok pressed\n"); - expect(t.kernel.batchCalls).toHaveLength(1); - const body = t.kernel.batchCalls[0]!.body as { actions: Array<{ type: string; press_key?: { keys: string[]; hold_keys?: string[] } }> }; - expect(body.actions).toEqual([{ type: "press_key", press_key: { keys: ["l"], hold_keys: ["Control_L"] } }]); - }); - - it("click dispatches an OS-level click at the coordinates", async () => { - const t = setup(); - const code = await runDeterministicOnHandle({ action: "click", x: 10, y: 20 }, t.handle, t.createTranslator); - expect(code).toBe(0); - expect(stdoutLines.join("")).toBe("ok clicked (10, 20)\n"); - const body = t.kernel.batchCalls[0]!.body as { actions: Array<{ type: string; click_mouse?: { x: number; y: number } }> }; - expect(body.actions[0]!.type).toBe("click_mouse"); - expect(body.actions[0]!.click_mouse).toMatchObject({ x: 10, y: 20 }); - }); - - it("click dispatches a CDP click on the ref", async () => { - const t = setup(); - const code = await runDeterministicOnHandle({ action: "click", ref: "e12" }, t.handle, t.createTranslator); - expect(code).toBe(0); - expect(stdoutLines.join("")).toBe("ok clicked e12\n"); - expect(t.state.actions).toEqual([{ type: "browser_click", ref: "e12" }]); - expect(t.kernel.batchCalls).toHaveLength(0); - }); - - it("click exits 1 when the ref is stale", async () => { - const t = setup({ failWith: new Error("ref e12 is stale or not on the current page. Call snapshot to get fresh refs.") }); - const code = await runDeterministicOnHandle({ action: "click", ref: "e12" }, t.handle, t.createTranslator); - expect(code).toBe(1); - expect(stdoutLines.join("")).toContain("not_found"); - }); - - it("fill fills that element, mapping toggle words to booleans", async () => { - const t = setup(); - expect(await runDeterministicOnHandle({ action: "fill", ref: "e7", value: "a@b.c" }, t.handle, t.createTranslator)).toBe(0); - expect(await runDeterministicOnHandle({ action: "fill", ref: "e8", value: "on" }, t.handle, t.createTranslator)).toBe(0); - expect(t.state.actions).toEqual([ - { type: "browser_fill", ref: "e7", value: "a@b.c" }, - { type: "browser_fill", ref: "e8", value: true }, - ]); - expect(stdoutLines.join("")).toBe("ok filled e7\nok filled e8\n"); - }); - - it("loads persisted ref state before executing and saves it after", async () => { - const t = setup(); - const saved: BrowserRefState[] = []; - const store = { - async load() { - return FAKE_REF_STATE; - }, - async save(state: BrowserRefState) { - saved.push(state); - }, - }; - const code = await runDeterministicOnHandle({ action: "click", ref: "e7" }, t.handle, t.createTranslator, store); - expect(code).toBe(0); - expect(t.state.imported).toEqual([FAKE_REF_STATE]); - expect(saved).toEqual([FAKE_REF_STATE]); - }); - - it("does not touch ref state without a store", async () => { - const t = setup(); - await runDeterministicOnHandle({ action: "click", ref: "e7" }, t.handle, t.createTranslator); - expect(t.state.imported).toEqual([]); - expect(t.state.exported).toBe(0); - }); - - it("screenshot captures via the SDK and writes the file", async () => { - const t = setup(); - const out = join(mkdtempSync(join(tmpdir(), "cua-shot-")), "shot.png"); - const code = await runDeterministicOnHandle({ action: "screenshot", out }, t.handle, t.createTranslator); - expect(code).toBe(0); - expect(stdoutLines.join("")).toBe(`${out}\n`); - expect(t.kernel.screenshots).toBe(1); - expect((await readFile(out)).length).toBeGreaterThan(0); - }); - - it("screenshot --out - writes only the PNG bytes to stdout", async () => { - const t = setup(); - const code = await runDeterministicOnHandle({ action: "screenshot", out: "-" }, t.handle, t.createTranslator); - expect(code).toBe(0); - expect(stdoutLines).toHaveLength(1); - expect(stdoutLines[0]!.startsWith("\x89PNG\r\n\x1a\n")).toBe(true); - expect(t.kernel.screenshots).toBe(1); - }); - - it("screenshot exits 2 when capture fails", async () => { - const t = setup(); - const computer = t.kernel.client.browsers.computer as unknown as { captureScreenshot: () => Promise }; - computer.captureScreenshot = async () => { - throw new Error("capture unavailable"); - }; - const code = await runDeterministicOnHandle({ action: "screenshot", out: "-" }, t.handle, t.createTranslator); - expect(code).toBe(2); - expect(stdoutLines.join("")).toBe("error failed to capture screenshot\n"); - }); - - it("exits 2 and still closes executor and handle when the executor throws", async () => { - const t = setup({ failWith: new Error("cdp exploded") }); - const code = await runDeterministicOnHandle({ action: "open", url: "example.test" }, t.handle, t.createTranslator); - expect(code).toBe(2); - expect(stdoutLines.join("")).toBe("error cdp exploded\n"); - expect(t.state.closed).toBe(1); - expect(t.handleCloses()).toBe(1); - }); -}); diff --git a/packages/cli/test/fixtures/fake-kernel.ts b/packages/cli/test/fixtures/fake-kernel.ts deleted file mode 100644 index 4a51c8bc..00000000 --- a/packages/cli/test/fixtures/fake-kernel.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type Kernel from "@onkernel/sdk"; -import type { KernelBrowser } from "@onkernel/cua-agent"; - -const TINY_PNG = Buffer.from( - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=", - "base64", -); - -export interface FakeBatchCall { - id: string; - body: unknown; -} - -/** Minimal Kernel client + browser pair sufficient to run the CUA harness. */ -export interface FakeKernelEnvironment { - client: Kernel; - browser: KernelBrowser; - batchCalls: FakeBatchCall[]; - screenshots: number; - deleted: string[]; -} - -export function createFakeKernelEnvironment(overrides: Partial = {}): FakeKernelEnvironment { - const browser = { - session_id: overrides.session_id ?? "browser_test_123", - browser_live_view_url: overrides.browser_live_view_url ?? "https://example.test/live", - cdp_ws_url: overrides.cdp_ws_url ?? "wss://example.test/cdp", - created_at: overrides.created_at ?? new Date().toISOString(), - viewport: overrides.viewport ?? { width: 1024, height: 768 }, - } as KernelBrowser; - - const env: FakeKernelEnvironment = { - client: undefined as unknown as Kernel, - browser, - batchCalls: [], - screenshots: 0, - deleted: [], - }; - - env.client = { - browsers: { - create: async () => browser, - retrieve: async () => browser, - deleteByID: async (id: string) => { - env.deleted.push(id); - }, - computer: { - batch: async (sessionId: string, body: unknown) => { - env.batchCalls.push({ id: sessionId, body }); - }, - captureScreenshot: async () => { - env.screenshots += 1; - return new Response(new Uint8Array(TINY_PNG)); - }, - getMousePosition: async () => ({ x: 0, y: 0 }), - readClipboard: async () => ({ text: "" }), - }, - }, - profiles: { - retrieve: async () => ({ id: "profile_test", name: "test" }), - create: async ({ name }: { name: string }) => ({ id: "profile_test", name }), - }, - } as unknown as Kernel; - - return env; -} diff --git a/packages/cli/test/fixtures/harness.ts b/packages/cli/test/fixtures/harness.ts deleted file mode 100644 index 27420d19..00000000 --- a/packages/cli/test/fixtures/harness.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - InMemorySessionRepo, - type Session, - type Skill, -} from "@onkernel/cua-agent"; -import { tmpdir } from "node:os"; -import { mkdtempSync } from "node:fs"; -import { join } from "node:path"; -import { parseCuaModelRef } from "@onkernel/cua-ai"; -import { buildCuaHarness, type CuaCliSession, type CuaCliTool, defaultInteractionTools } from "../../src/harness"; -import { createFakeKernelEnvironment, type FakeKernelEnvironment } from "./fake-kernel"; -import type { ScriptedProviderHandle, ScriptedTurn } from "./scripted-provider"; -import { createScriptedCuaModels } from "./scripted-provider"; - -export interface TestHarnessFixture { - provider: ScriptedProviderHandle; - kernel: FakeKernelEnvironment; - session: Session; - cwd: string; - harness: CuaCliSession["harness"]; - catalog: CuaCliSession["catalog"]; -} - -export interface BuildTestHarnessOptions { - turns: ScriptedTurn[]; - skills?: Skill[]; - /** CUA model ref. Defaults to the CLI's OpenAI default. */ - modelRef?: string; - tools?: CuaCliTool[]; - retry?: Parameters[0]["retry"]; -} - -export async function buildTestHarness(opts: BuildTestHarnessOptions): Promise { - const modelRef = opts.modelRef ?? "openai:gpt-5.6-sol"; - const provider = createScriptedCuaModels(parseCuaModelRef(modelRef).provider, opts.turns); - - const kernel = createFakeKernelEnvironment(); - const cwd = mkdtempSync(join(tmpdir(), "cua-cli-test-")); - - const sessionRepo = new InMemorySessionRepo(); - const session = await sessionRepo.create(); - - const { harness, catalog } = buildCuaHarness({ - cwd, - client: kernel.client, - browser: kernel.browser, - session, - model: modelRef as never, - skills: opts.skills, - tools: opts.tools ?? defaultInteractionTools(modelRef as never), - models: provider.models, - retry: opts.retry, - }); - - return { - provider, - kernel, - session, - cwd, - harness, - catalog, - }; -} diff --git a/packages/cli/test/fixtures/scripted-provider.ts b/packages/cli/test/fixtures/scripted-provider.ts deleted file mode 100644 index 6c2ff2fe..00000000 --- a/packages/cli/test/fixtures/scripted-provider.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { - type Api, - type AssistantMessage, - type Context, - createAssistantMessageEventStream, - createCuaModels, - type CuaSimpleStreamOptions, - type Model, - type MutableModels, - type Usage, -} from "@onkernel/cua-ai"; - -/** One scripted step replayed when the harness asks the provider for a turn. */ -export type ScriptedStep = - | { type: "text"; text: string; chunkSize?: number; chunkMs?: number } - | { type: "tool_call"; toolName: string; args: Record; id?: string } - | { type: "wait_abort"; settleMs?: number } - | { type: "error"; message: string }; - -export interface ScriptedTurn { - steps: ScriptedStep[]; - /** - * Stop reason for the turn. Defaults to "stop" when there are no tool - * calls and to "toolUse" otherwise. - */ - stopReason?: "stop" | "toolUse" | "length"; - /** Overrides the zeroed default usage on the resulting assistant message. */ - usage?: Partial; -} - -export interface ScriptedProviderHandle { - /** pi `Models` collection whose named provider replays the scripted turns. */ - models: MutableModels; - /** Reset the turn cursor; the next provider call replays the first turn. */ - reset(): void; - /** Number of provider calls dispatched so far. */ - callCount(): number; - /** Latest context the provider was called with (assistant-side mock). */ - lastContext(): Context | undefined; - /** Latest stream options the provider was called with. */ - lastStreamOptions(): CuaSimpleStreamOptions | undefined; -} - -/** - * Build a CUA `Models` collection whose named provider is replaced with a - * scripted double that replays one `ScriptedTurn` per provider call, - * regardless of the model's api id. Pass `handle.models` to - * `buildCuaHarness`; nothing global is mutated. - */ -export function createScriptedCuaModels(providerId: string, turns: ScriptedTurn[]): ScriptedProviderHandle { - const state = { - index: 0, - lastContext: undefined as Context | undefined, - lastStreamOptions: undefined as CuaSimpleStreamOptions | undefined, - }; - const dispatch = (model: Model, context: Context, options?: CuaSimpleStreamOptions) => { - state.lastContext = context; - state.lastStreamOptions = options; - const turn = turns[state.index]; - state.index += 1; - return buildStream(model, turn, options?.signal); - }; - const models = createCuaModels(); - models.setProvider({ - id: providerId, - name: `Scripted ${providerId}`, - auth: { apiKey: { name: "scripted test key", resolve: async () => ({ auth: { apiKey: "test-key" } }) } }, - getModels: () => [], - stream: (model, context, options) => dispatch(model, context, options), - streamSimple: (model, context, options) => dispatch(model, context, options), - }); - return { - models, - reset(): void { - state.index = 0; - state.lastContext = undefined; - state.lastStreamOptions = undefined; - }, - callCount(): number { - return state.index; - }, - lastContext(): Context | undefined { - return state.lastContext; - }, - lastStreamOptions(): CuaSimpleStreamOptions | undefined { - return state.lastStreamOptions; - }, - }; -} - -function buildStream(model: Model, turn: ScriptedTurn | undefined, signal?: AbortSignal) { - const stream = createAssistantMessageEventStream(); - void (async () => { - const message = baseAssistantMessage(model, turn?.usage); - if (!turn) { - message.stopReason = "stop"; - stream.push({ type: "start", partial: message }); - stream.push({ type: "done", reason: "stop", message }); - stream.end(message); - return; - } - - stream.push({ type: "start", partial: message }); - - let hasToolCall = false; - let errorStep: { message: string } | undefined; - let contentIndex = 0; - let aborted = false; - - for (const step of turn.steps) { - if (signal?.aborted) { - aborted = true; - break; - } - if (step.type === "text") { - const chunkSize = Math.max(1, step.chunkSize ?? step.text.length); - const chunkMs = step.chunkMs ?? 0; - const aggregated = { text: "" }; - stream.push({ type: "text_start", contentIndex, partial: message }); - for (const chunk of chunkText(step.text, chunkSize)) { - if (signal?.aborted) { - aborted = true; - break; - } - aggregated.text += chunk; - // Append text to message progressively so the final assistant - // message reflects the full streamed value when consumers - // inspect partials. - if (message.content[contentIndex]?.type === "text") { - (message.content[contentIndex] as { text: string }).text = aggregated.text; - } else { - message.content.push({ type: "text", text: aggregated.text }); - } - stream.push({ type: "text_delta", contentIndex, delta: chunk, partial: message }); - if (chunkMs > 0) await delay(chunkMs, signal); - } - if (!aborted) { - stream.push({ type: "text_end", contentIndex, content: aggregated.text, partial: message }); - contentIndex += 1; - } - } else if (step.type === "tool_call") { - hasToolCall = true; - const id = step.id ?? `call_${contentIndex + 1}`; - message.content.push({ - type: "toolCall", - id, - name: step.toolName, - arguments: step.args, - }); - stream.push({ type: "toolcall_start", contentIndex, partial: message }); - stream.push({ - type: "toolcall_end", - contentIndex, - toolCall: { type: "toolCall", id, name: step.toolName, arguments: step.args }, - partial: message, - }); - contentIndex += 1; - } else if (step.type === "wait_abort") { - await waitForAbort(signal); - if (step.settleMs) await new Promise((resolve) => setTimeout(resolve, step.settleMs)); - aborted = true; - break; - } else if (step.type === "error") { - errorStep = { message: step.message }; - break; - } - } - - if (errorStep) { - message.stopReason = "error"; - message.errorMessage = errorStep.message; - stream.push({ type: "error", reason: "error", error: message }); - stream.end(message); - return; - } - - if (aborted) { - message.stopReason = "aborted"; - message.errorMessage = "aborted"; - stream.push({ type: "error", reason: "aborted", error: message }); - stream.end(message); - return; - } - - const stopReason = turn.stopReason ?? (hasToolCall ? "toolUse" : "stop"); - message.stopReason = stopReason; - stream.push({ type: "done", reason: stopReason, message }); - stream.end(message); - })(); - return stream; -} - -function chunkText(text: string, chunkSize: number): string[] { - const chunks: string[] = []; - for (let index = 0; index < text.length; index += chunkSize) { - chunks.push(text.slice(index, index + chunkSize)); - } - return chunks.length > 0 ? chunks : [""]; -} - -async function delay(ms: number, signal?: AbortSignal): Promise { - if (signal?.aborted || ms <= 0) return; - await new Promise((resolve) => { - const timer = setTimeout(() => { - cleanup(); - resolve(); - }, ms); - const cleanup = () => { - clearTimeout(timer); - signal?.removeEventListener("abort", onAbort); - }; - const onAbort = () => { - cleanup(); - resolve(); - }; - signal?.addEventListener("abort", onAbort, { once: true }); - }); -} - -async function waitForAbort(signal?: AbortSignal): Promise { - if (!signal) return; - if (signal.aborted) return; - await new Promise((resolve) => { - signal.addEventListener("abort", () => resolve(), { once: true }); - }); -} - -function baseAssistantMessage(model: Model, usage?: Partial): AssistantMessage { - return { - role: "assistant", - content: [], - api: model.api, - provider: model.provider, - model: model.id, - usage: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - totalTokens: 0, - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, - ...usage, - }, - stopReason: "stop", - timestamp: Date.now(), - }; -} diff --git a/packages/cli/test/fixtures/tui-fixture-runner.ts b/packages/cli/test/fixtures/tui-fixture-runner.ts deleted file mode 100644 index dfe54f28..00000000 --- a/packages/cli/test/fixtures/tui-fixture-runner.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Child-process entry point for ptywright-driven TUI tests. Spawned via - * `tsx` so the same source file the vitest harness imports gets type-checked - * and exercised. Receives a JSON fixture path on argv[2], registers the - * scripted provider, assembles the real {@link buildCuaHarness}, and starts - * the interactive TUI. - */ -import { InMemorySessionRepo, type Skill } from "@onkernel/cua-agent"; -import { parseCuaModelRef, type CuaModelRef } from "@onkernel/cua-ai"; -import { readFile } from "node:fs/promises"; -import { resolve } from "node:path"; -import { buildCuaHarness, defaultApplicationTools, defaultInteractionTools } from "../../src/harness"; -import type { ContextFile } from "../../src/harness-skills"; -import { runInteractive } from "../../src/tui/main"; -import { createFakeKernelEnvironment } from "./fake-kernel"; -import { createScriptedCuaModels, type ScriptedTurn } from "./scripted-provider"; - -interface TuiFixture { - modelRef?: string; - turns: ScriptedTurn[]; - skills?: Skill[]; - contextFiles?: ContextFile[]; - /** - * Assemble the real CLI tool policy (interaction tools for the model plus - * the coding tools) instead of an empty list, so `/tools` and `/model` tool - * revalidation have a genuine baseline to work with. - */ - tools?: boolean; -} - -async function main(): Promise { - const fixtureArg = process.argv[2]; - if (!fixtureArg) { - throw new Error("usage: tui-fixture-runner "); - } - const fixturePath = resolve(process.cwd(), fixtureArg); - const fixture = JSON.parse(await readFile(fixturePath, "utf8")) as TuiFixture; - - const modelRef = fixture.modelRef ?? "openai:gpt-5.5"; - const scripted = createScriptedCuaModels(parseCuaModelRef(modelRef).provider, fixture.turns); - - const kernel = createFakeKernelEnvironment(); - const sessionRepo = new InMemorySessionRepo(); - const session = await sessionRepo.create(); - const cwd = process.cwd(); - const skills = fixture.skills ?? []; - const contextFiles = fixture.contextFiles ?? []; - const applicationTools = fixture.tools ? defaultApplicationTools(cwd) : []; - const interactionToolsForModel = fixture.tools ? defaultInteractionTools : undefined; - const { harness, catalog } = buildCuaHarness({ - cwd, - client: kernel.client, - browser: kernel.browser, - session, - model: modelRef as CuaModelRef, - skills, - contextFiles, - tools: fixture.tools - ? [...defaultInteractionTools(modelRef as CuaModelRef), ...applicationTools] - : [], - models: scripted.models, - }); - - const code = await runInteractive({ - cwd, - harness, - catalog, - browserHandle: { - client: kernel.client, - browser: kernel.browser, - async close(): Promise {}, - }, - session, - skills, - contextFiles, - modelRef, - provider: modelRef.split(":", 1)[0] ?? "openai", - applicationTools, - interactionToolsForModel, - }); - process.exit(code); -} - -main().catch((err) => { - process.stderr.write(`fixture error: ${(err as Error).message}\n`); - process.exit(1); -}); diff --git a/packages/cli/test/fixtures/tui-fixtures/abort.json b/packages/cli/test/fixtures/tui-fixtures/abort.json deleted file mode 100644 index 5742ca56..00000000 --- a/packages/cli/test/fixtures/tui-fixtures/abort.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "modelRef": "openai:gpt-5.5", - "turns": [ - { - "steps": [ - { - "type": "text", - "text": "working...", - "chunkSize": 5, - "chunkMs": 10 - }, - { - "type": "wait_abort" - } - ] - }, - { - "steps": [ - { - "type": "text", - "text": "fixture response" - } - ] - } - ] -} diff --git a/packages/cli/test/fixtures/tui-fixtures/error.json b/packages/cli/test/fixtures/tui-fixtures/error.json deleted file mode 100644 index ad53ac9b..00000000 --- a/packages/cli/test/fixtures/tui-fixtures/error.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "modelRef": "openai:gpt-5.5", - "turns": [ - { - "steps": [ - { - "type": "error", - "message": "fixture provider failed" - } - ] - } - ] -} diff --git a/packages/cli/test/fixtures/tui-fixtures/interrupt-cancel.json b/packages/cli/test/fixtures/tui-fixtures/interrupt-cancel.json deleted file mode 100644 index e007f95d..00000000 --- a/packages/cli/test/fixtures/tui-fixtures/interrupt-cancel.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "modelRef": "openai:gpt-5.5", - "turns": [ - { - "steps": [ - { - "type": "text", - "text": "working..." - }, - { - "type": "wait_abort", - "settleMs": 3000 - } - ] - }, - { - "steps": [ - { - "type": "text", - "text": "fixture response" - } - ] - } - ] -} diff --git a/packages/cli/test/fixtures/tui-fixtures/model-arg.json b/packages/cli/test/fixtures/tui-fixtures/model-arg.json deleted file mode 100644 index 5ec60ffb..00000000 --- a/packages/cli/test/fixtures/tui-fixtures/model-arg.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "modelRef": "openai:gpt-5.5", - "turns": [ - { - "steps": [ - { "type": "text", "text": "fixture response", "chunkSize": 8, "chunkMs": 5 } - ] - } - ] -} diff --git a/packages/cli/test/fixtures/tui-fixtures/model-picker-cancel.json b/packages/cli/test/fixtures/tui-fixtures/model-picker-cancel.json deleted file mode 100644 index 5ec60ffb..00000000 --- a/packages/cli/test/fixtures/tui-fixtures/model-picker-cancel.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "modelRef": "openai:gpt-5.5", - "turns": [ - { - "steps": [ - { "type": "text", "text": "fixture response", "chunkSize": 8, "chunkMs": 5 } - ] - } - ] -} diff --git a/packages/cli/test/fixtures/tui-fixtures/model-picker.json b/packages/cli/test/fixtures/tui-fixtures/model-picker.json deleted file mode 100644 index 5ec60ffb..00000000 --- a/packages/cli/test/fixtures/tui-fixtures/model-picker.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "modelRef": "openai:gpt-5.5", - "turns": [ - { - "steps": [ - { "type": "text", "text": "fixture response", "chunkSize": 8, "chunkMs": 5 } - ] - } - ] -} diff --git a/packages/cli/test/fixtures/tui-fixtures/multiline.json b/packages/cli/test/fixtures/tui-fixtures/multiline.json deleted file mode 100644 index 6254d72c..00000000 --- a/packages/cli/test/fixtures/tui-fixtures/multiline.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "modelRef": "openai:gpt-5.5", - "turns": [ - { - "steps": [ - { - "type": "text", - "text": "multiline ok" - } - ] - } - ] -} diff --git a/packages/cli/test/fixtures/tui-fixtures/resources.json b/packages/cli/test/fixtures/tui-fixtures/resources.json deleted file mode 100644 index 247484e6..00000000 --- a/packages/cli/test/fixtures/tui-fixtures/resources.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "modelRef": "openai:gpt-5.5", - "skills": [ - { - "name": "deploy-skill", - "description": "Ship the build.", - "content": "Run the deploy steps.", - "filePath": "/tmp/skills/deploy-skill/SKILL.md" - }, - { - "name": "review-skill", - "description": "Review a diff.", - "content": "Run the review steps.", - "filePath": "/tmp/skills/review-skill/SKILL.md" - } - ], - "contextFiles": [ - { - "path": "/tmp/project/AGENTS.md", - "content": "Be concise." - } - ], - "turns": [ - { - "steps": [ - { - "type": "text", - "text": "fixture response" - } - ] - } - ] -} diff --git a/packages/cli/test/fixtures/tui-fixtures/steer.json b/packages/cli/test/fixtures/tui-fixtures/steer.json deleted file mode 100644 index b27fea98..00000000 --- a/packages/cli/test/fixtures/tui-fixtures/steer.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "modelRef": "openai:gpt-5.5", - "turns": [ - { - "steps": [ - { - "type": "text", - "text": "working...", - "chunkSize": 5, - "chunkMs": 500 - } - ] - }, - { - "steps": [ - { - "type": "text", - "text": "queued response" - } - ] - } - ] -} diff --git a/packages/cli/test/fixtures/tui-fixtures/streaming.json b/packages/cli/test/fixtures/tui-fixtures/streaming.json deleted file mode 100644 index f3496d03..00000000 --- a/packages/cli/test/fixtures/tui-fixtures/streaming.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "modelRef": "openai:gpt-5.5", - "turns": [ - { - "steps": [ - { - "type": "text", - "text": "fixture response", - "chunkSize": 8, - "chunkMs": 5 - } - ] - } - ] -} diff --git a/packages/cli/test/fixtures/tui-fixtures/tools-picker.json b/packages/cli/test/fixtures/tui-fixtures/tools-picker.json deleted file mode 100644 index 320d2ee9..00000000 --- a/packages/cli/test/fixtures/tui-fixtures/tools-picker.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "modelRef": "openai:gpt-5.5", - "tools": true, - "turns": [ - { - "steps": [ - { "type": "text", "text": "fixture response", "chunkSize": 8, "chunkMs": 5 } - ] - } - ] -} diff --git a/packages/cli/test/fixtures/tui-fixtures/tools-reset-on-model.json b/packages/cli/test/fixtures/tui-fixtures/tools-reset-on-model.json deleted file mode 100644 index 320d2ee9..00000000 --- a/packages/cli/test/fixtures/tui-fixtures/tools-reset-on-model.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "modelRef": "openai:gpt-5.5", - "tools": true, - "turns": [ - { - "steps": [ - { "type": "text", "text": "fixture response", "chunkSize": 8, "chunkMs": 5 } - ] - } - ] -} diff --git a/packages/cli/test/harness-assembly.test.ts b/packages/cli/test/harness-assembly.test.ts deleted file mode 100644 index 70837f93..00000000 --- a/packages/cli/test/harness-assembly.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - formatSkillsForSystemPrompt, - InMemorySessionRepo, - type Skill, -} from "@onkernel/cua-agent"; -import { tmpdir } from "node:os"; -import { mkdtempSync } from "node:fs"; -import { join } from "node:path"; -import { buildCuaHarness, defaultInteractionTools } from "../src/harness"; -import { createFakeKernelEnvironment } from "./fixtures/fake-kernel"; -import { createScriptedCuaModels } from "./fixtures/scripted-provider"; - -describe("buildCuaHarness", () => { - it("chooses explicit model-specific interaction catalogs", () => { - const openaiNames = defaultInteractionTools("openai:gpt-5.6-sol").map((tool) => tool.name); - expect(openaiNames[0]).toBe("browser_snapshot"); - expect(openaiNames.at(-1)).toBe("browser_act"); - expect(defaultInteractionTools("anthropic:claude-opus-5")).toEqual([ - expect.objectContaining({ name: "browser", origin: "provider-native" }), - ]); - expect(defaultInteractionTools("anthropic:claude-3-7-sonnet").map((tool) => tool.name).at(-1)).toBe("browser_act"); - const googleNames = defaultInteractionTools("google:gemini-3.6-flash").map((tool) => tool.name); - expect(googleNames).toContain("take_screenshot"); - expect(googleNames).not.toContain("browser_act"); - for (const model of ["xai:grok-4.5", "openrouter:meta/muse-spark-1.1"] as const) { - const tools = defaultInteractionTools(model); - expect(tools[0]).toMatchObject({ name: "browser_snapshot", origin: "cua" }); - expect(tools.at(-1)?.name).toBe("browser_act"); - } - // Kimi's API rejects the request once browser_act's schema is attached. - for (const model of ["moonshotai:kimi-k3", "openrouter:moonshotai/kimi-k3"] as const) { - const kimiNames = defaultInteractionTools(model).map((tool) => tool.name); - expect(kimiNames[0]).toBe("browser_snapshot"); - expect(kimiNames).not.toContain("browser_act"); - expect(kimiNames).toContain("browser_wait_for"); - } - }); - - it("installs interaction and coding tools in one explicit default list", async () => { - const cwd = mkdtempSync(join(tmpdir(), "cua-cli-harness-")); - const kernel = createFakeKernelEnvironment(); - const session = await new InMemorySessionRepo().create(); - const { harness, catalog } = buildCuaHarness({ - cwd, - client: kernel.client, - browser: kernel.browser, - session, - model: "openai:gpt-5.5", - }); - const toolNames = catalog.getTools().map((tool) => tool.name); - expect(toolNames).toContain("browser_click"); - expect(toolNames).toContain("browser_screenshot"); - expect(toolNames).toContain("browser_act"); - // pi's native read/bash/edit/write coding tools close the list, in order. - expect(toolNames.slice(-4)).toEqual(["read", "bash", "edit", "write"]); - }); - - it("uses only the caller-owned skill block as its system prompt", async () => { - const provider = createScriptedCuaModels("openai", [ - { steps: [{ type: "text", text: "ok" }] }, - ]); - const cwd = mkdtempSync(join(tmpdir(), "cua-cli-harness-")); - const kernel = createFakeKernelEnvironment(); - const session = await new InMemorySessionRepo().create(); - const skill: Skill = { - name: "demo", - description: "demo skill for tests", - content: "Use the demo workflow.", - filePath: join(cwd, "demo.md"), - }; - const { harness } = buildCuaHarness({ - cwd, - client: kernel.client, - browser: kernel.browser, - session, - model: "openai:gpt-5.5", - skills: [skill], - tools: [], - models: provider.models, - }); - let capturedSystemPrompt: string | undefined; - harness.on("before_agent_start", (event) => { - capturedSystemPrompt = event.systemPrompt; - return undefined; - }); - await harness.prompt("hi"); - const skillBlock = formatSkillsForSystemPrompt([skill]).trim(); - expect(capturedSystemPrompt?.trim()).toBe(skillBlock); - }); - - it("injects loaded context files into the system prompt", async () => { - const provider = createScriptedCuaModels("openai", [ - { steps: [{ type: "text", text: "ok" }] }, - ]); - const cwd = mkdtempSync(join(tmpdir(), "cua-cli-harness-")); - const kernel = createFakeKernelEnvironment(); - const session = await new InMemorySessionRepo().create(); - const { harness } = buildCuaHarness({ - cwd, - client: kernel.client, - browser: kernel.browser, - session, - model: "openai:gpt-5.5", - contextFiles: [{ path: join(cwd, "AGENTS.md"), content: "Always prefer tabs over spaces." }], - tools: [], - models: provider.models, - }); - let capturedSystemPrompt: string | undefined; - harness.on("before_agent_start", (event) => { - capturedSystemPrompt = event.systemPrompt; - return undefined; - }); - await harness.prompt("hi"); - expect(capturedSystemPrompt).toContain("Always prefer tabs over spaces."); - expect(capturedSystemPrompt).toContain(join(cwd, "AGENTS.md")); - }); - - it("forwards response-threading configuration to the provider", async () => { - const provider = createScriptedCuaModels("openai", [ - { steps: [{ type: "text", text: "ok" }] }, - ]); - const cwd = mkdtempSync(join(tmpdir(), "cua-cli-harness-")); - const kernel = createFakeKernelEnvironment(); - const session = await new InMemorySessionRepo().create(); - const { harness } = buildCuaHarness({ - cwd, - client: kernel.client, - browser: kernel.browser, - session, - model: "openai:gpt-5.5", - tools: [], - models: provider.models, - responseThreading: false, - }); - - await harness.prompt("hi"); - - expect(provider.lastStreamOptions()?.disableResponseThreading).toBe(true); - }); - - it("delivers the first prompt with an image attached via harness.prompt({ images })", async () => { - const provider = createScriptedCuaModels("openai", [ - { steps: [{ type: "text", text: "done" }] }, - ]); - - const cwd = mkdtempSync(join(tmpdir(), "cua-cli-harness-")); - const kernel = createFakeKernelEnvironment(); - const session = await new InMemorySessionRepo().create(); - const { harness } = buildCuaHarness({ - cwd, - client: kernel.client, - browser: kernel.browser, - session, - model: "openai:gpt-5.5", - tools: [], - models: provider.models, - }); - - const tinyPngBase64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="; - await harness.prompt("look at this", { - images: [{ type: "image", data: tinyPngBase64, mimeType: "image/png" }], - }); - - const entries = await session.getBranch(); - const firstUser = entries.find((e) => e.type === "message" && e.message.role === "user"); - expect(firstUser).toBeDefined(); - const content = (firstUser as { message: { content: unknown[] } }).message.content as Array<{ - type: string; - data?: string; - }>; - expect(content.some((c) => c.type === "image" && c.data === tinyPngBase64)).toBe(true); - }); -}); diff --git a/packages/cli/test/harness-browser.test.ts b/packages/cli/test/harness-browser.test.ts deleted file mode 100644 index 2af93595..00000000 --- a/packages/cli/test/harness-browser.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import Kernel, { NotFoundError } from "@onkernel/sdk"; -import { describe, expect, it } from "vitest"; -import { resolveProxyId } from "../src/harness-browser"; - -const notFound = () => new NotFoundError(404, { message: "not found" }, "not found", new Headers()); - -function fakeClient(overrides: { - retrieve?: (id: string) => Promise<{ id?: string }>; - list?: () => Promise>; -}): Kernel { - return { - proxies: { - retrieve: overrides.retrieve ?? (async () => Promise.reject(notFound())), - list: overrides.list ?? (async () => []), - }, - } as unknown as Kernel; -} - -describe("resolveProxyId", () => { - it("returns the id when the selector is an existing proxy id", async () => { - const client = fakeClient({ retrieve: async (id) => ({ id }) }); - await expect(resolveProxyId(client, "proxy_abc")).resolves.toBe("proxy_abc"); - }); - - it("falls back to a unique name match from the proxy list", async () => { - const client = fakeClient({ list: async () => [{ id: "proxy_1", name: "residential-us" }, { id: "proxy_2", name: "other" }] }); - await expect(resolveProxyId(client, "residential-us")).resolves.toBe("proxy_1"); - }); - - it("rejects an ambiguous name instead of guessing", async () => { - const client = fakeClient({ list: async () => [{ id: "proxy_1", name: "us" }, { id: "proxy_2", name: "us" }] }); - await expect(resolveProxyId(client, "us")).rejects.toThrow(/ambiguous/); - }); - - it("never auto-creates: an unknown selector is an error", async () => { - const client = fakeClient({}); - await expect(resolveProxyId(client, "does-not-exist")).rejects.toThrow(/was not found/); - }); - - it("propagates non-404 lookup failures", async () => { - const client = fakeClient({ - retrieve: async () => Promise.reject(new Error("boom")), - }); - await expect(resolveProxyId(client, "proxy_abc")).rejects.toThrow(/looking up proxy/); - }); -}); diff --git a/packages/cli/test/harness-models.test.ts b/packages/cli/test/harness-models.test.ts deleted file mode 100644 index b27c4d51..00000000 --- a/packages/cli/test/harness-models.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { DEFAULT_CUA_MODEL_REF, listSupportedModels, resolveCuaModelRef } from "../src/harness-models"; - -describe("resolveCuaModelRef", () => { - it("defaults to openai:gpt-5.6-sol", () => { - expect(resolveCuaModelRef(undefined)).toBe(DEFAULT_CUA_MODEL_REF); - expect(resolveCuaModelRef("")).toBe(DEFAULT_CUA_MODEL_REF); - }); - - it("passes provider-qualified refs through", () => { - expect(resolveCuaModelRef("openai:gpt-5.6-sol")).toBe("openai:gpt-5.6-sol"); - expect(resolveCuaModelRef("openai:gpt-5.5")).toBe("openai:gpt-5.5"); - expect(resolveCuaModelRef("anthropic:claude-opus-5")).toBe("anthropic:claude-opus-5"); - expect(resolveCuaModelRef("openrouter:meta/muse-spark-1.1")).toBe("openrouter:meta/muse-spark-1.1"); - expect(resolveCuaModelRef("xai:grok-4.5")).toBe("xai:grok-4.5"); - expect(resolveCuaModelRef("moonshotai:kimi-k3")).toBe("moonshotai:kimi-k3"); - expect(resolveCuaModelRef("moonshot:kimi-k3")).toBe("moonshotai:kimi-k3"); - }); - - it("accepts bare ids when they match exactly one catalog entry", () => { - expect(resolveCuaModelRef("gpt-5.6-sol")).toBe("openai:gpt-5.6-sol"); - expect(resolveCuaModelRef("gpt-5.5")).toBe("openai:gpt-5.5"); - expect(resolveCuaModelRef("claude-opus-5")).toBe("anthropic:claude-opus-5"); - expect(resolveCuaModelRef("meta/muse-spark-1.1")).toBe("openrouter:meta/muse-spark-1.1"); - expect(resolveCuaModelRef("grok-4.5")).toBe("xai:grok-4.5"); - expect(resolveCuaModelRef("kimi-k3")).toBe("moonshotai:kimi-k3"); - }); - - it("throws on unknown bare ids", () => { - expect(() => resolveCuaModelRef("does-not-exist")).toThrow(/unknown model/); - }); - - it("filters to a provider's whole catalog", () => { - // No allowlist: every model the provider carries is listed, including the - // ones no CUA table mentions. - const xai = listSupportedModels("xai").map((model) => model.ref); - expect(xai).toContain("xai:grok-4.5"); - expect(xai).toContain("xai:grok-4.3"); - expect(listSupportedModels("moonshotai").map((model) => model.ref)).toContain("moonshotai:kimi-k3"); - expect(listSupportedModels("moonshot").map((model) => model.ref)).toContain("moonshotai:kimi-k3"); - expect(listSupportedModels("openrouter").map((model) => model.ref)).toContain("openrouter:meta/muse-spark-1.1"); - expect(resolveCuaModelRef("openrouter:moonshotai/kimi-k3")).toBe("openrouter:moonshotai/kimi-k3"); - }); - - it("rejects a provider pi-ai does not carry", () => { - expect(() => listSupportedModels("bogus")).toThrow(/unknown provider "bogus"/); - }); - - it("treats 'gemini' as an alias for google when filtering", () => { - const fromGemini = listSupportedModels("gemini"); - const fromGoogle = listSupportedModels("google"); - expect(fromGemini.map((m) => m.ref)).toEqual(fromGoogle.map((m) => m.ref)); - expect(fromGoogle.length).toBeGreaterThan(0); - }); -}); diff --git a/packages/cli/test/harness-named-sessions.test.ts b/packages/cli/test/harness-named-sessions.test.ts deleted file mode 100644 index 20b43542..00000000 --- a/packages/cli/test/harness-named-sessions.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { mkdtempSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { applyNamedSessionDefaults, type HarnessCliFlags } from "../src/cli-harness"; -import { - listNamedSessions, - type NamedSessionMetadata, - readNamedSession, - recordSessionModel, - updateNamedSessionRuntime, - writeNamedSession, - writeNamedSessionRefs, -} from "../src/harness-named-sessions"; - -const originalXdg = process.env.XDG_DATA_HOME; - -function baseMeta(overrides: Partial = {}): NamedSessionMetadata { - return { name: "foo", kernel_session_id: "ks_123", created_at: Date.now(), ...overrides }; -} - -function baseFlags(overrides: Partial = {}): HarnessCliFlags { - return { - verbose: false, - profileSaveChanges: false, - continueLatest: false, - resumePicker: false, - noSession: false, - noSkills: false, - debugTui: false, - jsonlIncludeDeltas: false, - jsonlIncludeImages: false, - namedSession: "foo", - skillPaths: [], - ...overrides, - }; -} - -describe("named session model persistence", () => { - beforeEach(() => { - process.env.XDG_DATA_HOME = mkdtempSync(join(tmpdir(), "cua-cli-named-")); - }); - - afterEach(() => { - if (originalXdg === undefined) delete process.env.XDG_DATA_HOME; - else process.env.XDG_DATA_HOME = originalXdg; - }); - - it("records the model onto the metadata file", async () => { - await writeNamedSession(baseMeta()); - await recordSessionModel("foo", { model: "anthropic:claude-opus-4-8" }); - const meta = await readNamedSession("foo"); - expect(meta?.model).toBe("anthropic:claude-opus-4-8"); - }); - - it("overwrites a previously recorded model on an explicit switch", async () => { - await writeNamedSession(baseMeta({ model: "openai:gpt-5.5" })); - await recordSessionModel("foo", { model: "anthropic:claude-opus-4-8" }); - const meta = await readNamedSession("foo"); - expect(meta?.model).toBe("anthropic:claude-opus-4-8"); - }); - - it("is a no-op for an unknown session", async () => { - await recordSessionModel("missing", { model: "openai:gpt-5.5" }); - expect(await readNamedSession("missing")).toBeUndefined(); - }); - - it("patches the model without clobbering session identity", async () => { - await writeNamedSession(baseMeta({ model: "openai:gpt-5.5" })); - await updateNamedSessionRuntime("foo", { model: "anthropic:claude-opus-4-8" }); - const meta = await readNamedSession("foo"); - expect(meta?.model).toBe("anthropic:claude-opus-4-8"); - expect(meta?.kernel_session_id).toBe("ks_123"); - }); - - it("defaults flags from the stored session model when -m is omitted", () => { - const meta = baseMeta({ model: "anthropic:claude-opus-4-8" }); - const flags = applyNamedSessionDefaults(baseFlags(), meta); - expect(flags.model).toBe("anthropic:claude-opus-4-8"); - }); - - it("keeps an explicit model over the stored session value", () => { - const meta = baseMeta({ model: "anthropic:claude-opus-4-8" }); - const flags = applyNamedSessionDefaults(baseFlags({ model: "openai:gpt-5.5" }), meta); - expect(flags.model).toBe("openai:gpt-5.5"); - }); - - it("excludes refs sidecar files from listNamedSessions", async () => { - await writeNamedSession(baseMeta()); - await writeNamedSessionRefs("foo", { refCounter: 3, generations: [], refs: [] }); - const sessions = await listNamedSessions(); - expect(sessions).toHaveLength(1); - expect(sessions[0]?.name).toBe("foo"); - }); - - it("skips metadata entries missing required fields", async () => { - await writeNamedSession(baseMeta()); - const dir = join(process.env.XDG_DATA_HOME!, "cua", "named-sessions"); - writeFileSync(join(dir, "bogus.json"), JSON.stringify({ unrelated: true })); - writeFileSync(join(dir, "no-age.json"), JSON.stringify({ name: "no-age", kernel_session_id: "k1" })); - const sessions = await listNamedSessions(); - expect(sessions).toHaveLength(1); - expect(sessions[0]?.name).toBe("foo"); - }); -}); diff --git a/packages/cli/test/harness-sessions.test.ts b/packages/cli/test/harness-sessions.test.ts deleted file mode 100644 index 17f3440b..00000000 --- a/packages/cli/test/harness-sessions.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { mkdtempSync, writeFileSync } from "node:fs"; -import { mkdir } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { - createSession, - createSessionRepo, - findLatestSession, - listSessionsForCwd, - resolveSessionRef, -} from "../src/harness-sessions"; - -function freshRoot(): string { - return mkdtempSync(join(tmpdir(), "cua-cli-sessions-")); -} - -describe("JsonlSessionRepo-backed sessions", () => { - it("creates and lists sessions for a cwd", async () => { - const root = freshRoot(); - const cwd = mkdtempSync(join(tmpdir(), "cua-cli-cwd-")); - const repo = createSessionRepo(root); - await createSession(repo, cwd); - const sessions = await listSessionsForCwd(repo, cwd); - expect(sessions.length).toBe(1); - expect(sessions[0]?.cwd).toBe(cwd); - }); - - it("tolerates legacy / unknown files in the sessions root", async () => { - const root = freshRoot(); - const cwd = mkdtempSync(join(tmpdir(), "cua-cli-cwd-")); - const repo = createSessionRepo(root); - - // Create a session via the repo first so the root layout exists. - await createSession(repo, cwd); - - // Now drop a legacy file alongside it that does not match the v0.79 layout. - const legacy = join(root, "legacy-session.jsonl"); - writeFileSync(legacy, '{"role":"user","content":"hi"}\n', "utf8"); - const orphanDir = join(root, "definitely-not-a-session"); - await mkdir(orphanDir, { recursive: true }); - writeFileSync(join(orphanDir, "garbage.txt"), "noise", "utf8"); - - // list() must still succeed and return only the valid session. - const sessions = await listSessionsForCwd(repo, cwd); - expect(sessions.length).toBe(1); - }); - - it("resolves the latest session for a cwd", async () => { - const root = freshRoot(); - const cwd = mkdtempSync(join(tmpdir(), "cua-cli-cwd-")); - const repo = createSessionRepo(root); - await createSession(repo, cwd); - await new Promise((r) => setTimeout(r, 5)); - const second = await createSession(repo, cwd); - const latest = await findLatestSession(repo, cwd); - expect(latest?.id).toBe((await second.getMetadata()).id); - const viaLatest = await resolveSessionRef(repo, cwd, "latest"); - expect(viaLatest.id).toBe(latest?.id); - }); - - it("resolves by id prefix and errors on ambiguity / miss", async () => { - const root = freshRoot(); - const cwd = mkdtempSync(join(tmpdir(), "cua-cli-cwd-")); - const repo = createSessionRepo(root); - const created = await createSession(repo, cwd); - const id = (await created.getMetadata()).id; - const byPrefix = await resolveSessionRef(repo, cwd, id.slice(0, 6)); - expect(byPrefix.id).toBe(id); - await expect(resolveSessionRef(repo, cwd, "no-such")).rejects.toThrow(/no session matches/); - }); - - it("resolves an absolute --session from a different cwd", async () => { - const root = freshRoot(); - const originCwd = mkdtempSync(join(tmpdir(), "cua-cli-cwd-")); - const otherCwd = mkdtempSync(join(tmpdir(), "cua-cli-other-")); - const repo = createSessionRepo(root); - const created = await createSession(repo, originCwd); - const path = (await created.getMetadata()).path; - // Invoke resolution from a different cwd than the session was created in. - const resolved = await resolveSessionRef(repo, otherCwd, path); - expect(resolved.path).toBe(path); - expect(resolved.cwd).toBe(originCwd); - }); -}); diff --git a/packages/cli/test/harness-skills.test.ts b/packages/cli/test/harness-skills.test.ts deleted file mode 100644 index 3c01e0ec..00000000 --- a/packages/cli/test/harness-skills.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { NodeExecutionEnv } from "@onkernel/cua-agent"; -import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { discoverCuaSkills } from "../src/harness-skills"; - -/** - * Skill/context discovery is hermetic: pi's resource loader reads - * `$HOME/.agents/skills` and `/.agents/skills`, so each test isolates - * `HOME` and uses a fresh empty cwd plus an explicit temp `agentDir`. That way - * the only resources in scope are the fixtures the test writes. - */ -let originalHome: string | undefined; -let cwd: string; -let agentDir: string; - -beforeEach(() => { - originalHome = process.env.HOME; - const home = mkdtempSync(join(tmpdir(), "cua-home-")); - process.env.HOME = home; - cwd = mkdtempSync(join(tmpdir(), "cua-cwd-")); - agentDir = mkdtempSync(join(tmpdir(), "cua-agentdir-")); -}); - -afterEach(() => { - if (originalHome === undefined) delete process.env.HOME; - else process.env.HOME = originalHome; -}); - -function writeSkill(dir: string, name: string, description: string, body: string): void { - mkdirSync(dir, { recursive: true }); - writeFileSync(join(dir, "SKILL.md"), `---\nname: ${name}\ndescription: ${description}\n---\n${body}\n`); -} - -function writeSettings(packages: string[]): void { - writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ packages }, null, 2)); -} - -describe("discoverCuaSkills", () => { - it("discovers a skill bundled in a pi-installed package", async () => { - // A local package fixture mirrors what `pi install` leaves on disk: the - // package is recorded in settings.json and its skills live under - // /skills//SKILL.md. - const pkgDir = join(agentDir, "weather-pkg"); - writeSkill(join(pkgDir, "skills", "weather"), "weather", "Check the weather forecast.", "Run the weather workflow."); - writeSettings([pkgDir]); - - const env = new NodeExecutionEnv({ cwd }); - const result = await discoverCuaSkills({ cwd, env, agentDir }); - - const weather = result.skills.find((s) => s.name === "weather"); - expect(weather, "package skill should be discovered").toBeDefined(); - expect(weather?.content).toContain("Run the weather workflow."); - // The skill came from the package, not ~/.agents/skills (which is empty here). - expect(result.skills).toHaveLength(1); - }); - - it("loads each package skill once when a skills root mixes loose and nested skills", async () => { - // A skills root holding both a loose `.md` and a nested `/SKILL.md` - // must yield both skills exactly once (the nested skill's directory and - // the root both surface it). - const pkgDir = join(agentDir, "mixed-pkg"); - const skillsRoot = join(pkgDir, "skills"); - mkdirSync(skillsRoot, { recursive: true }); - writeFileSync(join(skillsRoot, "loose.md"), "---\nname: loose\ndescription: A loose skill.\n---\nLoose body.\n"); - writeSkill(join(skillsRoot, "nested"), "nested", "A nested skill.", "Nested body."); - writeSettings([pkgDir]); - - const env = new NodeExecutionEnv({ cwd }); - const result = await discoverCuaSkills({ cwd, env, agentDir }); - - expect(result.skills.map((s) => s.name).sort()).toEqual(["loose", "nested"]); - }); - - it("skips a configured-but-not-installed package without throwing or hanging", async () => { - // An npm package that was never installed. Resolution must not attempt a - // network install or block; the package is skipped and discovery returns - // cleanly with no skills. - writeSettings(["npm:@example/totally-not-installed-package"]); - - const env = new NodeExecutionEnv({ cwd }); - const result = await discoverCuaSkills({ cwd, env, agentDir }); - - expect(result.skills).toHaveLength(0); - }); - - it("discovers a project-local skill from /.agents/skills", async () => { - // Project settings stay untrusted, so pi's trusted project scan is off. - // The project skills dir must still be discovered (via additionalSkillPaths) - // without enabling untrusted `.pi/` extensions. - writeSkill(join(cwd, ".agents", "skills", "lint"), "lint", "Run the linter.", "Run the lint workflow."); - - const env = new NodeExecutionEnv({ cwd }); - const result = await discoverCuaSkills({ cwd, env, agentDir }); - - const lint = result.skills.find((s) => s.name === "lint"); - expect(lint, "project-local skill should be discovered").toBeDefined(); - expect(lint?.content).toContain("Run the lint workflow."); - }); - - it("loads skills from an explicit --skill path", async () => { - const extraDir = mkdtempSync(join(tmpdir(), "cua-extra-skill-")); - writeSkill(join(extraDir, "deploy"), "deploy", "Ship the build.", "Run the deploy steps."); - - const env = new NodeExecutionEnv({ cwd }); - const result = await discoverCuaSkills({ cwd, env, agentDir, extraPaths: [extraDir] }); - - expect(result.skills.map((s) => s.name)).toContain("deploy"); - }); - - it("returns no skills when disabled, but still loads context files", async () => { - const pkgDir = join(agentDir, "weather-pkg"); - writeSkill(join(pkgDir, "skills", "weather"), "weather", "Check the weather forecast.", "Run the weather workflow."); - writeSettings([pkgDir]); - writeFileSync(join(cwd, "AGENTS.md"), "# Project context\n\nBe concise.\n"); - - const env = new NodeExecutionEnv({ cwd }); - const result = await discoverCuaSkills({ cwd, env, agentDir, disabled: true }); - - expect(result.skills).toHaveLength(0); - expect(result.contextFiles.map((f) => f.path)).toContain(join(cwd, "AGENTS.md")); - }); - - it("loads an AGENTS.md context file from the cwd", async () => { - writeFileSync(join(cwd, "AGENTS.md"), "# Project context\n\nUse two-space indentation.\n"); - - const env = new NodeExecutionEnv({ cwd }); - const result = await discoverCuaSkills({ cwd, env, agentDir }); - - const agents = result.contextFiles.find((f) => f.path === join(cwd, "AGENTS.md")); - expect(agents, "AGENTS.md should be discovered").toBeDefined(); - expect(agents?.content).toContain("Use two-space indentation."); - }); -}); diff --git a/packages/cli/test/kimi-reasoning-payload.test.ts b/packages/cli/test/kimi-reasoning-payload.test.ts deleted file mode 100644 index d84fcefb..00000000 --- a/packages/cli/test/kimi-reasoning-payload.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { mkdtempSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { InMemorySessionRepo } from "@onkernel/cua-agent"; -import { createCuaModels, type CuaModelRef } from "@onkernel/cua-ai"; -import { mapThinkingLevel } from "../src/cli-harness"; -import { buildCuaHarness } from "../src/harness"; -import { createFakeKernelEnvironment } from "./fixtures/fake-kernel"; - -const SSE_CHUNKS = [ - 'data: {"id":"chatcmpl-k3","object":"chat.completion.chunk","created":1,"model":"kimi-k3","choices":[{"index":0,"delta":{"role":"assistant","content":"ok"},"finish_reason":null}]}\n\n', - 'data: {"id":"chatcmpl-k3","object":"chat.completion.chunk","created":1,"model":"kimi-k3","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}\n\n', - "data: [DONE]\n\n", -]; - -function sseResponse(): Response { - const body = new ReadableStream({ - start(controller) { - for (const chunk of SSE_CHUNKS) controller.enqueue(new TextEncoder().encode(chunk)); - controller.close(); - }, - }); - return new Response(body, { status: 200, headers: { "content-type": "text/event-stream" } }); -} - -/** Run one turn against the real pi provider with fetch stubbed, returning the request payloads. */ -async function capturePayloads(apiKeyEnv: string, model: CuaModelRef): Promise[]> { - vi.stubEnv(apiKeyEnv, "test-key"); - vi.stubGlobal("fetch", vi.fn(async () => sseResponse())); - const kernel = createFakeKernelEnvironment(); - const { harness } = buildCuaHarness({ - cwd: mkdtempSync(join(tmpdir(), "cua-kimi-payload-")), - client: kernel.client, - browser: kernel.browser, - session: await new InMemorySessionRepo().create(), - model, - thinkingLevel: mapThinkingLevel(undefined), - models: createCuaModels(), - tools: [], - }); - const payloads: Record[] = []; - harness.on("before_provider_payload", (event) => { - payloads.push(event.payload as Record); - return undefined; - }); - await harness.prompt("hi"); - return payloads; -} - -describe("Kimi K3 reasoning effort", () => { - afterEach(() => { - vi.unstubAllGlobals(); - vi.unstubAllEnvs(); - }); - - it("resolves the CLI default to low when --thinking is not passed", () => { - expect(mapThinkingLevel(undefined)).toBe("low"); - }); - - it("sends reasoning_effort: low to Moonshot at the CLI default thinking level", async () => { - const payloads = await capturePayloads("MOONSHOT_API_KEY", "moonshotai:kimi-k3"); - expect(payloads.length).toBe(1); - expect(payloads[0]?.model).toBe("kimi-k3"); - expect(payloads[0]?.reasoning_effort).toBe("low"); - }); - - it("sends reasoning effort through OpenRouter's nested reasoning object", async () => { - const payloads = await capturePayloads("OPENROUTER_API_KEY", "openrouter:moonshotai/kimi-k3"); - expect(payloads.length).toBe(1); - expect(payloads[0]?.model).toBe("moonshotai/kimi-k3"); - expect(payloads[0]?.reasoning).toEqual({ effort: "low" }); - }); -}); diff --git a/packages/cli/test/model-picker.test.ts b/packages/cli/test/model-picker.test.ts deleted file mode 100644 index 664c309c..00000000 --- a/packages/cli/test/model-picker.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { listCuaModels } from "@onkernel/cua-ai"; -import { - clampSelection, - filterModelsForPicker, - modelSearchText, - moveSelection, - sortModelsForPicker, - visibleWindow, -} from "../src/tui/model-picker"; - -const catalog = listCuaModels(); - -describe("modelSearchText", () => { - it("leads with the provider and repeats it, keeping the bare id last", () => { - const item = { ref: "openai:gpt-5.5" as const, provider: "openai" as const, model: "gpt-5.5", name: "GPT-5.5" }; - expect(modelSearchText(item)).toBe("openai openai:gpt-5.5 openai gpt-5.5 GPT-5.5"); - }); - - it("omits an empty name", () => { - const item = { ref: "openai:x" as const, provider: "openai" as const, model: "x", name: "" }; - expect(modelSearchText(item)).toBe("openai openai:x openai x"); - }); -}); - -describe("filterModelsForPicker", () => { - it("returns the whole catalog for an empty query", () => { - expect(filterModelsForPicker(catalog, "")).toHaveLength(catalog.length); - }); - - it("ranks an exact provider-qualified ref first", () => { - const target = catalog.find((m) => m.provider === "openai"); - expect(target).toBeDefined(); - const filtered = filterModelsForPicker(catalog, target!.ref); - expect(filtered[0]?.ref).toBe(target!.ref); - }); - - it("matches on the human-readable name across the colon-delimited ref", () => { - const google = catalog.filter((m) => m.provider === "google"); - expect(google.length).toBeGreaterThan(0); - const filtered = filterModelsForPicker(catalog, "google"); - // The full pi-ai catalog contains related providers (google-vertex), so - // the query narrows rather than isolating one provider. - expect(filtered.some((m) => m.provider === "google")).toBe(true); - expect(filtered.some((m) => m.provider === "anthropic")).toBe(false); - }); - - it("returns an empty list when nothing matches", () => { - expect(filterModelsForPicker(catalog, "zzzzz-no-such-model")).toEqual([]); - }); -}); - -describe("sortModelsForPicker", () => { - it("hoists the current ref and preserves catalog order for the rest", () => { - const current = catalog[catalog.length - 1]!; - const sorted = sortModelsForPicker(catalog, current.ref); - expect(sorted[0]?.ref).toBe(current.ref); - const rest = sorted.slice(1).map((m) => m.ref); - const expected = catalog.filter((m) => m.ref !== current.ref).map((m) => m.ref); - expect(rest).toEqual(expected); - }); - - it("leaves order untouched when the current ref is unknown", () => { - expect(sortModelsForPicker(catalog, undefined).map((m) => m.ref)).toEqual(catalog.map((m) => m.ref)); - }); -}); - -describe("moveSelection", () => { - it("wraps at both ends", () => { - expect(moveSelection(0, -1, 5)).toBe(4); - expect(moveSelection(4, 1, 5)).toBe(0); - expect(moveSelection(2, 1, 5)).toBe(3); - expect(moveSelection(2, -1, 5)).toBe(1); - }); - - it("is a no-op on an empty list", () => { - expect(moveSelection(0, 1, 0)).toBe(0); - expect(moveSelection(0, -1, 0)).toBe(0); - }); -}); - -describe("clampSelection", () => { - it("pulls the cursor into a list that shrank", () => { - expect(clampSelection(9, 3)).toBe(2); - expect(clampSelection(1, 3)).toBe(1); - expect(clampSelection(4, 0)).toBe(0); - }); -}); - -describe("visibleWindow", () => { - it("shows the whole list when it fits", () => { - expect(visibleWindow(0, 4, 10)).toEqual({ start: 0, end: 4 }); - }); - - it("centres the cursor and clamps at the tail", () => { - expect(visibleWindow(0, 30, 10)).toEqual({ start: 0, end: 10 }); - expect(visibleWindow(15, 30, 10)).toEqual({ start: 10, end: 20 }); - expect(visibleWindow(29, 30, 10)).toEqual({ start: 20, end: 30 }); - }); -}); diff --git a/packages/cli/test/mutation-queue.test.ts b/packages/cli/test/mutation-queue.test.ts deleted file mode 100644 index 959569a6..00000000 --- a/packages/cli/test/mutation-queue.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { createMutationQueue } from "../src/tui/mutation-queue"; - -/** A promise plus its resolve/reject handles, for driving interleavings. */ -function deferred(): { promise: Promise; resolve: (value: T) => void; reject: (err: unknown) => void } { - let resolve!: (value: T) => void; - let reject!: (err: unknown) => void; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { promise, resolve, reject }; -} - -describe("createMutationQueue", () => { - it("does not start a queued mutation until the previous one settles", async () => { - const queue = createMutationQueue(); - const first = deferred(); - const events: string[] = []; - - const a = queue.run(async () => { - events.push("a:start"); - await first.promise; - events.push("a:end"); - }); - const b = queue.run(async () => { - events.push("b:start"); - }); - - // `b` must not have begun while `a` is still suspended. This is the whole - // point: a `/tools` apply's setTools() must never land inside a `/model` - // switch's setModel()/setTools() pair. - await Promise.resolve(); - expect(events).toEqual(["a:start"]); - - first.resolve(); - await Promise.all([a, b]); - expect(events).toEqual(["a:start", "a:end", "b:start"]); - }); - - it("surfaces a mutation's rejection to its own caller only", async () => { - const queue = createMutationQueue(); - const failure = new Error("compile rejected"); - - const a = queue.run(async () => { - throw failure; - }); - const b = queue.run(async () => "ok"); - - await expect(a).rejects.toThrow("compile rejected"); - // A failed mutation must not wedge the queue or poison its successor. - await expect(b).resolves.toBe("ok"); - }); - - it("keeps ordering after a failure", async () => { - const queue = createMutationQueue(); - const events: string[] = []; - const blocked = deferred(); - - const a = queue.run(async () => { - events.push("a:start"); - await blocked.promise; - throw new Error("boom"); - }); - const b = queue.run(async () => { - events.push("b:start"); - }); - - await Promise.resolve(); - expect(events).toEqual(["a:start"]); - blocked.resolve(); - await expect(a).rejects.toThrow("boom"); - await b; - expect(events).toEqual(["a:start", "b:start"]); - }); - - it("returns each mutation's own resolved value", async () => { - const queue = createMutationQueue(); - const results = await Promise.all([queue.run(async () => 1), queue.run(async () => 2), queue.run(async () => 3)]); - expect(results).toEqual([1, 2, 3]); - }); - - it("drains to idle", async () => { - const queue = createMutationQueue(); - const gate = deferred(); - let done = false; - void queue.run(async () => { - await gate.promise; - done = true; - }); - gate.resolve(); - await queue.drain(); - expect(done).toBe(true); - }); -}); diff --git a/packages/cli/test/print.test.ts b/packages/cli/test/print.test.ts deleted file mode 100644 index 01f1e8f2..00000000 --- a/packages/cli/test/print.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { runPrint } from "../src/print"; -import { buildTestHarness, type TestHarnessFixture } from "./fixtures/harness"; - -let fixture: TestHarnessFixture | undefined; - -describe("runPrint", () => { - it("streams assistant text in plain text mode", async () => { - fixture = await buildTestHarness({ - turns: [ - { - steps: [{ type: "text", text: "Hello, world." }], - }, - ], - }); - const lines: string[] = []; - const exitCode = await runPrintIntoBuffer(fixture, "say hi", lines); - expect(exitCode).toBe(0); - expect(lines.join("\n")).toContain("Hello, world."); - }); - - it("emits jsonl with the documented session_created and run_complete envelope", async () => { - fixture = await buildTestHarness({ - turns: [ - { - steps: [{ type: "text", text: "ok" }], - }, - ], - }); - const events = await runPrintAsJsonl(fixture, "go"); - const types = events.map((e) => e.type); - expect(types[0]).toBe("session_created"); - expect(types).toContain("browser_created"); - expect(types).toContain("assistant_text_done"); - expect(types).toContain("turn_done"); - expect(types).toContain("run_complete"); - expect((events[0] as { schema_version: number }).schema_version).toBe(2); - }); - - it("emits assistant_usage with the billed-prompt cache hit ratio", async () => { - fixture = await buildTestHarness({ - turns: [ - { - steps: [{ type: "text", text: "ok" }], - usage: { input: 100, output: 20, cacheRead: 300, cacheWrite: 50, totalTokens: 420 }, - }, - ], - }); - const events = await runPrintAsJsonl(fixture, "go"); - const usage = events.find((e) => e.type === "assistant_usage") as Record; - expect(usage).toMatchObject({ - turn: 1, - input: 100, - output: 20, - cache_read: 300, - cache_write: 50, - total_tokens: 420, - }); - // billed prompt = input + cache_read + cache_write = 450; ratio = cache_read / billed prompt. - expect(usage.cache_hit_ratio).toBeCloseTo(300 / 450); - }); - - it("returns exit code 1 when the provider emits an error", async () => { - fixture = await buildTestHarness({ - turns: [ - { steps: [{ type: "error", message: "boom" }] }, - ], - }); - const lines: string[] = []; - const exitCode = await runPrintIntoBuffer(fixture, "fail", lines); - expect(exitCode).toBe(1); - }); - - it("emits tool_call and tool_result envelopes for tool turns in jsonl mode", async () => { - fixture = await buildTestHarness({ - turns: [ - { - steps: [ - { - type: "tool_call", - toolName: "click", - args: { x: 12, y: 34 }, - }, - ], - }, - { steps: [{ type: "text", text: "done" }] }, - ], - }); - const events = await runPrintAsJsonl(fixture, "click button"); - const types = events.map((e) => e.type); - expect(types).toContain("tool_call"); - expect(types).toContain("tool_result"); - const call = events.find((e) => e.type === "tool_call") as Record; - expect(call.tool_name).toBe("click"); - const result = events.find((e) => e.type === "tool_result") as Record; - expect(result.tool_name).toBe("click"); - // ok / call_id present on the result envelope, mirroring the documented schema. - expect(typeof result.ok).toBe("boolean"); - expect(typeof result.call_id).toBe("string"); - }); -}); - -async function runPrintIntoBuffer( - fixture: TestHarnessFixture, - prompt: string, - out: string[], -): Promise { - const stdoutWrite = process.stdout.write.bind(process.stdout); - const stderrWrite = process.stderr.write.bind(process.stderr); - const stdoutChunks: string[] = []; - process.stdout.write = ((chunk: string | Uint8Array): boolean => { - stdoutChunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString()); - return true; - }) as typeof process.stdout.write; - process.stderr.write = ((_chunk: string | Uint8Array): boolean => true) as typeof process.stderr.write; - try { - const code = await runPrint({ - harness: fixture.harness, - browserHandle: { - client: fixture.kernel.client, - browser: fixture.kernel.browser, - async close(): Promise {}, - }, - modelRef: "openai:gpt-5.5", - provider: "openai", - prompt, - }); - out.push(...stdoutChunks); - return code; - } finally { - process.stdout.write = stdoutWrite; - process.stderr.write = stderrWrite; - } -} - -async function runPrintAsJsonl( - fixture: TestHarnessFixture, - prompt: string, -): Promise>> { - const lines: string[] = []; - const stdoutWrite = process.stdout.write.bind(process.stdout); - process.stdout.write = ((chunk: string | Uint8Array): boolean => { - const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(); - for (const line of text.split("\n")) { - if (line.trim()) lines.push(line); - } - return true; - }) as typeof process.stdout.write; - try { - await runPrint({ - harness: fixture.harness, - browserHandle: { - client: fixture.kernel.client, - browser: fixture.kernel.browser, - async close(): Promise {}, - }, - modelRef: "openai:gpt-5.5", - provider: "openai", - prompt, - jsonlMode: true, - }); - } finally { - process.stdout.write = stdoutWrite; - } - return lines.map((line) => JSON.parse(line) as Record); -} diff --git a/packages/cli/test/slash-commands.test.ts b/packages/cli/test/slash-commands.test.ts deleted file mode 100644 index 247d9b9f..00000000 --- a/packages/cli/test/slash-commands.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { buildAutocompleteProvider, parseSlashCommand } from "../src/tui/slash-commands"; - -describe("parseSlashCommand", () => { - it("returns undefined for non-slash input", () => { - expect(parseSlashCommand("hello world")).toBeUndefined(); - expect(parseSlashCommand("")).toBeUndefined(); - }); - - it("parses /model with a provider:model argument", () => { - expect(parseSlashCommand("/model openai:gpt-5.5")).toEqual({ - command: "model", - argument: "openai:gpt-5.5", - }); - expect(parseSlashCommand("/model")).toEqual({ command: "model", argument: "" }); - }); - - it("parses /tools with and without an argument", () => { - expect(parseSlashCommand("/tools")).toEqual({ command: "tools", argument: "" }); - expect(parseSlashCommand("/tools something")).toEqual({ - command: "tools", - argument: "something", - }); - }); - - it("parses /thinking with a reasoning level", () => { - expect(parseSlashCommand("/thinking high")).toEqual({ - command: "thinking", - argument: "high", - }); - }); - - it("parses /compact", () => { - expect(parseSlashCommand("/compact")).toEqual({ command: "compact", argument: "" }); - }); - - it("parses /skill: with optional remainder", () => { - expect(parseSlashCommand("/skill:hello")).toEqual({ - command: "skill", - name: "hello", - remainder: "", - }); - expect(parseSlashCommand("/skill:hello with args")).toEqual({ - command: "skill", - name: "hello", - remainder: "with args", - }); - }); - - it("returns undefined for unknown slash commands", () => { - expect(parseSlashCommand("/totally-unknown-command")).toBeUndefined(); - }); -}); - -describe("buildAutocompleteProvider", () => { - it("offers /tools alongside the other built-in commands", async () => { - const provider = buildAutocompleteProvider(process.cwd(), []); - const result = await provider.getSuggestions(["/"], 0, 1, { - signal: new AbortController().signal, - }); - const names = (result?.items ?? []).map((item) => item.value); - expect(names).toContain("tools"); - expect(names).toContain("model"); - expect(names).toContain("thinking"); - expect(names).toContain("compact"); - }); -}); diff --git a/packages/cli/test/tool-revalidation.test.ts b/packages/cli/test/tool-revalidation.test.ts deleted file mode 100644 index 8b8834c1..00000000 --- a/packages/cli/test/tool-revalidation.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { defaultApplicationTools, defaultInteractionTools } from "../src/harness"; -import { describeMenu, selectedKeys, toolKey, toolsForSelection } from "../src/tui/tool-selection"; -import { buildTestHarness } from "./fixtures/harness"; - -/** - * The `/tools` picker applies a selection of the model's tool menu via - * `catalog.setTools()`. These tests pin the behavior the picker relies on: - * compile-and-validate happens before any mutation, so a rejected selection - * leaves the live catalog untouched. - */ -describe("/tools selection revalidation", () => { - it("accepts a partial Google native subset", async () => { - const modelRef = "google:gemini-3.6-flash"; - const baseline = [...defaultInteractionTools(modelRef), ...defaultApplicationTools()]; - const fixture = await buildTestHarness({ turns: [], modelRef, tools: baseline }); - - const items = describeMenu(modelRef, defaultApplicationTools(), baseline); - const dropped = items.find((item) => item.group === "native" && item.available)!; - const next = baseline.filter((tool) => toolKey(tool) !== dropped.key); - - await fixture.catalog.setTools(next); - expect(fixture.catalog.getTools().map(toolKey)).toEqual(next.map(toolKey)); - expect(fixture.catalog.getTools().map(toolKey)).not.toContain(dropped.key); - }); - - it("rejects a duplicated tool name and leaves the catalog unchanged", async () => { - const modelRef = "google:gemini-3.6-flash"; - const baseline = [...defaultInteractionTools(modelRef), ...defaultApplicationTools()]; - const fixture = await buildTestHarness({ turns: [], modelRef, tools: baseline }); - const before = fixture.catalog.getTools().map(toolKey); - - const [first] = baseline; - await expect(fixture.catalog.setTools([...baseline, first!])).rejects.toThrow(/requested more than once/); - // Atomicity: the failed compile must not have mutated live state. - expect(fixture.catalog.getTools().map(toolKey)).toEqual(before); - }); - - it("accepts dropping the whole Google native group", async () => { - const modelRef = "google:gemini-3.6-flash"; - const baseline = [...defaultInteractionTools(modelRef), ...defaultApplicationTools()]; - const fixture = await buildTestHarness({ turns: [], modelRef, tools: baseline }); - - const items = describeMenu(modelRef, defaultApplicationTools(), baseline); - const nativeKeys = new Set(items.filter((item) => item.group === "native").flatMap((item) => item.tools.map(toolKey))); - const next = baseline.filter((tool) => !nativeKeys.has(toolKey(tool))); - - await fixture.catalog.setTools(next); - expect(fixture.catalog.getTools().map(toolKey)).toEqual(next.map(toolKey)); - }); - - it("accepts an empty selection (text-only agent)", async () => { - const modelRef = "openai:gpt-5.6-sol"; - const baseline = [...defaultInteractionTools(modelRef), ...defaultApplicationTools()]; - const fixture = await buildTestHarness({ turns: [], modelRef, tools: baseline }); - - await fixture.catalog.setTools([]); - expect(fixture.catalog.getTools()).toEqual([]); - }); - - it("recomposes the baseline after a model switch across providers", async () => { - const cwd = process.cwd(); - const from = "openai:gpt-5.6-sol"; - const to = "anthropic:claude-opus-5"; - const application = defaultApplicationTools(); - const fixture = await buildTestHarness({ - turns: [], - modelRef: from, - tools: [...defaultInteractionTools(from), ...application], - }); - - // Mirrors switchModel(): the new model and its interaction catalog compile - // as one pair, because the selected tools decide the transport. - await fixture.catalog.setModelAndTools(to, [...defaultInteractionTools(to), ...application]); - - const expected = [...defaultInteractionTools(to), ...application].map(toolKey); - expect(fixture.catalog.getTools().map(toolKey)).toEqual(expected); - expect(fixture.harness.getModel().provider).toBe("anthropic"); - }); - - it("adds a tool the application never composed", async () => { - const modelRef = "openai:gpt-5.6-sol"; - const baseline = [...defaultInteractionTools(modelRef), ...defaultApplicationTools()]; - const fixture = await buildTestHarness({ turns: [], modelRef, tools: baseline }); - expect(baseline.some((tool) => tool.name === "playwright_execute")).toBe(false); - - // The picker offers the model's whole menu, not just the baseline, so a - // selection can grow past what the CLI composed. - const items = describeMenu(modelRef, defaultApplicationTools(), baseline); - const playwright = items.find((item) => item.label === "playwright_execute")!; - expect(playwright.available).toBe(true); - - const enabled = new Set([...selectedKeys(items, baseline), playwright.key]); - await fixture.catalog.setTools(toolsForSelection(items, enabled)); - expect(fixture.catalog.getTools().map((tool) => tool.name)).toContain("playwright_execute"); - }); -}); diff --git a/packages/cli/test/tool-selection.test.ts b/packages/cli/test/tool-selection.test.ts deleted file mode 100644 index a3665138..00000000 --- a/packages/cli/test/tool-selection.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { cua } from "@onkernel/cua-ai"; -import { defaultApplicationTools, defaultInteractionTools } from "../src/harness"; -import { - describeMenu, - disableTools, - selectedKeys, - toolsForSelection, - enableTools, - sameSelection, - toggleTool, - toolKey, - toolSearchText, -} from "../src/tui/tool-selection"; - -const cwd = process.cwd(); - -describe("toolKey", () => { - it("uses a spec's own identity", () => { - expect(toolKey(cua.tools.browser.snapshot())).toBe("cua.browser.snapshot.v1"); - expect(toolKey(cua.tools.browser.act())).toBe("cua.browser.act.v1"); - }); - - it("namespaces a plain caller tool as caller.", () => { - const coding = defaultApplicationTools(); - expect(coding.length).toBeGreaterThan(0); - for (const tool of coding) { - expect(toolKey(tool)).toBe(`caller.${tool.name}`); - } - }); -}); - -describe("describeMenu", () => { - it("offers the model's whole menu, not just what the application composed", () => { - const baseline = [...defaultInteractionTools("openai:gpt-5.6-sol"), ...defaultApplicationTools()]; - const items = describeMenu("openai:gpt-5.6-sol", defaultApplicationTools(), baseline); - expect(items.length).toBeGreaterThan(baseline.length); - // Not composed by the CLI for this model, but offerable. - expect(items.some((item) => item.label === "playwright_execute")).toBe(true); - expect(items.some((item) => item.label === "computer_click")).toBe(true); - }); - - it("marks what the live selection already holds", () => { - const baseline = [...defaultInteractionTools("openai:gpt-5.6-sol"), ...defaultApplicationTools()]; - const items = describeMenu("openai:gpt-5.6-sol", defaultApplicationTools(), baseline); - const snapshot = items.find((item) => item.label === "browser_snapshot")!; - expect(snapshot.available).toBe(true); - expect(items.filter((item) => item.group === "application").every((item) => item.available)).toBe(true); - }); - - it("marks a tool the model cannot take, with the compiler's reason", () => { - const items = describeMenu("google:gemini-3.6-flash", defaultApplicationTools(), []); - const waitFor = items.find((item) => item.label === "browser_wait_for")!; - expect(waitFor.available).toBe(false); - expect(waitFor.unavailableReason).toContain("does not accept the schema"); - - const openaiNative = items.filter((item) => item.group === "native" && !item.available); - expect(openaiNative.length).toBeGreaterThan(0); - }); - - it("keeps the caller's configured spec for a row already installed", () => { - // The CLI enables `javascript` on Anthropic's native browser. A row that is - // already installed must contribute that exact object, or a no-op apply - // would rebuild the catalog without the option. - const model = "anthropic:claude-opus-5" as const; - const application = defaultApplicationTools(); - const baseline = [...defaultInteractionTools(model), ...application]; - const live = baseline.find((tool) => tool.name === "browser")!; - const items = describeMenu(model, application, baseline); - const row = items.find((item) => item.label === "browser" && item.group === "native")!; - expect(row.tools[0]).toBe(live); - - const applied = toolsForSelection(items, selectedKeys(items, baseline)); - expect(applied.find((tool) => tool.name === "browser")).toBe(live); - }); - - it("labels provider-native, cua, and application groups", () => { - const items = describeMenu("google:gemini-3.6-flash", defaultApplicationTools(), []); - const groups = new Set(items.map((item) => item.group)); - expect(groups.has("native")).toBe(true); - expect(groups.has("application")).toBe(true); - expect(groups.has("browser")).toBe(true); - }); -}); - -describe("toolSearchText", () => { - it("covers the label, group, and identity", () => { - const items = describeMenu("openai:gpt-5.6-sol", [], []); - const item = items.find((entry) => entry.label === "browser_snapshot")!; - const text = toolSearchText(item); - expect(text).toContain(item.label); - expect(text).toContain("browser"); - expect(text).toContain("cua.browser.snapshot.v1"); - }); -}); - -describe("selection state machine", () => { - const baseline = [...defaultInteractionTools("openai:gpt-5.6-sol"), ...defaultApplicationTools()]; - const items = describeMenu("openai:gpt-5.6-sol", defaultApplicationTools(), baseline); - const allKeys = items.map((item) => item.key); - - it("toggles a single tool off and back on", () => { - const target = allKeys[0]!; - const off = toggleTool(new Set(allKeys), target); - expect(off.has(target)).toBe(false); - expect(off.size).toBe(allKeys.length - 1); - const on = toggleTool(off, target); - expect(sameSelection(on, new Set(allKeys))).toBe(true); - }); - - it("enables and clears in bulk", () => { - expect(disableTools(new Set(allKeys), allKeys).size).toBe(0); - expect(sameSelection(enableTools(new Set(), allKeys), new Set(allKeys))).toBe(true); - }); - - it("restricts bulk actions to the keys it is given", () => { - const subset = allKeys.slice(0, 2); - const cleared = disableTools(new Set(allKeys), subset); - expect(cleared.size).toBe(allKeys.length - 2); - for (const key of subset) expect(cleared.has(key)).toBe(false); - }); -}); - -describe("sameSelection", () => { - it("compares by membership, not order", () => { - expect(sameSelection(new Set(["a", "b"]), new Set(["b", "a"]))).toBe(true); - expect(sameSelection(new Set(["a"]), new Set(["a", "b"]))).toBe(false); - expect(sameSelection(new Set(), new Set())).toBe(true); - }); -}); diff --git a/packages/cli/test/tools-picker.test.ts b/packages/cli/test/tools-picker.test.ts deleted file mode 100644 index 4f0c5df9..00000000 --- a/packages/cli/test/tools-picker.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { beforeAll, describe, expect, it } from "vitest"; -import { getKeybindings, type TUI } from "@earendil-works/pi-tui"; -import { initTheme } from "@earendil-works/pi-coding-agent"; -import { installCuaKeybindings } from "../src/tui/keybindings"; -import { ToolsPickerComponent } from "../src/tui/tools-picker"; -import type { ToolSelectionItem } from "../src/tui/tool-selection"; - -/** - * The picker only ever calls `requestRender()` on its TUI, so a counter stands - * in for the real terminal and keeps these tests free of a PTY. - */ -function fakeTui(): TUI { - return { requestRender: () => {} } as unknown as TUI; -} - -const items: readonly ToolSelectionItem[] = [ - { key: "cua.browser.snapshot", label: "browser_snapshot", group: "browser", description: "Capture a page snapshot", available: true, tools: [] }, - { key: "cua.browser.act", label: "browser_act", group: "browser", description: "Run an action plan", available: true, tools: [] }, - { key: "caller.read_file", label: "read_file", group: "application", description: "Read a stale ref safely", available: true, tools: [] }, -]; - -interface Harness { - picker: ToolsPickerComponent; - applied: ReadonlySet[]; - cancels: number; -} - -function mount(enabled: readonly string[] = items.map((item) => item.key)): Harness { - installCuaKeybindings(); - const applied: ReadonlySet[] = []; - let cancels = 0; - const picker = new ToolsPickerComponent({ - tui: fakeTui(), - items, - enabledKeys: new Set(enabled), - defaultKeys: new Set(items.map((item) => item.key)), - onApply: (next) => applied.push(next), - onCancel: () => { - cancels += 1; - }, - }); - return { - picker, - applied, - get cancels() { - return cancels; - }, - }; -} - -const CTRL_A = "\x01"; -const CTRL_C = "\x03"; -const CTRL_S = "\x13"; -const CTRL_X = "\x18"; -const ESCAPE = "\x1b"; -const ENTER = "\r"; - -// The picker bakes themed strings into its Text children, so the theme has to -// exist before one is constructed — exactly as `runInteractive` arranges. -beforeAll(() => { - initTheme(); -}); - -describe("ToolsPickerComponent input handling", () => { - it("toggles on space while the search box is empty", () => { - const h = mount(); - h.picker.handleInput(" "); - h.picker.handleInput(CTRL_S); - // The first row was toggled off, not typed into the search box. - expect(h.picker.getSearchInput().getValue()).toBe(""); - expect([...(h.applied[0] ?? [])]).toEqual(["cua.browser.act", "caller.read_file"]); - }); - - it("types spaces into a non-empty search instead of toggling", () => { - const h = mount(); - // Descriptions are searchable, so a multi-word query has to be typeable. - for (const ch of "stale ref") h.picker.handleInput(ch); - expect(h.picker.getSearchInput().getValue()).toBe("stale ref"); - h.picker.handleInput(CTRL_S); - // Nothing was toggled: the space landed in the query. - expect([...(h.applied[0] ?? [])].sort()).toEqual(items.map((item) => item.key).sort()); - }); - - it("still toggles with enter while a search is active", () => { - const h = mount(); - for (const ch of "stale ref") h.picker.handleInput(ch); - h.picker.handleInput(ENTER); - h.picker.handleInput(CTRL_S); - expect([...(h.applied[0] ?? [])]).toEqual(["cua.browser.snapshot", "cua.browser.act"]); - }); - - it("clears an active search on ctrl+c before cancelling", () => { - const h = mount(); - for (const ch of "act") h.picker.handleInput(ch); - h.picker.handleInput(CTRL_C); - expect(h.picker.getSearchInput().getValue()).toBe(""); - expect(h.cancels).toBe(0); - // A second ctrl+c, with the search empty, cancels. - h.picker.handleInput(CTRL_C); - expect(h.cancels).toBe(1); - }); - - it("cancels on escape even with an active search", () => { - const h = mount(); - for (const ch of "act") h.picker.handleInput(ch); - h.picker.handleInput(ESCAPE); - expect(h.cancels).toBe(1); - expect(h.applied).toHaveLength(0); - }); - - it("never applies on cancel", () => { - const h = mount(); - h.picker.handleInput(" "); - h.picker.handleInput(ESCAPE); - expect(h.applied).toHaveLength(0); - }); - - it("scopes bulk actions to an active filter", () => { - const h = mount([]); - for (const ch of "browser") h.picker.handleInput(ch); - h.picker.handleInput(CTRL_A); - h.picker.handleInput(CTRL_S); - // read_file was filtered out, so ctrl+a left it disabled. - expect([...(h.applied[0] ?? [])].sort()).toEqual(["cua.browser.act", "cua.browser.snapshot"]); - }); - - it("clears every listed row with ctrl+x and allows an empty selection", () => { - const h = mount(); - h.picker.handleInput(CTRL_X); - h.picker.handleInput(CTRL_S); - expect([...(h.applied[0] ?? [])]).toEqual([]); - }); - - it("honours a rebound tui.select.cancel so the footer hint stays truthful", () => { - const h = mount(); - const kb = getKeybindings(); - try { - kb.setUserBindings({ "tui.select.cancel": "ctrl+g" }); - // The rebound key cancels... - h.picker.handleInput("\x07"); - expect(h.cancels).toBe(1); - // ...and the key it replaced no longer does. - h.picker.handleInput("\x1b"); - expect(h.cancels).toBe(1); - } finally { - kb.setUserBindings({}); - } - }); -}); diff --git a/packages/cli/test/tui-keybindings.test.ts b/packages/cli/test/tui-keybindings.test.ts deleted file mode 100644 index 9e686da0..00000000 --- a/packages/cli/test/tui-keybindings.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { getKeybindings } from "@earendil-works/pi-tui"; -import { cuaKeyText, installCuaKeybindings } from "../src/tui/keybindings"; -import { fitMaxVisible, PICKER_MAX_VISIBLE } from "../src/tui/model-picker"; - -describe("installCuaKeybindings", () => { - it("registers cua.tools.* ids so the pickers can match them", () => { - installCuaKeybindings(); - const kb = getKeybindings(); - // Without registration these resolve to [] and the bulk actions silently - // do nothing. - expect(kb.getKeys("cua.tools.enableAll")).toEqual(["ctrl+a"]); - expect(kb.getKeys("cua.tools.clearAll")).toEqual(["ctrl+x"]); - expect(kb.getKeys("cua.tools.reset")).toEqual(["ctrl+r"]); - expect(kb.getKeys("cua.tools.apply")).toEqual(["ctrl+s"]); - expect(kb.matches("\x01", "cua.tools.enableAll")).toBe(true); - expect(kb.matches("\x13", "cua.tools.apply")).toBe(true); - }); - - it("keeps pi's base bindings intact", () => { - installCuaKeybindings(); - const kb = getKeybindings(); - expect(kb.getKeys("tui.select.confirm")).toEqual(["enter"]); - expect(kb.getKeys("tui.select.cancel")).toEqual(["escape", "ctrl+c"]); - }); -}); - -describe("cuaKeyText", () => { - it("renders hint text for cua and pi ids alike", () => { - installCuaKeybindings(); - expect(cuaKeyText("cua.tools.apply")).toBe("ctrl+s"); - expect(cuaKeyText("cua.tools.enableAll")).toBe("ctrl+a"); - expect(cuaKeyText("tui.select.confirm")).toBe("enter"); - expect(cuaKeyText("tui.select.cancel")).toBe("escape/ctrl+c"); - }); -}); - -describe("fitMaxVisible", () => { - it("uses the full height cap on a roomy terminal", () => { - expect(fitMaxVisible(50, 22)).toBe(PICKER_MAX_VISIBLE); - }); - - it("shrinks the list rather than overflowing a short terminal", () => { - expect(fitMaxVisible(30, 22)).toBe(8); - expect(fitMaxVisible(24, 22)).toBe(3); - expect(fitMaxVisible(10, 22)).toBe(3); - }); -}); diff --git a/packages/cli/test/tui.fixture.test.ts b/packages/cli/test/tui.fixture.test.ts deleted file mode 100644 index 94a8fd47..00000000 --- a/packages/cli/test/tui.fixture.test.ts +++ /dev/null @@ -1,544 +0,0 @@ -import { describe, test } from "vitest"; -import { fileURLToPath } from "node:url"; -import { existsSync } from "node:fs"; -import { strict as assert } from "node:assert"; -import { createRequire } from "node:module"; -import { dirname, resolve } from "node:path"; -import type { CuaModelRef } from "@onkernel/cua-ai"; -import { defaultApplicationTools, defaultInteractionTools } from "../src/harness"; -import { describeMenu } from "../src/tui/tool-selection"; - -/** - * Drive the interactive TUI through ptywright with a scripted provider sitting - * below the real harness. The runner script ({@link tuiRunnerPath}) - * registers the scripted provider, assembles the harness via the production - * {@link buildCuaHarness}, and starts {@link runInteractive}. Each test case - * spawns a fresh process with its own per-scenario fixture JSON so the - * scripted provider's sequential turn replay never crosses scenarios. - * - * ptywright requires a native ghostty-vt binding (built via Zig). When that - * binding is missing the suite is skipped by default; set PTYWRIGHT_REQUIRED=1 - * (CI uses this) to turn the silent skip into a failure. - */ - -const tuiRunnerPath = fileURLToPath(new URL("./fixtures/tui-fixture-runner.ts", import.meta.url)); -const require = createRequire(import.meta.url); -const tsxCliPath = require.resolve("tsx/cli"); -const fixtureDir = fileURLToPath(new URL("./fixtures/tui-fixtures/", import.meta.url)); -const cwd = fileURLToPath(new URL("../", import.meta.url)); - -const ptywrightDist = fileURLToPath(new URL("../../ptywright/dist/index.js", import.meta.url)); -const ptywrightNative = resolve(dirname(ptywrightDist), "..", "native", "build", "Release", "ptywright_native.node"); -const ptywrightAvailable = existsSync(ptywrightNative); - -if (!ptywrightAvailable && process.env.PTYWRIGHT_REQUIRED) { - throw new Error( - `ptywright native binding not found at ${ptywrightNative}; build with 'npm run build --workspace @onkernel/ptywright' or unset PTYWRIGHT_REQUIRED`, - ); -} - -const suite = ptywrightAvailable ? describe : describe.skip; -const WAIT_MS = 15_000; -/** - * Pickers render their frame across several component updates, so snapshot - * assertions must wait for the screen to settle first. - */ -const STABLE_MS = 250; - -/** - * The baseline size the `/tools` picker reports for a `tools: true` fixture, - * derived from the same production defaults the fixture runner assembles. - * Deriving it keeps these assertions from churning whenever a default tool is - * added or removed, while still pinning the exact number the picker shows. - */ -function baselineToolCount(modelRef: string): number { - return defaultInteractionTools(modelRef as CuaModelRef).length + defaultApplicationTools().length; -} - -/** - * Rows the picker can select for a model. The footer counts selectable rows, - * not the baseline: `/tools` offers the model's whole menu, of which the - * application-composed baseline is just the part enabled on open. - */ -function selectableToolCount(modelRef: string): number { - const application = defaultApplicationTools(); - const baseline = [...defaultInteractionTools(modelRef as CuaModelRef), ...application]; - return describeMenu(modelRef as CuaModelRef, application, baseline).filter((item) => item.available).length; -} - -suite("TUI ptywright scenarios", () => { - test("streams assistant text into the message list", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady } = await loadPtywrightHelpers(); - const session = spawnFixture("streaming.json"); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - - // The pi-styled preamble renders the "cua v" logo and a - // key-hint row reflecting cua's real bindings. - const preamble = session.snapshot(); - assert.match(preamble.visible, /cua v/); - assert.match(preamble.visible, /to interrupt/); - assert.match(preamble.visible, /for commands/); - - session.line("say hi"); - await session.waitForVisible("fixture response", { timeoutMs: WAIT_MS }); - - const snapshot = session.snapshot(); - assert.match(snapshot.visible, /say hi/); - assert.match(snapshot.visible, /fixture response/); - - await exitFixture(session); - }); - - test("renders [Context] and [Skills] sections and no [Extensions]", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady } = await loadPtywrightHelpers(); - const session = spawnFixture("resources.json"); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - - const snapshot = session.snapshot(); - assert.match(snapshot.visible, /\[Context\]/); - assert.match(snapshot.visible, /AGENTS\.md/); - assert.match(snapshot.visible, /\[Skills\]/); - assert.match(snapshot.visible, /deploy-skill/); - assert.match(snapshot.visible, /review-skill/); - assert.doesNotMatch(snapshot.visible, /\[Extensions\]/); - - await exitFixture(session); - }); - - test("keeps multiline drafts left aligned", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady, KeyEnter } = await loadPtywrightHelpers(); - const session = spawnFixture("multiline.json"); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - session.send("first line\\"); - session.press(KeyEnter); - session.send("second line"); - await session.waitForVisible("second line", { timeoutMs: WAIT_MS }); - - const beforeSubmit = session.snapshot(); - assert.match(beforeSubmit.visible, /^second line/m); - assert.doesNotMatch(beforeSubmit.visible, /^\s+second line/m); - - session.press(KeyEnter); - await session.waitForVisible("multiline ok", { timeoutMs: WAIT_MS }); - - await exitFixture(session); - }); - - test("queues input during a running turn for the next agent step", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady } = await loadPtywrightHelpers(); - const session = spawnFixture("steer.json"); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - session.line("start the turn"); - await session.waitForVisible("working...", { timeoutMs: WAIT_MS }); - - session.line("use this next"); - await session.waitForVisible("queued for the next available turn", { timeoutMs: WAIT_MS }); - await session.waitForVisible("queued response", { timeoutMs: WAIT_MS }); - - const snapshot = session.snapshot(); - assert.match(snapshot.visible, /use this next/); - assert.doesNotMatch(snapshot.visible, /AgentHarness is busy/); - - await exitFixture(session); - }); - - test("escape interrupts and immediately sends queued input", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady, KeyEscape } = await loadPtywrightHelpers(); - const session = spawnFixture("abort.json"); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - session.line("please run forever"); - await session.waitForVisible("working...", { timeoutMs: WAIT_MS }); - - session.line("switch to this instead"); - await session.waitForVisible("queued for the next available turn", { timeoutMs: WAIT_MS }); - session.press(KeyEscape); - await session.waitForVisible("turn interrupted; sending 1 queued message", { timeoutMs: WAIT_MS }); - await session.waitForVisible("fixture response", { timeoutMs: WAIT_MS }); - - await exitFixture(session); - }); - - test("ctrl+c cancels an escape-triggered queued replay", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady, KeyCtrlC, KeyEscape } = await loadPtywrightHelpers(); - const session = spawnFixture("interrupt-cancel.json"); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - session.line("please run forever"); - await session.waitForVisible("working...", { timeoutMs: WAIT_MS }); - session.line("do not replay this"); - await session.waitForVisible("queued for the next available turn", { timeoutMs: WAIT_MS }); - - session.press(KeyEscape); - await session.waitForVisible("interrupting…", { timeoutMs: WAIT_MS }); - session.press(KeyCtrlC); - await session.waitForVisible("aborted", { timeoutMs: WAIT_MS }); - session.line("recover after cancelling replay"); - await session.waitForVisible("queued for after abort", { timeoutMs: WAIT_MS }); - await session.waitForVisible("fixture response", { timeoutMs: WAIT_MS }); - assert.doesNotMatch(session.snapshot().visible, /turn interrupted; sending 1 queued message/); - - await exitFixture(session); - }); - - test("refuses slash commands while a turn is running", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady, KeyCtrlC, KeyEnter } = await loadPtywrightHelpers(); - const session = spawnFixture("abort.json"); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - session.line("please run forever"); - await session.waitForVisible("working...", { timeoutMs: WAIT_MS }); - session.send("/thinking high"); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - session.press(KeyEnter); - session.press(KeyEnter); - await session.waitForVisible("/thinking is unavailable while a turn is running", { timeoutMs: WAIT_MS }); - - session.press(KeyCtrlC); - await session.waitForVisible("aborted", { timeoutMs: WAIT_MS }); - await exitFixture(session); - }); - - test("aborts a running turn with ctrl+c and recovers on the next prompt", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady, KeyCtrlC } = await loadPtywrightHelpers(); - const session = spawnFixture("abort.json"); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - session.line("please run forever"); - await session.waitForVisible("working...", { timeoutMs: WAIT_MS }); - - session.press(KeyCtrlC); - await session.waitForVisible("aborted", { timeoutMs: WAIT_MS }); - - session.line("recover after abort"); - await session.waitForVisible("fixture response", { timeoutMs: WAIT_MS }); - - await exitFixture(session); - }); - - test("opens a searchable model picker for /model with no argument", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady, KeyArrowDown, KeyEscape } = await loadPtywrightHelpers(); - const session = spawnFixture("model-picker.json", { rows: 50 }); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - session.line("/model"); - await session.waitForVisible("Model Name:", { timeoutMs: WAIT_MS }); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - - // pi's row format: cursor arrow, provider badge, and a check on the current model. - const opened = session.snapshot(); - assert.match(opened.visible, /gpt-5\.5 \[openai\]/); - assert.match(opened.visible, /→ /); - assert.match(opened.visible, /✓/); - - // Typing filters the list; the fuzzy match ranks Gemini models first and - // drops the previously-listed current model. - session.send("gemini"); - await session.waitForVisible("[google]", { timeoutMs: WAIT_MS }); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - const filtered = session.snapshot(); - assert.match(filtered.visible, /→ gemini-/); - assert.doesNotMatch(filtered.visible, /gpt-5\.5 \[openai\]/); - - session.press(KeyArrowDown); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - assert.match(session.snapshot().visible, /\[google\]/); - - // Escape closes without switching, and hands focus back to the editor. - session.press(KeyEscape); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - const closed = session.snapshot(); - assert.doesNotMatch(closed.visible, /Model Name:/); - assert.doesNotMatch(closed.visible, /model → /); - - session.line("say hi"); - await session.waitForVisible("fixture response", { timeoutMs: WAIT_MS }); - - await exitFixture(session); - }); - - test("cancels the model picker with ctrl+c and selects with enter", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady, KeyCtrlC, KeyEnter } = await loadPtywrightHelpers(); - const session = spawnFixture("model-picker-cancel.json", { rows: 50 }); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - session.line("/model"); - await session.waitForVisible("Model Name:", { timeoutMs: WAIT_MS }); - - // Regression: the global input listener must not treat ctrl+c as "quit" - // while a picker owns the keyboard. - session.press(KeyCtrlC); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - assert.doesNotMatch(session.snapshot().visible, /Model Name:/); - - // The process is still alive: a prompt still round-trips. - session.line("say hi"); - await session.waitForVisible("fixture response", { timeoutMs: WAIT_MS }); - - session.line("/model"); - await session.waitForVisible("Model Name:", { timeoutMs: WAIT_MS }); - session.send("gpt-5.6-sol"); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - session.press(KeyEnter); - await session.waitForVisible("model → openai:gpt-5.6-sol", { timeoutMs: WAIT_MS }); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - assert.doesNotMatch(session.snapshot().visible, /Model Name:/); - - await exitFixture(session); - }); - - test("keeps /model non-interactive and prefills the picker for an unknown ref", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady, KeyEscape } = await loadPtywrightHelpers(); - const session = spawnFixture("model-arg.json", { rows: 50 }); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - - // An explicit ref switches directly; the picker never opens. - await submitCommand(session, "/model openai:gpt-5.6-sol"); - await session.waitForVisible("model → openai:gpt-5.6-sol", { timeoutMs: WAIT_MS }); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - assert.doesNotMatch(session.snapshot().visible, /Model Name:/); - - // An unresolvable ref still reports the error, then offers the picker - // prefilled with what was typed. - await submitCommand(session, "/model nope-not-a-real-model"); - await session.waitForVisible("No matching models", { timeoutMs: WAIT_MS }); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - const prefilled = session.snapshot(); - assert.match(prefilled.visible, /unknown model/); - assert.match(prefilled.visible, /nope-not-a-real-model/); - - session.press(KeyEscape); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - - await exitFixture(session); - }); - - test("toggles tools through /tools, discarding staged edits on cancel", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady, KeyArrowDown, KeyCtrlA, KeyCtrlR, KeyCtrlS, KeyCtrlX, KeyEscape } = - await loadPtywrightHelpers(); - const session = spawnFixture("tools-picker.json", { rows: 50 }); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - session.line("/tools"); - await session.waitForVisible("Tool Configuration", { timeoutMs: WAIT_MS }); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - - const opened = session.snapshot(); - assert.match(opened.visible, /browser_snapshot/); - assert.match(opened.visible, /✓ enabled/); - // Keyboard-only controls are advertised, including the cua-specific ones. - assert.match(opened.visible, /ctrl\+s apply/); - assert.match(opened.visible, /ctrl\+a all/); - const baseline = baselineToolCount("openai:gpt-5.5"); - const selectable = selectableToolCount("openai:gpt-5.5"); - assert.ok(selectable > baseline, "the menu offers more than the composed baseline"); - assert.match(opened.visible, new RegExp(`${baseline}/${selectable} enabled`)); - - // Stage a toggle, then cancel: live state must be untouched. - session.send(" "); - await session.waitForVisible("✗ disabled", { timeoutMs: WAIT_MS }); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - assert.match(session.snapshot().visible, /unapplied/); - session.press(KeyEscape); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - const cancelled = session.snapshot(); - assert.doesNotMatch(cancelled.visible, /Tool Configuration/); - assert.doesNotMatch(cancelled.visible, /tools → /); - - // Reopening shows the discarded edit is gone. - session.line("/tools"); - await session.waitForVisible("Tool Configuration", { timeoutMs: WAIT_MS }); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - assert.doesNotMatch(session.snapshot().visible, /✗ disabled/); - - // Now toggle and apply for real. - session.press(KeyArrowDown); - session.send(" "); - await session.waitForVisible("✗ disabled", { timeoutMs: WAIT_MS }); - session.press(KeyCtrlS); - await session.waitForVisible(`tools → ${baseline - 1} enabled`, { timeoutMs: WAIT_MS }); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - assert.doesNotMatch(session.snapshot().visible, /Tool Configuration/); - - // The applied selection persists: reopening shows the disabled row and the - // reduced count, and the picker's baseline is still the full default list. - session.line("/tools"); - await session.waitForVisible("Tool Configuration", { timeoutMs: WAIT_MS }); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - const reopened = session.snapshot(); - assert.match(reopened.visible, /✗ disabled/); - assert.match(reopened.visible, new RegExp(`${baseline - 1}/${selectable} enabled`)); - - // ctrl+a enables every selectable row — including tools the application - // never composed — and ctrl+x clears it; both are staged. - session.press(KeyCtrlA); - // No `waitForVisible` here: the footer already reads `…/${selectable} - // enabled`, so a substring wait would resolve on the pre-keypress screen. - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - const enabledAll = /(\d+)\/\d+ enabled/.exec(session.snapshot().visible); - assert.ok(enabledAll && Number(enabledAll[1]) > baseline, "ctrl+a grows the selection past the baseline"); - session.press(KeyCtrlX); - await session.waitForVisible("text-only agent", { timeoutMs: WAIT_MS }); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - assert.match(session.snapshot().visible, new RegExp(`0/${selectable} enabled`)); - - // ctrl+r restores the model defaults, and escape discards all of it. - session.press(KeyCtrlR); - await session.waitForVisible(`${baseline}/${selectable} enabled`, { timeoutMs: WAIT_MS }); - session.press(KeyEscape); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - assert.doesNotMatch(session.snapshot().visible, /Tool Configuration/); - - await exitFixture(session); - }); - - test("resets a customized tool selection when the model changes", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady, KeyCtrlS } = await loadPtywrightHelpers(); - const session = spawnFixture("tools-reset-on-model.json", { rows: 50 }); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - session.line("/tools"); - await session.waitForVisible("Tool Configuration", { timeoutMs: WAIT_MS }); - session.send(" "); - await session.waitForVisible("✗ disabled", { timeoutMs: WAIT_MS }); - session.press(KeyCtrlS); - await session.waitForVisible("tools → ", { timeoutMs: WAIT_MS }); - - await submitCommand(session, "/model openai:gpt-5.6-sol"); - await session.waitForVisible("tool selection reset", { timeoutMs: WAIT_MS }); - - // The new model's full default catalog is live again. - session.line("/tools"); - await session.waitForVisible("Tool Configuration", { timeoutMs: WAIT_MS }); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - assert.doesNotMatch(session.snapshot().visible, /✗ disabled/); - - await exitFixture(session); - }); - - test("renders assistant errors as error notices", async (ctx) => { - const { spawnFixture, exitFixture, waitForFixtureReady } = await loadPtywrightHelpers(); - const session = spawnFixture("error.json"); - ctx.onTestFinished(() => session.close()); - - await waitForFixtureReady(session); - session.line("please fail"); - await session.waitForVisible("fixture provider failed", { timeoutMs: WAIT_MS }); - - const snapshot = session.snapshot(); - assert.match(snapshot.visible, /error fixture provider failed/); - - await exitFixture(session); - }); -}); - -/** - * Submit a slash command whose argument triggers editor autocomplete. The - * dropdown swallows the first Enter (it accepts the completion), so dismiss it - * with Escape before submitting. - */ -async function submitCommand( - session: { send: (text: string) => void; press: (key: string) => void; waitForStable: (ms: number, o: { timeoutMs: number }) => Promise }, - text: string, -): Promise { - session.send(text); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - session.press("\x1b"); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); - session.press("\r"); - await session.waitForStable(STABLE_MS, { timeoutMs: WAIT_MS }); -} - -/** - * Lazy-load ptywright so missing native bindings only fail this suite. The - * suite is gated behind `describe.skip` when the binding is missing, but the - * dynamic import also keeps the import graph clean for the rest of vitest. - */ -async function loadPtywrightHelpers() { - const ptywright = await import("@onkernel/ptywright"); - const { KeyArrowDown, KeyCtrlC, KeyEnter, KeyEscape, spawnSession } = ptywright; - // ptywright has no constants for the picker's bulk-action bindings; send the - // raw control bytes (ctrl+ is 0x01 + letter offset). - const KeyCtrlA = "\x01"; - const KeyCtrlR = "\x12"; - const KeyCtrlS = "\x13"; - const KeyCtrlX = "\x18"; - - // Picker scenarios need more rows than the base 40 so the whole selector - // frame stays inside the viewport alongside the header and message list. - const spawnFixture = (fixtureFile: string, options: { rows?: number } = {}) => - spawnSession({ - command: process.execPath, - args: [tsxCliPath, tuiRunnerPath, resolve(fixtureDir, fixtureFile)], - cwd, - cols: 160, - rows: options.rows ?? 40, - env: { - ...process.env, - KERNEL_API_KEY: "fixture-key", - OPENAI_API_KEY: "fixture-key", - }, - }); - - type FixtureSession = ReturnType; - - async function waitForFixtureReady(session: FixtureSession): Promise { - await session.waitForVisible("openai/gpt-5.5", { timeoutMs: WAIT_MS }); - } - - async function exitFixture(session: FixtureSession): Promise { - try { - await session.waitForStable(100, { timeoutMs: 2_000 }); - } catch { - // fall through to abort-then-exit path - } - - session.press(KeyCtrlC); - try { - await session.waitForExit({ timeoutMs: 1_500 }); - return; - } catch { - // continue to the second-ctrl-c path - } - try { - await session.waitForVisible("aborted", { timeoutMs: 2_000 }); - } catch { - // first ctrl+c may have landed during final run settlement - } - await session.waitForStable(100, { timeoutMs: 5_000 }); - session.press(KeyCtrlC); - await session.waitForExit({ timeoutMs: 5_000 }); - } - - return { - spawnFixture, - exitFixture, - waitForFixtureReady, - KeyArrowDown, - KeyCtrlA, - KeyCtrlC, - KeyCtrlR, - KeyCtrlS, - KeyCtrlX, - KeyEnter, - KeyEscape, - }; -} diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json deleted file mode 100644 index d57acd1b..00000000 --- a/packages/cli/tsconfig.build.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist-tsc", - "rootDir": "./src", - "emitDeclarationOnly": true, - "sourceMap": false, - "declarationMap": false - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.d.ts"], - "references": [ - { "path": "../ptywright" } - ] -} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json deleted file mode 100644 index d8faaf50..00000000 --- a/packages/cli/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./tsconfig.build.json" -} diff --git a/packages/cli/tsdown.config.ts b/packages/cli/tsdown.config.ts deleted file mode 100644 index cc69ae19..00000000 --- a/packages/cli/tsdown.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createRequire } from "node:module"; -import { defineConfig } from "tsdown"; - -const require = createRequire(import.meta.url); -const { version } = require("./package.json") as { version: string }; - -export default defineConfig({ - entry: ["src/cli.ts"], - format: ["esm"], - platform: "node", - dts: false, - sourcemap: false, - clean: true, - outExtensions: () => ({ js: ".js" }), - define: { - __CUA_VERSION__: JSON.stringify(version), - }, -}); diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts deleted file mode 100644 index a622b2f8..00000000 --- a/packages/cli/vitest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig } from "vitest/config"; - -// `server.host` pins vitest's internal dev server to a literal IP. Without -// this, `localhost` is resolved by Node's DNS — in sandboxed CI environments -// that don't have `localhost` in /etc/hosts the bootstrap fails with -// `ENOTFOUND localhost`. The setting is a no-op when running tests directly -// but keeps the dev-server bootstrap from doing a DNS lookup. -export default defineConfig({ - server: { - host: "127.0.0.1", - }, - test: { - include: ["test/**/*.test.ts"], - environment: "node", - hookTimeout: 30_000, - testTimeout: 30_000, - }, -}); diff --git a/packages/pi-extension/CHANGELOG.md b/packages/pi-extension/CHANGELOG.md index 729f6d24..b1316f64 100644 --- a/packages/pi-extension/CHANGELOG.md +++ b/packages/pi-extension/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- `@onkernel/cua-cli` and the `cua` binary are removed. Everything the CLI built + because it needed an agent front-end — sessions and resume, skills, the TUI, + print and RPC modes, model selection — pi supplies, so the extension replaces + it rather than reimplementing it. The `cua act` model-free executor path and + the `--print -o jsonl` telemetry schema are gone with it. - Add `@onkernel/cua-pi-extension`, an installable pi extension that contributes Kernel browser tools to pi's own agent session. Selectors cover the CDP browser toolset, the canonical computer toolset, the batch and Playwright tools, and diff --git a/packages/ptywright/scripts/build-ghostty.mjs b/packages/ptywright/scripts/build-ghostty.mjs index 041a41e2..5cd3570d 100644 --- a/packages/ptywright/scripts/build-ghostty.mjs +++ b/packages/ptywright/scripts/build-ghostty.mjs @@ -25,7 +25,7 @@ mkdirSync(config.zigGlobalCacheDir, { recursive: true }); // Pin the version explicitly: ghostty otherwise derives it from git, and since // the extracted source has no .git, discovery walks up into the host repo and -// panics when HEAD is on a non-vX.Y.Z tag (e.g. a cua-cli/vX.Y.Z release tag). +// panics when HEAD is on a non-vX.Y.Z tag (e.g. a package release tag). run([ zig, "build", diff --git a/skills/cua-cli/SKILL.md b/skills/cua-cli/SKILL.md deleted file mode 100644 index 26d720ff..00000000 --- a/skills/cua-cli/SKILL.md +++ /dev/null @@ -1,314 +0,0 @@ ---- -name: cua-cli -description: Drive a Kernel cloud browser from the shell using the `cua` CLI. Use this skill when you need to open URLs, click elements, type into fields, inspect pages, fill forms, take screenshots, or chain multi-step browser tasks across shell calls. Supports named sessions for stateful workflows. ---- - -# cua-cli - -`cua` is a single-binary CLI that drives a real Chrome session running in Kernel. It's designed for agentic use: each subcommand returns a stable result on stdout and a deterministic exit code documented below, so you can chain calls together and parse the output. - -## One-shot subcommands - -Each call below provisions a fresh Kernel browser by default, runs the action, and tears the browser down. Use `-s ` (see "Named sessions" below) to keep state across calls. - -### Model-free subcommands - -These run directly against the browser (CDP or OS input) — no LLM involved, no model API key needed, only `KERNEL_API_KEY`. - -| Subcommand | What it does | Stdout | Exit code | -| --- | --- | --- | --- | -| `cua open ` | Navigate via CDP; `back`/`forward` walk history. | `ok` | 0 ok, 2 error | -| `cua url` | Print the active tab's URL. | the URL | 0 ok, 2 error | -| `cua snapshot [--filter interactive]` | Print the page's accessibility tree with element refs like `[e12]`. `--filter interactive` keeps only interactive elements. | the tree (multi-line) | 0 ok, 2 error | -| `cua act ''` | Execute one direct `browser_act` plan. JSON is the tool input without the `type` discriminator; ref steps use refs from `snapshot`/`find`. | bounded `browser_act` outcome, expectation evidence, and successor diff | 0 worked, 1 didnt/unknown, 2 invalid/error | -| `cua find ""` | Lexically score elements against the query, best first. | one match per line: `role "name" [eN]` (the quoted name is omitted when the element has none; role falls back to `node`) | 0 ok, 1 not_found, 2 error | -| `cua text` | Print the page's visible text (`innerText`). | the text (multi-line) | 0 ok, 2 error | -| `cua fill ""` | Set a form field's value. With a ref (`e12` from `snapshot`/`find`) it targets that exact element. With a query it finds the unique best-matching form field (textbox, searchbox, combobox, checkbox, radio, listbox, spinbutton); exit 1 with the tied matches listed if the query is ambiguous — tighten it and retry. For checkbox/radio pass `true\|false\|checked\|unchecked\|on\|off` (query form also accepts `1\|0`). `fill` leaves the field focused, so a following `cua press Return` submits the form. | `ok filled ""` (query) or `ok filled e12` (ref) | 0 ok, 1 not_found, 2 error | -| `cua press [...]` | Send one key chord (e.g. `cua press ctrl l`, `cua press Return`). | `ok pressed` | 0 ok, 2 error | -| `cua click ` | OS-level click at viewport coordinates. Exactly two integer arguments. | `ok clicked (x, y)` | 0 ok, 2 error | -| `cua click ` | CDP click on an element ref from `snapshot`/`find`, e.g. `cua click e12`. Any other single `click` argument routes to the model-mediated `click` below. | `ok clicked e12` | 0 ok, 1 not_found (stale ref — re-snapshot), 2 error | -| `cua tabs` | List open tabs. | one line per tab: `tab_id XXXX: "title" (url)` | 0 ok, 2 error | -| `cua screenshot [--out ]` | Save a PNG (default `screenshot.png`). `--out -` writes the bytes to stdout. | the saved path; with `--out -`, stdout is exactly the PNG bytes (safe to pipe) | 0 ok, 2 error | - -**Element refs span invocations within a named session.** Refs printed by `snapshot`/`find` (`[e12]`) are persisted per `-s` session, so `cua -s x snapshot` then `cua -s x click e12` works. Refs self-heal across in-page DOM changes when the element is still unambiguous, but any navigation — including reloading the same URL — invalidates them; the command then exits 1 with a stale-ref message — re-run `snapshot` and use a fresh ref. Without `-s` there is no shared browser, so refs from a previous invocation are meaningless. - -### Verified `browser_act` plans (model-free) - -> **Use `cua act` when the result matters, not merely the input dispatch.** It -> executes dependent ref-based steps and checks semantic postconditions against -> structured browser observations, without an LLM. - -The one shell argument is the `browser_act` input as JSON, **without** the -outer `"type": "browser_act"` discriminator. Each individual step still needs -its own `type`. The complete top-level input is: - -```ts -type BrowserActInput = { - steps: Step[]; // required; 1–20 entries - expect?: Expectation; // final plan postcondition - timeout_ms?: number; // whole plan; 1–30000, default 30000 - poll_ms?: number; // expectation polling; 10–1000, default 50 - successor?: { - filter?: "all" | "interactive"; - depth?: number; - }; - tab_id?: string; // defaults to the active tab -}; -``` - -Supported step objects: - -| `type` | Required fields | Optional action fields | -| --- | --- | --- | -| `click` | `ref` | `button: "left"\|"right"\|"middle"`, `num_clicks: 1..3`, `modifiers: string[]` | -| `hover` | `ref` | — | -| `fill` | `ref`, `value: string\|number\|boolean` | — | -| `type` | `text` | — | -| `key` | `text` | `repeat: number` | -| `scroll_to` | `ref` | — | -| `wait` | — | `ms: 0..30000` | - -Every step also accepts `expect?: Expectation` and `timeout_ms?: 1..30000`. -A step timeout covers both its input execution and postcondition verification -and is capped by the plan deadline. If a step cannot establish its expectation, -later steps are skipped. Navigation is a control-flow boundary, so put a -navigation-producing action last and obtain fresh refs afterward. - -An expectation is one leaf below or a non-empty `{"all": [leaf, ...]}` / -`{"any": [leaf, ...]}` group. Groups contain leaves, not nested groups. - -| Leaf | JSON shape and matching behavior | -| --- | --- | -| Accessible text | `{"type":"text","text":"Done","exists":true}` — case-insensitive, whitespace-normalized substring; `exists` defaults to `true` | -| Role/name | `{"type":"role_name","role":"button","name":"Submit","exists":false}` — `role` or `name` is required; matching is exact and the name is case-sensitive | -| Ref state | `{"type":"ref","ref":"e7","value":"ready"}` — provide at least one of `value`, `checked` (`boolean` or `"mixed"`), `selected`, or `expanded` | -| URL/title | `{"type":"url","changed":true}` — `type` is `url` or `title`; provide at least one of `equals`, case-sensitive `contains`, or `changed` | - -`changed` compares against the observation captured before the step (or before -the whole plan for top-level `expect`). Evidence counts as causal only when the -condition was not matched before input and is matched afterward. A condition -that was already true is reported as `preexisting`, not proof that the action -worked. - -A robust verified submit/navigation pattern is: - -```bash -cua -s checkout snapshot --filter interactive -cua -s checkout act '{ - "steps": [{ - "type": "click", - "ref": "e42", - "expect": { - "any": [ - {"type": "url", "changed": true}, - {"type": "role_name", "role": "button", "name": "Submit", "exists": false} - ] - }, - "timeout_ms": 30000 - }], - "expect": { - "any": [ - {"type": "url", "changed": true}, - {"type": "role_name", "role": "button", "name": "Submit", "exists": false} - ] - }, - "timeout_ms": 30000, - "poll_ms": 100, - "successor": {"filter": "all", "depth": 12} -}' -``` - -The step expectation gates later steps; the top-level expectation determines -the final plan result. `successor` controls the bounded accessibility-tree -feedback and diff but is feedback, not proof—the expectations provide proof. -Stdout begins with `browser_act: worked|didnt|unknown`; exit code `0` means -`worked`, `1` means `didnt` or `unknown`, and `2` means invalid JSON/input or an -execution error. - -For irreversible actions, require a postcondition that demonstrates the -transition. If the result is `unknown` or times out, inspect with `snapshot`, -`text`, or `url` before retrying; the input may have settled even when its -verification did not. - -### Model-mediated subcommands - -These resolve a natural-language description with an LLM, so they need the model provider's API key (e.g. `OPENAI_API_KEY` for the default model). - -| Subcommand | What it does | Stdout | Exit code | -| --- | --- | --- | --- | -| `cua click ""` | Find the element matching the visible, natural-language description and click it. | `ok clicked (x, y)` or `not_found ` | 0 ok, 1 not_found, 2 error | -| `cua type "" ""` | Focus the field matching the visible, natural-language description and type text. | `ok typed` or `not_found ` | 0 ok, 1 not_found, 2 error | -| `cua observe ["question"]` | Describe the page; optionally answer a question. | the description | 0 ok, 2 error | -| `cua do ""` | Open-ended; let the agent plan and act. Bound by `--max-steps` (default 3). | the assistant's final text | 0 ok, 2 error | - -Useful flags: - -- `-m ` — pick the LLM for model-mediated subcommands (default `gpt-5.6-sol`). - Recommended refs are `openai:gpt-5.6-sol`, `anthropic:claude-opus-5`, - `google:gemini-3.6-flash`, `xai:grok-4.5`, - and `moonshotai:kimi-k3`. -- `cua models` — list supported `-m` values and their providers; filter with - `cua models -p openai|anthropic|google|meta|xai|moonshotai|openrouter`. - `gemini` aliases `google`, and `moonshot` aliases `moonshotai`. Model refs - print as `provider:model`; `-m` accepts either the full ref or a bare model id - that matches exactly one entry. -- `--max-steps ` — bound the agent loop on `cua do` (default 3). -- `--filter interactive` — restrict `cua snapshot` to interactive elements. -- `--proxy ` — route the browser through a Kernel proxy. - The proxy must already exist (create one via the Kernel API/CLI first); - unlike `--profile`, an unknown name is an error, never auto-created. For - named sessions pass it to `session start`; later `-s` calls attach to the - same browser and inherit it. -- `--profile ` — load a Kernel browser profile for cookies / - storage. Existing ids or names are reused; a non-id name is created if it - does not exist. Use this whenever logged-in state or other persisted browser - state matters across fresh browser sessions. Changes save back by default; - pass `--profile-no-save-changes` for a read-only run. -- `-v` — verbose progress on stderr (provisioning, tool calls, transcript path). - -### Model tool policy - -The CLI selects its interaction tools from the model: structured CUA browser -primitives plus `browser_act` verified plans for OpenAI, Meta, xAI, and older -Anthropic models; browser primitives alone for Moonshot, whose API rejects -`browser_act`'s schema; and native browser tools for current Anthropic and -Google models. It also appends workspace coding tools in `--print`, TUI, and -model-mediated action runs. - -There is no `--mode`, `--native-tool`, or `--playwright` flag. Those catalogs -remain explicit SDK choices rather than CLI defaults. The CLI also does not -attach screenshots automatically to the first prompt or after writes. Ask the -model to capture a screenshot when the task specifically requires visual -feedback. Use direct `cua act ''` when the caller already has refs and -needs dependent actions with semantic verification; use `cua do` or free-form -mode when a model should construct the plan. - -## Named sessions for multi-call workflows - -Without `-s`, each subcommand provisions a brand-new browser. To keep -state (cookies, scroll position, current URL) across calls, allocate a -named session first: - -```bash -cua --profile github session start login # creates a Kernel browser, prints `name=login` -cua -s login open https://github.com/login -cua -s login fill "email field" "$EMAIL" # model-free -cua -s login fill "password field" "$PASSWORD" # model-free -cua -s login click "Sign in" # model-mediated -cua -s login url # prints the post-login URL -cua session stop login # tears down the Kernel browser -``` - -Inspecting a page mid-flow, entirely model-free: - -```bash -cua -s login snapshot --filter interactive # what can I interact with? -cua -s login find "sign in button" # score elements against a query -cua -s login click e12 # click a ref from the snapshot/find output -cua -s login fill e7 "$EMAIL" # fill a ref directly -cua -s login text # read the page's visible text -cua -s login tabs # list open tabs -``` - -Inspect sessions: - -```bash -cua session list # tab-formatted: NAME, KERNEL_ID, AGE, LIVE_URL -cua session show login # full JSON metadata -``` - -`cua session show ` and `cua session stop ` exit 1 when the named -session does not exist (`no named session ""`); other session failures -exit 2. - -Pass `--profile` when starting the named session; later `cua -s login ...` -calls attach to that same browser, so they do not need the profile flag. - -Liveness: Kernel browsers can time out from inactivity even between your calls. If `cua -s ...` fails with `error: named session "" is no longer alive on Kernel ...` (printed to stderr, exit 2), run `cua session stop && cua --profile github session start ` to provision a fresh one with the same persisted profile. - -## Session transcripts - -Every `cua --print`, interactive TUI, and model-mediated `cua -s ` -invocation appends to a JSONL transcript. Model-free subcommands do not touch -transcripts — there is no model conversation to record. Treat the on-disk -directory name as internal; find the exact path instead of trying to -reconstruct it: - -```bash -cua -v --print "..." # stderr includes: [cua] session= -cua session show login | jq -r .transcript_path -``` - -The default root is `$XDG_DATA_HOME/cua/sessions` or -`~/.local/share/cua/sessions`. For named sessions, `transcript_path` appears -after the first model-mediated `-s` call (`click ""`, `type`, `observe`, -`do`, `--print`, or a TUI attach). - -Each line is a pi `SessionManager` record with a top-level `type` of `session`, `message`, or `custom`. Conversation entries have `type: "message"` with the role nested at `.message.role` (`user`, `assistant`, or `toolResult`). There's also a `type: "custom"` entry with `customType: "cua-browser"` written once per session whose `data` carries `sessionId` / `liveUrl` (and `profileId` when a profile is loaded). - -Use `cua --print -o jsonl "..."` only when you need live stdout events while a -run is happening. That stream is a compact event feed (`tool_call`, -`tool_result`, `assistant_text_done`, etc.), not the persisted pi -`SessionManager` transcript schema above. - -## Free-form mode - -Two ways to give the agent free rein: - -```bash -cua --print "open hn and tell me the top story" # one-shot, streams text to stdout -cua --print -o jsonl "..." # one-shot, streams JSONL events -cua "..." # interactive TUI (requires a real terminal) -``` - -`--print` exits when the agent finishes; the interactive TUI keeps -running until you Ctrl+C. - -### Interactive slash commands - -Only available in the TUI (`cua` with no `--print`); `/` opens autocomplete. - -| Command | Behavior | -| --- | --- | -| `/model` | Open a searchable model picker | -| `/model ` | Switch directly, no UI. An unknown ref errors, then opens the picker prefilled | -| `/tools` | Open a menu to enable/disable this session's model-callable tools | -| `/thinking ` | Set reasoning level | -| `/compact` | Summarize older turns | -| `/skill: [args]` | Invoke a loaded skill | - -Both pickers are keyboard-only and refuse to open while a turn is running. - -**`/model` picker** — type to fuzzy-search provider/ref/model/name, `↑`/`↓` to -move (wraps), `enter` to select, `esc` or `ctrl+c` to cancel. Active model is -first and marked `✓`. Selecting runs the same tool revalidation as -`/model `. - -**`/tools` picker** — lists exactly the tools the CLI composed for the active -model (interaction tools + coding tools) so you can disable a subset for -testing. It can only remove from that list, never add unsupported tools. - -| Key | Action | -| --- | --- | -| `↑` / `↓` | Move cursor | -| `enter` | Toggle highlighted tool | -| `space` | Toggle highlighted tool (only while the search box is empty) | -| `ctrl+a` / `ctrl+x` | Enable / disable everything listed (respects search) | -| `ctrl+r` | Reset to model defaults | -| `ctrl+s` | Apply | -| `esc` | Cancel (discards staged edits) | -| `ctrl+c` | Clear an active search, else cancel | - -Edits are staged — nothing applies until `ctrl+s`, and cancel leaves live state -untouched. A selection rejected by catalog validation reports the error and -changes nothing. Selections are session-only and are reset to the new model's -defaults by `/model`. Disabling everything is allowed and yields a text-only -agent. - -## Don't forget - -- Prefer the model-free subcommands when they can do the job — they're faster, cheaper, and deterministic. Reach for `click ""` / `type` / `do` only when you need semantic matching or planning. -- Subcommands that take an element or field description (`click ""`, `type`) match SEMANTICALLY, not by selector. Use natural-language descriptions of what the user would see on screen. `fill` matches lexically against accessible role/name — use the words from `snapshot`/`find` output. -- Browser viewport defaults to 1920x1080. -- Keyboard navigation (`Page_Down`, `Home`, arrow keys via `cua press`) is more reliable than mouse-wheel scrolling. -- For multi-step state, you almost always want `-s `. Without it, the second subcommand can't see anything the first one did. diff --git a/tsconfig.json b/tsconfig.json index 6b096fee..311212bb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,6 @@ { "path": "./packages/ai" }, { "path": "./packages/agent" }, { "path": "./packages/ptywright" }, - { "path": "./packages/cli" }, { "path": "./packages/pi-extension" } ] } From 5a80882275bd9edad0405ef4d0e928192e38975d Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:02:29 +0000 Subject: [PATCH 2/7] Restore workspace typecheck and ptywright CI, clean the lockfile The deleted cli-unit job was the only one running tsc -b over the whole project graph, and also the only one building and testing ptywright -- so deleting it dropped both. A typecheck-and-ptywright job restores them, Zig setup included. Regenerating the lockfile drops the extraneous packages/cli entry and 2,400 lines of CLI-only dependencies. That exposed a second problem: pi was reaching node_modules only through npm's automatic peer install once the CLI stopped depending on it, while the extension's end-to-end test spawns the pi binary. The extension now declares the pi packages it tests against, and drops the pi-tui peer it never imports. --- .github/workflows/ci.yml | 46 + package-lock.json | 2974 ++++------------------------ packages/pi-extension/package.json | 6 +- 3 files changed, 387 insertions(+), 2639 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00c8aa2a..19c82e9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,52 @@ jobs: - name: Pi extension unit tests run: npm test --workspace @onkernel/cua-pi-extension + typecheck-and-ptywright: + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + PTYWRIGHT_ZIG_VERSION: "0.15.2" + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + - run: npm ci + - name: Cache Zig toolchain + uses: actions/cache@v4 + with: + path: .dev/tools/zig-x86_64-linux-${{ env.PTYWRIGHT_ZIG_VERSION }} + key: zig-${{ runner.os }}-${{ env.PTYWRIGHT_ZIG_VERSION }} + - name: Install Zig ${{ env.PTYWRIGHT_ZIG_VERSION }} + run: | + set -euo pipefail + ZIG_DIR=".dev/tools/zig-x86_64-linux-${PTYWRIGHT_ZIG_VERSION}" + if [ ! -x "${ZIG_DIR}/zig" ]; then + mkdir -p .dev/tools + curl -fsSL "https://ziglang.org/download/${PTYWRIGHT_ZIG_VERSION}/zig-x86_64-linux-${PTYWRIGHT_ZIG_VERSION}.tar.xz" \ + | tar -xJ -C .dev/tools + fi + "${ZIG_DIR}/zig" version + echo "${GITHUB_WORKSPACE}/${ZIG_DIR}" >> "${GITHUB_PATH}" + - name: Cache ptywright native artifacts + uses: actions/cache@v4 + with: + path: | + packages/ptywright/.cache + packages/ptywright/native/build + key: ptywright-${{ runner.os }}-${{ hashFiles('packages/ptywright/GHOSTTY_UPSTREAM', 'packages/ptywright/native/**', 'packages/ptywright/scripts/**') }} + - run: npm run build --workspace @onkernel/cua-ai + - run: npm run build --workspace @onkernel/cua-agent + # The only job that typechecks the whole project graph rather than one + # package, and the only one that builds and tests ptywright. + - name: Typecheck workspace + run: npx tsc -b + - name: Build ptywright (native binding) + run: npm run build --workspace @onkernel/ptywright + - name: Ptywright tests + run: npm test --workspace @onkernel/ptywright + integration: runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/package-lock.json b/package-lock.json index 78a99e57..4a7f3b02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,6 @@ }, "node_modules/@anthropic-ai/sdk": { "version": "0.91.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", - "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", "license": "MIT", "dependencies": { "json-schema-to-ts": "^3.1.1" @@ -44,8 +42,6 @@ }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", @@ -59,8 +55,6 @@ }, "node_modules/@aws-crypto/sha256-js": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", @@ -73,8 +67,6 @@ }, "node_modules/@aws-crypto/supports-web-crypto": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -82,8 +74,6 @@ }, "node_modules/@aws-crypto/util": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.222.0", @@ -93,8 +83,6 @@ }, "node_modules/@aws-sdk/client-bedrock-runtime": { "version": "3.1048.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", - "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -118,8 +106,6 @@ }, "node_modules/@aws-sdk/core": { "version": "3.977.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.5.tgz", - "integrity": "sha512-O5otOc1c6UZh5HsHAaPdYBcUUR9HL6mtnKqvc8nxN/CKDGUBUpsdh0q8K04Uz/dd1i0TaGyIQuQNqoO7+ad2TQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.974.2", @@ -137,8 +123,6 @@ }, "node_modules/@aws-sdk/credential-provider-env": { "version": "3.972.66", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.66.tgz", - "integrity": "sha512-bOzP2+zdJ0XrghywB4FaJXtGZCx9yS0AGps+VJ5yEgg30wVyHNmVDBwVDXcRypzQY5iLGCS3NSn0nsuISqjFCQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.977.5", @@ -153,8 +137,6 @@ }, "node_modules/@aws-sdk/credential-provider-http": { "version": "3.972.68", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.68.tgz", - "integrity": "sha512-lkunS8X+H6V76WE+t/uGQm/U8v0JXK5mLfNFTUAMlE1kqaCjwlmqKJrgCVtqjK/vqnlrSWsLK4Lr4NANBWlfTQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.977.5", @@ -171,8 +153,6 @@ }, "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { "version": "4.9.13", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.13.tgz", - "integrity": "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.31.1", @@ -185,8 +165,6 @@ }, "node_modules/@aws-sdk/credential-provider-ini": { "version": "3.973.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.11.tgz", - "integrity": "sha512-KoDEolYtLHG/8C+IiZpXbJWyBOMkrHV+j66Kb9PBXmLv5euGb7aELvuCmLenoGAV6gBW2wM7TsG/1e5iulH4kA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.977.5", @@ -209,8 +187,6 @@ }, "node_modules/@aws-sdk/credential-provider-login": { "version": "3.972.73", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.73.tgz", - "integrity": "sha512-tjsxMkTAFkmiV9ycmymapb9nLECWVOwFs0bZMQ9gB9bnbY8/HwfukHZlWbXZZp7qkPU6EXAfOcMm3DioFFEywA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.977.5", @@ -226,8 +202,6 @@ }, "node_modules/@aws-sdk/credential-provider-node": { "version": "3.972.77", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.77.tgz", - "integrity": "sha512-l4nitYCN/Ls57vtUfdextCjTjW41JD7lQiAnuR0RTbdByFc/6OmEAzwGd+lrp6CUtiXGQL1FCaYiamfHASrwBw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.66", @@ -248,8 +222,6 @@ }, "node_modules/@aws-sdk/credential-provider-process": { "version": "3.972.66", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.66.tgz", - "integrity": "sha512-YOnX6bIhdjx0QfaENu2PB0eFm5MEc9ft8XNGQ+NxMfeLSq9aE+XjWCwDupEnV4UWv5ZFpBLJbTREIx7KNOoqpQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.977.5", @@ -264,8 +236,6 @@ }, "node_modules/@aws-sdk/credential-provider-sso": { "version": "3.973.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.10.tgz", - "integrity": "sha512-IsXnQ35j5VE+3ZK6aIhT5ypB+Jim3zRwVz0nYuVwyBKZyu/SYx+O2/LQpng8c2EiuwyqceabsDlYrICHDlJPsA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.977.5", @@ -282,8 +252,6 @@ }, "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { "version": "3.1102.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1102.0.tgz", - "integrity": "sha512-Ua700vVvM1q105yABSUQWkCK6FeTrNfU6ORGetJe5BzkZWY7QhkF7SVTOlmDGWRDNd6jbyY0Dv5e+E4bMBEmLg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.977.5", @@ -299,8 +267,6 @@ }, "node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.972.72", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.72.tgz", - "integrity": "sha512-nj9Zlsy7ya+fy+jhWTJwgfr7YdtDM4xHyZvgKuftuny0UgROVx9lxwvsWJSLvpKk4lig0m0tHng3k1fEnt0LeA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.977.5", @@ -316,8 +282,6 @@ }, "node_modules/@aws-sdk/eventstream-handler-node": { "version": "3.972.31", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.31.tgz", - "integrity": "sha512-/BRzvkp46mF6eXBL/l9WKPQQfifLlUPaWli6n9/T/WDLUg8he7TCyuNFnk6RvHP5j5W/kMj5Gxw7W778LJaXDA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.974.2", @@ -331,8 +295,6 @@ }, "node_modules/@aws-sdk/middleware-eventstream": { "version": "3.972.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.26.tgz", - "integrity": "sha512-2eIvouTZoxPu5ClHY6ij13De1yhY8Rmllt0dlGeBNXX3wmR7fU1pvMCGb50fKm1GxcuntWK0t1T0cMjTyDoUQA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.974.2", @@ -346,8 +308,6 @@ }, "node_modules/@aws-sdk/middleware-websocket": { "version": "3.972.48", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.48.tgz", - "integrity": "sha512-1BYTN+c0J/n5HfoDl3IR2Cjhfp2coByofX+gOh8HhyXda1HP7oGAUI/uyKSn4Xc+rrvVcsSROTVF1ZRBs07ARQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.977.5", @@ -364,8 +324,6 @@ }, "node_modules/@aws-sdk/nested-clients": { "version": "3.997.40", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.40.tgz", - "integrity": "sha512-hEdHT0PBR4fkGxWhwKG5EtEYKnAM7HKkp0vD10ufk4YcXejH4r4q6G/XhPzjUc6Yxo5kBS2vHg7llj4ViR9VTQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.977.5", @@ -383,8 +341,6 @@ }, "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { "version": "4.9.13", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.13.tgz", - "integrity": "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.31.1", @@ -397,8 +353,6 @@ }, "node_modules/@aws-sdk/signature-v4-multi-region": { "version": "3.996.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.43.tgz", - "integrity": "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.974.2", @@ -412,8 +366,6 @@ }, "node_modules/@aws-sdk/token-providers": { "version": "3.1048.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", - "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.974.11", @@ -429,8 +381,6 @@ }, "node_modules/@aws-sdk/types": { "version": "3.974.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.2.tgz", - "integrity": "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.16.1", @@ -442,8 +392,6 @@ }, "node_modules/@aws-sdk/util-locate-window": { "version": "3.965.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.8.tgz", - "integrity": "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -454,8 +402,6 @@ }, "node_modules/@aws-sdk/xml-builder": { "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.37.tgz", - "integrity": "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.16.1", @@ -467,8 +413,6 @@ }, "node_modules/@aws/lambda-invoke-store": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", - "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -476,8 +420,6 @@ }, "node_modules/@babel/generator": { "version": "8.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.6.tgz", - "integrity": "sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -494,8 +436,6 @@ }, "node_modules/@babel/helper-string-parser": { "version": "8.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.6.tgz", - "integrity": "sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==", "dev": true, "license": "MIT", "engines": { @@ -504,8 +444,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "8.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.6.tgz", - "integrity": "sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==", "dev": true, "license": "MIT", "engines": { @@ -514,8 +452,6 @@ }, "node_modules/@babel/parser": { "version": "8.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.6.tgz", - "integrity": "sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog==", "dev": true, "license": "MIT", "dependencies": { @@ -530,8 +466,6 @@ }, "node_modules/@babel/runtime": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -539,8 +473,6 @@ }, "node_modules/@babel/types": { "version": "8.0.0-rc.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.6.tgz", - "integrity": "sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==", "dev": true, "license": "MIT", "dependencies": { @@ -553,8 +485,6 @@ }, "node_modules/@earendil-works/pi-agent-core": { "version": "0.83.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.83.0.tgz", - "integrity": "sha512-RorGp9OH5l3ElpuC5a5ZQ2eWcchZGXflXRzVGkV99y3y6tT+LLNyxoYIdVKvTKWEObwhExeQbTH0fI2tE4iX4g==", "license": "MIT", "dependencies": { "@earendil-works/pi-ai": "^0.83.0", @@ -569,8 +499,6 @@ }, "node_modules/@earendil-works/pi-ai": { "version": "0.83.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.83.0.tgz", - "integrity": "sha512-m3IZD4g3er0V8TC9+Vpgw/sjTKqcJlkcIBy/JvsgRubuuik3tAVzyugUg4rVrShIkkOT69mEd34NEqKUIsl6JQ==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -594,11 +522,8 @@ }, "node_modules/@earendil-works/pi-coding-agent": { "version": "0.83.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.83.0.tgz", - "integrity": "sha512-uYhF+FsZxogoSX/AxBcUdiY+ZklubwaXyAoEGA2eQwsHcyEAhUYIKh/WLXe/a8+k8eTCmxb+ZN2Zo9mzQtzbWw==", - "hasShrinkwrap": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@earendil-works/pi-agent-core": "^0.83.0", "@earendil-works/pi-ai": "^0.83.0", @@ -631,10 +556,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk": { "version": "0.91.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", - "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "json-schema-to-ts": "^3.1.1" }, @@ -652,10 +575,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/crc32": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", - "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", @@ -667,10 +588,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", @@ -683,10 +602,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-js": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", @@ -698,20 +615,16 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/supports-web-crypto": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/util": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", @@ -720,10 +633,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/client-bedrock-runtime": { "version": "3.1048.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", - "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -746,10 +657,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/core": { "version": "3.974.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", - "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.24", @@ -766,10 +675,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-env": { "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", - "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", @@ -783,10 +690,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-http": { "version": "3.972.39", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", - "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", @@ -802,10 +707,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-ini": { "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", - "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-env": "^3.972.37", @@ -827,10 +730,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-login": { "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", - "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", @@ -845,10 +746,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-node": { "version": "3.972.42", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", - "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.37", "@aws-sdk/credential-provider-http": "^3.972.39", @@ -868,10 +767,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-process": { "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", - "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", @@ -885,10 +782,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-sso": { "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", - "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", @@ -904,10 +799,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", - "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", @@ -922,10 +815,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/eventstream-handler-node": { "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", - "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", @@ -938,10 +829,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-eventstream": { "version": "3.972.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", - "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", @@ -954,10 +843,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-websocket": { "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", - "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/types": "^3.973.8", @@ -973,10 +860,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/nested-clients": { "version": "3.997.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", - "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -995,10 +880,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/signature-v4-multi-region": { "version": "3.996.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", - "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", @@ -1012,10 +895,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/token-providers": { "version": "3.1048.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", - "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", @@ -1030,10 +911,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/types": { "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", - "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" @@ -1044,10 +923,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/util-locate-window": { "version": "3.965.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", - "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -1057,10 +934,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/xml-builder": { "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", - "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@nodable/entities": "2.1.0", "@smithy/types": "^4.14.1", @@ -1073,29 +948,24 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws/lambda-invoke-store": { "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", - "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=18.0.0" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@babel/runtime": { "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { "version": "0.83.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.83.0.tgz", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@earendil-works/pi-ai": "^0.83.0", "diff": "8.0.4", @@ -1109,9 +979,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { "version": "0.83.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.83.0.tgz", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", @@ -1134,9 +1003,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { "version": "0.83.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.83.0.tgz", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "get-east-asian-width": "1.6.0", "marked": "18.0.5" @@ -1147,11 +1015,9 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@google/genai": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", - "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", @@ -1172,11 +1038,9 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard": { "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", - "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", + "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10" }, @@ -1193,179 +1057,40 @@ "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" } }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-arm64": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz", - "integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-universal": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz", - "integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-x64": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz", - "integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz", - "integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-musl": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz", - "integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz", - "integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-gnu": { "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz", - "integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-musl": { "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz", - "integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz", - "integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-x64-msvc": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz", - "integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, "engines": { "node": ">= 10" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@mistralai/mistralai": { "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", - "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", @@ -1383,116 +1108,88 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@nodable/entities": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/nodable" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/api": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/semantic-conventions": { "version": "1.41.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", - "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/aspromise": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause", - "peer": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/base64": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause", - "peer": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/codegen": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause", - "peer": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", - "license": "BSD-3-Clause", - "peer": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/fetch": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/float": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause", - "peer": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/path": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause", - "peer": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/pool": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause", - "peer": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/utf8": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", - "license": "BSD-3-Clause", - "peer": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@silvia-odwyer/photon-node": { "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", - "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", - "license": "Apache-2.0", - "peer": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/core": { "version": "3.24.3", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", - "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", @@ -1504,10 +1201,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/credential-provider-imds": { "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", - "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", @@ -1519,10 +1214,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/fetch-http-handler": { "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", - "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", @@ -1534,10 +1227,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/is-array-buffer": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -1547,10 +1238,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/node-http-handler": { "version": "4.7.3", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", - "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", @@ -1562,10 +1251,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/signature-v4": { "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", - "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", @@ -1577,10 +1264,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/types": { "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", - "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -1590,10 +1275,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-buffer-from": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" @@ -1604,10 +1287,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-utf8": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" @@ -1618,38 +1299,31 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@types/node": { "version": "22.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", - "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 14" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "18 || 20 || >=22" } }, "node_modules/@earendil-works/pi-coding-agent/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", @@ -1664,32 +1338,25 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/bignumber.js": { "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "*" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/bowser": { "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^4.0.2" }, @@ -1699,17 +1366,13 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/buffer-equal-constant-time": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause", - "peer": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/chalk": { "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -1719,10 +1382,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1734,20 +1395,16 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/data-uri-to-buffer": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 12" } }, "node_modules/@earendil-works/pi-coding-agent/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", - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -1762,35 +1419,28 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/diff": { "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.3.1" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/ecdsa-sig-formatter": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "safe-buffer": "^5.0.1" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/extend": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-builder": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "dev": true, "funding": [ { "type": "github", @@ -1798,7 +1448,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" @@ -1806,8 +1455,7 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-parser": { "version": "5.7.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", - "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "dev": true, "funding": [ { "type": "github", @@ -1815,7 +1463,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", @@ -1828,8 +1475,7 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/fetch-blob": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, "funding": [ { "type": "github", @@ -1841,7 +1487,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -1852,10 +1497,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/formdata-polyfill": { "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fetch-blob": "^3.1.2" }, @@ -1865,10 +1508,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/gaxios": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -1880,10 +1521,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/gcp-metadata": { "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", @@ -1895,10 +1534,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/get-east-asian-width": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1908,10 +1545,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/glob": { "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", @@ -1926,10 +1561,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/google-auth-library": { "version": "10.6.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", - "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", @@ -1944,37 +1577,29 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/google-logging-utils": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } }, "node_modules/@earendil-works/pi-coding-agent/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==", - "license": "ISC", - "peer": true + "dev": true, + "license": "ISC" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/highlight.js": { "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": "*" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/hosted-git-info": { "version": "9.0.3", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", - "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, "license": "ISC", - "peer": true, "dependencies": { "lru-cache": "^11.1.0" }, @@ -1984,10 +1609,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -1998,10 +1621,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -2012,47 +1633,37 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC", - "peer": true + "dev": true, + "license": "ISC" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/jiti": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/json-bigint": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bignumber.js": "^9.0.0" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/json-schema-to-ts": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" @@ -2063,10 +1674,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/jwa": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -2075,10 +1684,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/jws": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" @@ -2086,27 +1693,21 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/long": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0", - "peer": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/lru-cache": { "version": "11.4.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", - "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "engines": { "node": "20 || >=22" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/marked": { "version": "18.0.5", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", - "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -2116,10 +1717,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "brace-expansion": "^5.0.5" }, @@ -2132,26 +1731,20 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/minipass": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/@earendil-works/pi-coding-agent/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==", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/node-domexception": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", + "dev": true, "funding": [ { "type": "github", @@ -2163,17 +1756,14 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=10.5.0" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/node-fetch": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -2189,10 +1779,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/openai": { "version": "6.26.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", - "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "openai": "bin/cli" }, @@ -2211,10 +1799,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" @@ -2225,22 +1811,17 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry/node_modules/@types/retry": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/partial-json": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", - "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/path-expression-matcher": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "dev": true, "funding": [ { "type": "github", @@ -2248,27 +1829,22 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=14.0.0" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, "license": "BlueOak-1.0.0", - "peer": true, "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" @@ -2282,10 +1858,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", @@ -2294,21 +1868,17 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile/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", - "peer": true, "engines": { "node": ">= 4" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { "version": "7.6.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", - "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -2328,18 +1898,15 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/retry": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4" } }, "node_modules/@earendil-works/pi-coding-agent/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", @@ -2354,15 +1921,12 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/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", - "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -2372,10 +1936,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2385,88 +1947,68 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, "node_modules/@earendil-works/pi-coding-agent/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==", - "license": "ISC", - "peer": true + "dev": true, + "license": "ISC" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/strnum": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", - "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/ts-algebra": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "dev": true, + "license": "0BSD" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": { "version": "1.3.7", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", - "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { "version": "8.5.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", - "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/undici-types": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT", - "peer": true + "dev": true, + "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent/node_modules/web-streams-polyfill": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 8" } }, "node_modules/@earendil-works/pi-coding-agent/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", - "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -2479,10 +2021,8 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/ws": { "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -2501,8 +2041,7 @@ }, "node_modules/@earendil-works/pi-coding-agent/node_modules/xml-naming": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "dev": true, "funding": [ { "type": "github", @@ -2510,17 +2049,14 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=16.0.0" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/yaml": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -2533,1750 +2069,275 @@ }, "node_modules/@earendil-works/pi-coding-agent/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", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/zod-to-json-schema": { "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, "license": "ISC", - "peer": true, "peerDependencies": { "zod": "^3.25.28 || ^4" } }, - "node_modules/@earendil-works/pi-tui": { - "version": "0.83.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.83.0.tgz", - "integrity": "sha512-IoYrb0rORjELmEpNtoCA/U8je3KopMkRAVJRdSzvXRvgb+Huo1gNh8Q5CSZvNOiYtDxJdj2tYZZHZ4B3+IN3hA==", - "license": "MIT", - "peer": true, - "dependencies": { - "get-east-asian-width": "1.6.0", - "marked": "18.0.5" - }, - "engines": { - "node": ">=22.19.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { + "node_modules/@esbuild/linux-x64": { "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "aix" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@google/genai": { + "version": "1.52.0", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, "engines": { - "node": ">=18" + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@img/colour": { + "version": "1.1.0", "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "darwin" + "linux" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "freebsd" + "linux" ], - "engines": { - "node": ">=18" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", "cpu": [ - "arm64" + "x64" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "license": "MIT" }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "node_modules/@mistralai/mistralai": { + "version": "2.2.6", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } + "node_modules/@onkernel/cua-agent": { + "resolved": "packages/agent", + "link": true }, - "node_modules/@google/genai": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", - "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "p-retry": "^4.6.2", - "protobufjs": "^7.5.4", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.25.2" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } + "node_modules/@onkernel/cua-ai": { + "resolved": "packages/ai", + "link": true }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", - "engines": { - "node": ">=18" - } + "node_modules/@onkernel/cua-pi-extension": { + "resolved": "packages/pi-extension", + "link": true }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", - "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.2" - } + "node_modules/@onkernel/ptywright": { + "resolved": "packages/ptywright", + "link": true }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", - "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.2" - } + "node_modules/@onkernel/sdk": { + "version": "0.49.0", + "license": "Apache-2.0" }, - "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", - "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "node_modules/@opentelemetry/api": { + "version": "1.9.0", "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "dependencies": { - "@img/sharp-wasm32": "0.35.3" - }, "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", - "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", - "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node": ">=8.0.0" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", - "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", - "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", - "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", - "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", - "cpu": [ - "riscv64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", - "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", - "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", - "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", - "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", - "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", - "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", - "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", - "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", - "cpu": [ - "riscv64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", - "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.2" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", - "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.2" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", - "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", - "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.2" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", - "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.11.1" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", - "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@img/sharp-wasm32": "0.35.3" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", - "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", - "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", - "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mistralai/mistralai": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", - "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.40.0", - "ws": "^8.18.0", - "zod": "^3.25.0 || ^4.0.0", - "zod-to-json-schema": "^3.25.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - } - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@onkernel/cua-agent": { - "resolved": "packages/agent", - "link": true - }, - "node_modules/@onkernel/cua-ai": { - "resolved": "packages/ai", - "link": true - }, - "node_modules/@onkernel/cua-pi-extension": { - "resolved": "packages/pi-extension", - "link": true - }, - "node_modules/@onkernel/ptywright": { - "resolved": "packages/ptywright", - "link": true - }, - "node_modules/@onkernel/sdk": { - "version": "0.49.0", - "license": "Apache-2.0" - }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", - "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.134.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.134.0.tgz", - "integrity": "sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", - "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", - "license": "BSD-3-Clause" - }, - "node_modules/@quansync/fs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", - "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "quansync": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sxzz" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.0.tgz", - "integrity": "sha512-gCYzGOSkYY6Z034suzd20euvds7lPzMEEla62DJGE/ZAlR4OMBnNbvnBSsIGUCAr52gaWMsloGxP4tVGtN5aCA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.0.tgz", - "integrity": "sha512-JQBD77MNgu+4Z6RAyg69acugdrhhVoWesr3l47zohYZ2YV2fwkWMArkN/2p4l6Ei+Sno7W5q+UsKdVWq5Ens0w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.0.tgz", - "integrity": "sha512-p/8cXUTK4Sob604e+xxPhVSbDFf29E6J0l/xESM9rdCfn3aDai3nEs6TnMHUsdD5aNlFz0+gDbiGlozLKGa2YA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.0.tgz", - "integrity": "sha512-KbtOSlVv6fElujiZWMcC3aQYhEwLVVf073RcwlSmpGQvIsKZFUqc0ef4sjUuurRwfbiI6JJXji9DQn+86hawmQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.0.tgz", - "integrity": "sha512-9fZ9i0o0/MQaw7om6Z6TsT7tfCk0jtbEFtC+aPqZL5RNsGWNcHvn6EHgL3dAprjq+AZzPTAQjg2JtpJaMt+6pg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.0.tgz", - "integrity": "sha512-+tog7T66i+yFyIuuAnjL6xmW182W/qTBOUt6BtQ6lBIM1Eikh/fSMz4HGgvuCp5uU0zuIVWng7kDYthjCMOHcg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.0.tgz", - "integrity": "sha512-4b7yruLIIj/oZ3GpcLOvxcLCLDMraohn3IhQfN2hBP4w9UekG0DTIajWguJosRGfySf/+h/NwRUiMKoCpxCrqQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.0.tgz", - "integrity": "sha512-QRDOVZd0bhQ5jLsUsCC3dUxDWdTSVY9WMznowZgCGOrZfLLgctWpelhUASEiBwsXfat/JwYnVd1EaxMhqyT+UQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.0.tgz", - "integrity": "sha512-ypxT+Hq76NFG7woFbNbySnGEajFuYuIXeKz/jfCU+lXUoxfi3zLE6OG/ZQNeK3RpZSYJlAe2bokpsQ046CaieQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.0.tgz", - "integrity": "sha512-IdovCmfROFmpTLahdecTDFL74aLERVYN68F/mLZjfVh6LfoplPfI6deyHNMTcVujbokDV5k05XrFO22zfv+qjg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.0.tgz", - "integrity": "sha512-pcA8xlFp2tyk9T2R6Fi/rPe3bQ1MA+sSMDNUU5Ogu80GHOatkE4P8YCreGAvZErm5Ho2YRXnyvNrWiRncfVysQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.0.tgz", - "integrity": "sha512-4+fexHayrLCWpriPh4c6dNvL4an34DEZCG7zOM/FD5QNF6h8DT+bDXzyB/kfC8lDJbaFb7jKShtnjDQFXVQEjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.0.tgz", - "integrity": "sha512-SbL++MNmOw6QamrwIGDMSSfM4ceTzFr+RjbOExJSLLBinScU4WI5OdA413h1qwPw2yH7lVF1+H4svQ+6mSXKTQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.0.tgz", - "integrity": "sha512-+xTE6XC7wBgk0VKRXGG+QAnyW5S9b8vfsFpiMjf0waQTmSQSU8onsH/beyZ8X4aXVveJnotiy7VDjLOaW8bTrg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.0.tgz", - "integrity": "sha512-Ogji1TQNqH3ACLnYr+1Ns1nyrJ0CO2P585u9Hsh02pXvtFiFpgtgT2b3P4PnCOU86VVCvqtAeCN4OftMT8KU4w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", - "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", - "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", - "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", - "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", - "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", - "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", - "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", - "cpu": [ - "arm" - ], + "node_modules/@oxc-project/types": { + "version": "0.134.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "funding": { + "url": "https://github.com/sponsors/Boshen" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", - "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", - "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", - "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", - "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", - "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", - "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", - "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", - "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", - "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", - "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", - "cpu": [ - "s390x" - ], + "node_modules/@quansync/fs": { + "version": "1.0.0", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", - "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.0", "cpu": [ "x64" ], @@ -4285,26 +2346,13 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", - "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", - "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.0", "cpu": [ "x64" ], @@ -4312,55 +2360,19 @@ "license": "MIT", "optional": true, "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", - "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", - "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", - "cpu": [ - "arm64" + "linux" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", - "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", - "cpu": [ - "ia32" - ], + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-win32-x64-gnu": { + "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", - "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", "cpu": [ "x64" ], @@ -4368,13 +2380,11 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ] }, - "node_modules/@rollup/rollup-win32-x64-msvc": { + "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", - "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", "cpu": [ "x64" ], @@ -4382,13 +2392,11 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ] }, "node_modules/@smithy/core": { "version": "3.31.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.31.1.tgz", - "integrity": "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.16.1", @@ -4400,8 +2408,6 @@ }, "node_modules/@smithy/credential-provider-imds": { "version": "4.4.16", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.16.tgz", - "integrity": "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.31.1", @@ -4414,8 +2420,6 @@ }, "node_modules/@smithy/fetch-http-handler": { "version": "5.6.13", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.13.tgz", - "integrity": "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.31.1", @@ -4428,8 +2432,6 @@ }, "node_modules/@smithy/is-array-buffer": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4440,8 +2442,6 @@ }, "node_modules/@smithy/node-http-handler": { "version": "4.7.3", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", - "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.24.3", @@ -4454,8 +2454,6 @@ }, "node_modules/@smithy/signature-v4": { "version": "5.6.12", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.12.tgz", - "integrity": "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.31.1", @@ -4468,8 +2466,6 @@ }, "node_modules/@smithy/types": { "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz", - "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4480,8 +2476,6 @@ }, "node_modules/@smithy/util-buffer-from": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", @@ -4493,8 +2487,6 @@ }, "node_modules/@smithy/util-utf8": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", @@ -4504,21 +2496,8 @@ "node": ">=14.0.0" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -4528,22 +2507,16 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/jsesc": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", - "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", "dev": true, "license": "MIT" }, @@ -4556,14 +2529,10 @@ }, "node_modules/@types/retry": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, "node_modules/@vitest/expect": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", "dev": true, "license": "MIT", "dependencies": { @@ -4579,8 +2548,6 @@ }, "node_modules/@vitest/mocker": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", "dependencies": { @@ -4606,8 +2573,6 @@ }, "node_modules/@vitest/pretty-format": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { @@ -4619,8 +2584,6 @@ }, "node_modules/@vitest/runner": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", "dev": true, "license": "MIT", "dependencies": { @@ -4634,8 +2597,6 @@ }, "node_modules/@vitest/snapshot": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, "license": "MIT", "dependencies": { @@ -4649,8 +2610,6 @@ }, "node_modules/@vitest/spy": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4662,8 +2621,6 @@ }, "node_modules/@vitest/utils": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { @@ -4677,8 +2634,6 @@ }, "node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", "engines": { "node": ">= 14" @@ -4686,8 +2641,6 @@ }, "node_modules/ansis": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz", - "integrity": "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==", "dev": true, "license": "ISC", "engines": { @@ -4696,8 +2649,6 @@ }, "node_modules/assertion-error": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -4706,8 +2657,6 @@ }, "node_modules/ast-kit": { "version": "3.0.0-beta.1", - "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-3.0.0-beta.1.tgz", - "integrity": "sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==", "dev": true, "license": "MIT", "dependencies": { @@ -4724,8 +2673,6 @@ }, "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==", "funding": [ { "type": "github", @@ -4744,8 +2691,6 @@ }, "node_modules/bignumber.js": { "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", "engines": { "node": "*" @@ -4753,8 +2698,6 @@ }, "node_modules/birpc": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz", - "integrity": "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==", "dev": true, "license": "MIT", "funding": { @@ -4763,20 +2706,14 @@ }, "node_modules/bowser": { "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, "node_modules/cac": { "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, "license": "MIT", "engines": { @@ -4785,8 +2722,6 @@ }, "node_modules/chai": { "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", "dependencies": { @@ -4802,8 +2737,6 @@ }, "node_modules/check-error": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, "license": "MIT", "engines": { @@ -4812,8 +2745,6 @@ }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "license": "MIT", "engines": { "node": ">= 12" @@ -4836,8 +2767,6 @@ }, "node_modules/deep-eql": { "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", "engines": { @@ -4846,15 +2775,11 @@ }, "node_modules/defu": { "version": "6.1.7", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", - "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "dev": true, "license": "MIT" }, "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==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -4862,8 +2787,6 @@ }, "node_modules/diff": { "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -4871,8 +2794,6 @@ }, "node_modules/dts-resolver": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-3.0.0.tgz", - "integrity": "sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==", "dev": true, "license": "MIT", "engines": { @@ -4892,8 +2813,6 @@ }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" @@ -4901,8 +2820,6 @@ }, "node_modules/empathic": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", - "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", "dev": true, "license": "MIT", "engines": { @@ -4911,15 +2828,11 @@ }, "node_modules/es-module-lexer": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, "node_modules/esbuild": { "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4960,8 +2873,6 @@ }, "node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -4970,8 +2881,6 @@ }, "node_modules/expect-type": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4980,14 +2889,10 @@ }, "node_modules/extend": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, "node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -5004,8 +2909,6 @@ }, "node_modules/fetch-blob": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "funding": [ { "type": "github", @@ -5027,8 +2930,6 @@ }, "node_modules/formdata-polyfill": { "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" @@ -5037,25 +2938,8 @@ "node": ">=12.20.0" } }, - "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/gaxios": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.0.tgz", - "integrity": "sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA==", "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", @@ -5068,8 +2952,6 @@ }, "node_modules/gcp-metadata": { "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", "license": "Apache-2.0", "dependencies": { "gaxios": "^7.0.0", @@ -5080,23 +2962,8 @@ "node": ">=18" } }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/google-auth-library": { "version": "10.9.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz", - "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", @@ -5112,8 +2979,6 @@ }, "node_modules/google-logging-utils": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", "license": "Apache-2.0", "engines": { "node": ">=14" @@ -5121,15 +2986,11 @@ }, "node_modules/hookable": { "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", - "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", "dev": true, "license": "MIT" }, "node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -5141,8 +3002,6 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -5161,8 +3020,6 @@ }, "node_modules/import-without-cache": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.4.0.tgz", - "integrity": "sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==", "dev": true, "license": "MIT", "engines": { @@ -5174,15 +3031,11 @@ }, "node_modules/js-tokens": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "dev": true, "license": "MIT" }, "node_modules/jsesc": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -5194,8 +3047,6 @@ }, "node_modules/json-bigint": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "license": "MIT", "dependencies": { "bignumber.js": "^9.0.0" @@ -5203,8 +3054,6 @@ }, "node_modules/json-schema-to-ts": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", @@ -5216,8 +3065,6 @@ }, "node_modules/jwa": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", "dependencies": { "buffer-equal-constant-time": "^1.0.1", @@ -5227,8 +3074,6 @@ }, "node_modules/jws": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { "jwa": "^2.0.1", @@ -5237,48 +3082,27 @@ }, "node_modules/long": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, "node_modules/loupe": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, "license": "MIT" }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/marked": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", - "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", - "license": "MIT", - "peer": true, - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/ms": { "version": "2.1.3", "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -5296,8 +3120,6 @@ }, "node_modules/node-addon-api": { "version": "8.7.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", - "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", "license": "MIT", "engines": { "node": "^18 || ^20 || >= 21" @@ -5305,9 +3127,6 @@ }, "node_modules/node-domexception": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", "funding": [ { "type": "github", @@ -5325,8 +3144,6 @@ }, "node_modules/node-fetch": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", @@ -5343,8 +3160,6 @@ }, "node_modules/node-pty": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz", - "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -5353,14 +3168,10 @@ }, "node_modules/node-pty/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==", "license": "MIT" }, "node_modules/obug": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", - "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -5392,8 +3203,6 @@ }, "node_modules/p-retry": { "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", "license": "MIT", "dependencies": { "@types/retry": "0.12.0", @@ -5405,21 +3214,15 @@ }, "node_modules/partial-json": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", - "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", "license": "MIT" }, "node_modules/pathe": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/pathval": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { @@ -5428,15 +3231,11 @@ }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -5448,8 +3247,6 @@ }, "node_modules/postcss": { "version": "8.5.25", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -5477,8 +3274,6 @@ }, "node_modules/protobufjs": { "version": "7.6.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", - "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -5500,8 +3295,6 @@ }, "node_modules/quansync": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", - "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", "dev": true, "funding": [ { @@ -5517,8 +3310,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": { @@ -5527,8 +3318,6 @@ }, "node_modules/retry": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "license": "MIT", "engines": { "node": ">= 4" @@ -5536,8 +3325,6 @@ }, "node_modules/rolldown": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.0.tgz", - "integrity": "sha512-zpMvlJhs5PkXRTtKc0CaLBVI9AR/VDiJFpM+kx//hgToEca7FgMlGjaRIisXBcb19T76LswgmKECSQ96hjWr5A==", "dev": true, "license": "MIT", "dependencies": { @@ -5570,8 +3357,6 @@ }, "node_modules/rolldown-plugin-dts": { "version": "0.25.2", - "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.25.2.tgz", - "integrity": "sha512-nMhN/R+vmR8GM45ZW1FWMSjRTSDDn/6w4GTf8RNrEFCBdl8B1kySWrU1ixPtbwzXoRlcO+R/S88VgXuJQwfdDg==", "dev": true, "license": "MIT", "dependencies": { @@ -5614,8 +3399,6 @@ }, "node_modules/rolldown-plugin-dts/node_modules/get-tsconfig": { "version": "5.0.0-beta.5", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.5.tgz", - "integrity": "sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5630,8 +3413,6 @@ }, "node_modules/rollup": { "version": "4.60.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", - "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5675,8 +3456,6 @@ }, "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==", "funding": [ { "type": "github", @@ -5695,8 +3474,6 @@ }, "node_modules/semver": { "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5707,8 +3484,6 @@ }, "node_modules/sharp": { "version": "0.35.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", - "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.1.0", @@ -5756,15 +3531,11 @@ }, "node_modules/siginfo": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -5773,8 +3544,6 @@ }, "node_modules/stackback": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, @@ -5785,8 +3554,6 @@ }, "node_modules/strip-literal": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", "dev": true, "license": "MIT", "dependencies": { @@ -5798,22 +3565,16 @@ }, "node_modules/tinybench": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -5829,8 +3590,6 @@ }, "node_modules/tinypool": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", "engines": { @@ -5839,8 +3598,6 @@ }, "node_modules/tinyrainbow": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", "engines": { @@ -5849,8 +3606,6 @@ }, "node_modules/tinyspy": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, "license": "MIT", "engines": { @@ -5859,8 +3614,6 @@ }, "node_modules/tree-kill": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", "bin": { @@ -5869,14 +3622,10 @@ }, "node_modules/ts-algebra": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", "license": "MIT" }, "node_modules/tsdown": { "version": "0.22.2", - "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.22.2.tgz", - "integrity": "sha512-VX9gsyKXsTnBZjnIM4jsHl9aRv+GfgkE/k1hQslilaBfZMlaw3JuGR+6yhiU0QxWBtOCDnTjwOSoXzgB7Rr50g==", "dev": true, "license": "MIT", "dependencies": { @@ -5948,8 +3697,6 @@ }, "node_modules/tsdown/node_modules/cac": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", - "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", "dev": true, "license": "MIT", "engines": { @@ -5958,8 +3705,6 @@ }, "node_modules/tsdown/node_modules/tinyexec": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { @@ -5972,8 +3717,6 @@ }, "node_modules/tsx": { "version": "4.23.1", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", - "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5991,8 +3734,6 @@ }, "node_modules/typebox": { "version": "1.3.7", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", - "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", "license": "MIT" }, "node_modules/typescript": { @@ -6009,8 +3750,6 @@ }, "node_modules/unconfig-core": { "version": "7.5.0", - "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz", - "integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==", "dev": true, "license": "MIT", "dependencies": { @@ -6027,8 +3766,6 @@ }, "node_modules/vite": { "version": "7.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", - "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { @@ -6102,8 +3839,6 @@ }, "node_modules/vite-node": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", "dependencies": { @@ -6125,8 +3860,6 @@ }, "node_modules/vitest": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", - "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", "dev": true, "license": "MIT", "dependencies": { @@ -6198,8 +3931,6 @@ }, "node_modules/web-streams-polyfill": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "license": "MIT", "engines": { "node": ">= 8" @@ -6207,8 +3938,6 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -6224,8 +3953,6 @@ }, "node_modules/ws": { "version": "8.21.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", - "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6245,8 +3972,6 @@ }, "node_modules/yaml": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -6267,8 +3992,6 @@ }, "node_modules/zod-to-json-schema": { "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", "license": "ISC", "peerDependencies": { "zod": "^3.25.28 || ^4" @@ -6309,31 +4032,6 @@ "node": ">=22.19.0" } }, - "packages/cli": { - "name": "@onkernel/cua-cli", - "version": "0.9.0", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@earendil-works/pi-agent-core": "0.83.0", - "@earendil-works/pi-coding-agent": "0.83.0", - "@earendil-works/pi-tui": "0.83.0", - "@onkernel/cua-agent": "0.10.0", - "@onkernel/cua-ai": "0.10.0", - "@onkernel/sdk": "0.49.0" - }, - "bin": { - "cua": "dist/cli.js" - }, - "devDependencies": { - "@onkernel/ptywright": "0.1.0", - "tsdown": "^0.22.2", - "vitest": "^3.2.4" - }, - "engines": { - "node": ">=22.19.0" - } - }, "packages/pi-extension": { "name": "@onkernel/cua-pi-extension", "version": "0.10.0", @@ -6344,6 +4042,9 @@ "@onkernel/sdk": "0.49.0" }, "devDependencies": { + "@earendil-works/pi-agent-core": "0.83.0", + "@earendil-works/pi-ai": "0.83.0", + "@earendil-works/pi-coding-agent": "0.83.0", "vitest": "^3.2.4" }, "engines": { @@ -6352,8 +4053,7 @@ "peerDependencies": { "@earendil-works/pi-agent-core": "*", "@earendil-works/pi-ai": "*", - "@earendil-works/pi-coding-agent": "*", - "@earendil-works/pi-tui": "*" + "@earendil-works/pi-coding-agent": "*" } }, "packages/ptywright": { diff --git a/packages/pi-extension/package.json b/packages/pi-extension/package.json index bc18f445..9ecb18f0 100644 --- a/packages/pi-extension/package.json +++ b/packages/pi-extension/package.json @@ -47,10 +47,12 @@ "peerDependencies": { "@earendil-works/pi-agent-core": "*", "@earendil-works/pi-ai": "*", - "@earendil-works/pi-coding-agent": "*", - "@earendil-works/pi-tui": "*" + "@earendil-works/pi-coding-agent": "*" }, "devDependencies": { + "@earendil-works/pi-agent-core": "0.83.0", + "@earendil-works/pi-ai": "0.83.0", + "@earendil-works/pi-coding-agent": "0.83.0", "vitest": "^3.2.4" } } From 25e07efec92c0eaaf176109080a92c82b45bb15f Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:17:47 +0000 Subject: [PATCH 3/7] Reduce the menu to eight capability entries, and fix what QA found The menu is now one entry per capability: browser and computer (each including its batch form), browser-act, playwright, and the four provider-native surfaces. mixed, the standalone batch selectors, and the 37 individual tool names are gone -- they were packaging, not capability. Three fixes behind it, each verified against a live API rather than inferred: Anthropic rejects its native browser and computer tools in one request (400: viewport frame vs display frame). The compiler now refuses that pair instead of letting it reach the wire, and the menu reports it as a conflict rather than as unavailability. Google's schema quirk was not a model limit but a serialization gap: the Gemini API rejects the JSON Schema keywords const and additionalProperties outright rather than ignoring them. A payload transform narrows both for Google, so Gemini now takes every browser tool including browser_act, and the quirk is deleted. A deactivated selection now warns on stderr outside TUI mode. Previously a scripted run lost its tools silently, created no browser, and let the model answer from memory with exit 0. /cua-tools compiles each entry on its own. It had been passing the current selection to the tool menu, whose verdicts are relative to that selection, so one failing selection marked everything unavailable. --- packages/ai/src/models.ts | 5 - packages/ai/src/tool-catalog.ts | 69 ++++++ packages/ai/test/menu.test.ts | 6 +- packages/ai/test/models.test.ts | 8 +- packages/ai/test/tool-catalog.test.ts | 40 ++++ packages/pi-extension/CHANGELOG.md | 19 +- packages/pi-extension/README.md | 51 +++-- packages/pi-extension/src/index.ts | 14 +- packages/pi-extension/src/render.ts | 9 +- packages/pi-extension/src/selection.ts | 228 ++++++------------- packages/pi-extension/test/extension.test.ts | 58 +++-- packages/pi-extension/test/selection.test.ts | 73 ++++-- 12 files changed, 350 insertions(+), 230 deletions(-) diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index a7feb6e4..a64b212d 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -91,11 +91,6 @@ export const CUA_NATIVE_SURFACES: readonly { * observed against the live API. */ export const CUA_MODEL_QUIRKS: readonly CuaModelQuirk[] = [ - { - provider: "google", - capabilities: { acceptsComplexSchemas: false, acceptsLargeSchemas: false }, - reason: "The Gemini API accepts a subset of JSON Schema for function declarations and rejects browser_wait_for's shape.", - }, { provider: "moonshotai", match: { kind: "exact", id: "kimi-k3" }, diff --git a/packages/ai/src/tool-catalog.ts b/packages/ai/src/tool-catalog.ts index eea1d4e2..4fa70dec 100644 --- a/packages/ai/src/tool-catalog.ts +++ b/packages/ai/src/tool-catalog.ts @@ -418,6 +418,25 @@ function validateToolsetCompatibility(model: Model, entries: readonly CuaCa throw new Error(`selected tools contribute incompatible native provider transports: ${[...nativeProviderKinds].join(", ")}`); } + // Anthropic rejects its native browser and native computer tools in one + // request, because the browser tool addresses a viewport coordinate frame and + // the computer tool a display frame. Verified against the live API, which + // answers 400 "browser_20260701 cannot be declared alongside a computer_* + // tool". Catch it at compile time rather than on the wire. + const anthropicNativeTypes = entries.flatMap((entry) => + entry.providerBinding?.kind === "anthropic-native" && isRecord(entry.providerBinding.declaration) + ? [String(entry.providerBinding.declaration.type ?? "")] + : [], + ); + const anthropicBrowser = anthropicNativeTypes.find((type) => type.startsWith("browser_")); + const anthropicComputer = anthropicNativeTypes.find((type) => type.startsWith("computer_")); + if (anthropicBrowser && anthropicComputer) { + throw new Error( + `Anthropic's native browser tool (${anthropicBrowser}) cannot be selected alongside its native computer tool (${anthropicComputer}): ` + + "the browser tool's viewport coordinate frame is incompatible with the computer tool's display frame", + ); + } + const requiresApis = new Set(entries.flatMap((entry) => bindingRequiresApi(entry.providerBinding))); if (requiresApis.size > 1) { throw new Error(`selected tools require incompatible provider transports: ${[...requiresApis].join(", ")}`); @@ -524,6 +543,9 @@ function compilePayloadTransforms(model: Model, entries: readonly CuaCatalo const google = entries.filter((entry) => entry.providerBinding?.kind === "google-native"); if (google.length > 0) transforms.push(createGoogleTransform(google)); + if (model.provider === "google" && entries.some((entry) => entry.transport === "function")) { + transforms.push(createGeminiSchemaTransform()); + } if (cuaModelCapabilities(model).serializesStateMutations && entries.some((entry) => entry.stateMutating)) { transforms.push({ @@ -538,6 +560,53 @@ function compilePayloadTransforms(model: Model, entries: readonly CuaCatalo return transforms; } +/** + * Gemini's function-declaration dialect is a subset of JSON Schema. It rejects + * the request outright on an unknown keyword rather than ignoring it, so a + * declaration carrying `const` or `additionalProperties` fails with + * `Invalid JSON payload received. Unknown name "const"`. + * + * Both have exact equivalents Gemini does accept: `const: x` is a single-value + * `enum`, and `additionalProperties: false` only tightens validation the model + * never performs. Rewriting them is what lets Google take the same declarations + * every other provider gets, verified against the live API. + */ +function createGeminiSchemaTransform(): CuaPayloadTransform { + return { + identity: "provider.google.function-declaration-schema", + writes: ["tools.functionDeclarations"], + phase: "tool-declarations", + apply(payload) { + if (!isRecord(payload) || !Array.isArray(payload.tools)) return payload; + return { + ...payload, + tools: payload.tools.map((tool) => + isRecord(tool) && Array.isArray(tool.functionDeclarations) + ? { ...tool, functionDeclarations: tool.functionDeclarations.map(narrowToGeminiSchema) } + : tool, + ), + }; + }, + }; +} + +const GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS = new Set(["additionalProperties", "$schema", "$defs", "definitions"]); + +function narrowToGeminiSchema(node: unknown): unknown { + if (Array.isArray(node)) return node.map(narrowToGeminiSchema); + if (!isRecord(node)) return node; + const result: Record = {}; + for (const [key, value] of Object.entries(node)) { + if (key === "const") { + result.enum = [value]; + continue; + } + if (GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS.has(key)) continue; + result[key] = narrowToGeminiSchema(value); + } + return result; +} + function createGoogleTransform(entries: readonly CuaCatalogEntryDraft[]): CuaPayloadTransform { const firstBinding = entries[0]!.providerBinding; if (firstBinding?.kind !== "google-native") throw new Error("invalid Google catalog entry"); diff --git a/packages/ai/test/menu.test.ts b/packages/ai/test/menu.test.ts index 47842d69..1115a418 100644 --- a/packages/ai/test/menu.test.ts +++ b/packages/ai/test/menu.test.ts @@ -33,9 +33,11 @@ describe("cuaToolMenu", () => { expect(act?.available).toBe(false); expect(act?.unavailableReason).toContain("does not accept the schema size"); + // Google used to fail here. It no longer does: the two JSON Schema keywords + // the Gemini API rejects are narrowed by the catalog's payload transform, so + // browser_wait_for compiles like any other function tool. const waitFor = cuaToolMenu("google:gemini-3.6-flash").find((entry) => entry.label === "browser_wait_for"); - expect(waitFor?.available).toBe(false); - expect(waitFor?.unavailableReason).toContain("does not accept the schema"); + expect(waitFor?.available).toBe(true); }); it("offers a model's own native surfaces and no other provider's", () => { diff --git a/packages/ai/test/models.test.ts b/packages/ai/test/models.test.ts index 7529fa20..515b7a19 100644 --- a/packages/ai/test/models.test.ts +++ b/packages/ai/test/models.test.ts @@ -204,8 +204,12 @@ describe("model quirks", () => { }); it("keeps the limits we have evidence for", () => { - // Observed live: the Gemini API rejects browser_wait_for's schema shape. - expect(cuaModelCapabilities(getCuaModel("google:gemini-3.6-flash")).acceptsComplexSchemas).toBe(false); + // Google no longer carries a schema quirk. What the Gemini API rejects is two + // JSON Schema keywords it does not know — `const` and `additionalProperties` — + // and the catalog narrows both for it, so the same declarations every other + // provider gets are accepted. Verified live against the Gemini API. + expect(cuaModelCapabilities(getCuaModel("google:gemini-3.6-flash")).acceptsComplexSchemas).toBe(true); + expect(cuaModelCapabilities(getCuaModel("google:gemini-3.6-flash")).acceptsLargeSchemas).toBe(true); // Observed live: Kimi K3 rejects the request once browser_act is attached. expect(cuaModelCapabilities(getCuaModel("moonshotai:kimi-k3")).acceptsLargeSchemas).toBe(false); expect(cuaModelCapabilities(getCuaModel("openrouter:moonshotai/kimi-k3")).acceptsLargeSchemas).toBe(false); diff --git a/packages/ai/test/tool-catalog.test.ts b/packages/ai/test/tool-catalog.test.ts index 10d04403..bc70d81f 100644 --- a/packages/ai/test/tool-catalog.test.ts +++ b/packages/ai/test/tool-catalog.test.ts @@ -4,6 +4,7 @@ import { callerToolIdentity, compileCuaToolCatalog, cua, + getCuaModel, GOOGLE_CUA_INTERACTIONS_API, OPENAI_CUA_COMPUTER_API, type CuaToolSpec, @@ -378,4 +379,43 @@ describe("transport derivation", () => { const recompiled = compile(nativeCatalog.model, cua.toolsets.browser()); expect(recompiled.model.api).toBe("openai-responses"); }); + +}); +describe("Gemini function-declaration schema", () => { + it("rewrites the two keywords the Gemini API rejects", async () => { + const catalog = compileCuaToolCatalog({ + model: getCuaModel("google:gemini-3.6-flash"), + requestedTools: [cua.tools.browser.waitFor()], + }); + const raw = { + tools: [ + { + functionDeclarations: catalog.toolDeclarations.map((tool) => ({ + name: tool.name, + description: tool.description, + parameters: tool.parameters, + })), + }, + ], + }; + const sent = JSON.stringify(await catalog.payload.apply(raw, catalog.model)); + + // Verified live: the Gemini API answers 400 `Unknown name "const"` and the + // same for additionalProperties, rather than ignoring what it does not know. + expect(JSON.stringify(raw)).toContain('"const"'); + expect(sent).not.toContain('"const"'); + expect(sent).not.toContain('"additionalProperties"'); + // `const: x` becomes a single-value enum, which means the same thing. + expect(sent).toContain('"enum":["text"]'); + }); + + it("leaves other providers' declarations untouched", async () => { + const catalog = compileCuaToolCatalog({ + model: getCuaModel("openai:gpt-5.6-sol"), + requestedTools: [cua.tools.browser.waitFor()], + }); + const raw = { tools: [{ functionDeclarations: [{ name: "browser_wait_for", parameters: catalog.toolDeclarations[0]!.parameters }] }] }; + const sent = JSON.stringify(await catalog.payload.apply(raw, catalog.model)); + expect(sent).toContain('"const"'); + }); }); diff --git a/packages/pi-extension/CHANGELOG.md b/packages/pi-extension/CHANGELOG.md index b1316f64..7c9925d6 100644 --- a/packages/pi-extension/CHANGELOG.md +++ b/packages/pi-extension/CHANGELOG.md @@ -8,10 +8,21 @@ it rather than reimplementing it. The `cua act` model-free executor path and the `--print -o jsonl` telemetry schema are gone with it. - Add `@onkernel/cua-pi-extension`, an installable pi extension that contributes - Kernel browser tools to pi's own agent session. Selectors cover the CDP browser - toolset, the canonical computer toolset, the batch and Playwright tools, and - every provider-native surface: Anthropic's computer and browser tools, OpenAI's - native computer tool, and Google's predefined browser action set. + Kernel browser tools to pi's own agent session. The menu is eight entries, one + per capability: `browser` and `computer` (primitives plus their batch form), + `browser-act`, `playwright`, and the four provider-native surfaces — + `anthropic-computer`, `anthropic-browser`, `openai-computer`, `google-browser`. + Packaging variants are deliberately absent: `mixed`, the batch tools on their + own, and the 37 individual tool names offered nothing the eight entries do not. +- A deactivated selection now reports itself on stderr in print and RPC modes, + once per distinct reason. Previously the reason reached only the TUI status + line, so a scripted run lost its tools silently, created no browser, and let + the model answer from memory with exit 0. +- `/cua-tools` decides each entry's availability by compiling it on its own, and + reports pairwise conflicts separately. It previously passed the current + selection to the tool menu, whose verdicts are relative to that selection, so a + selection that failed to compile marked every entry unavailable with its + error — including entries that then activated fine. - Provider-native surfaces work because the extension owns the stream for the providers it registers, swapping pi's registry model for the compiled catalog's model — which carries the transport the selected tools derive — and passing the diff --git a/packages/pi-extension/README.md b/packages/pi-extension/README.md index 52ed8bbd..e7891c6d 100644 --- a/packages/pi-extension/README.md +++ b/packages/pi-extension/README.md @@ -33,32 +33,30 @@ pi -p --provider anthropic --model claude-opus-5 --cua-tools anthropic-computer "Open example.com and report its heading" ``` -### Selectors - -| selector | tools | -| --- | --- | -| `browser` | the CDP browser toolset | -| `computer` | the canonical computer toolset | -| `mixed` | both, deduplicated | -| `browser-act` | `browser_act` alone, the verified-plan tool | -| `browser-batch`, `computer-batch` | one mechanical batch tool | -| `playwright` | `playwright_execute` | -| `anthropic-computer`, `anthropic-browser` | Anthropic's native surfaces | -| `openai-computer` | OpenAI's native computer tool | -| `google-browser` | Google's predefined browser action set | -| any individual tool name | that tool alone | - -Provider-native surfaces work because the extension **owns the stream** for the -providers it registers. pi resolves and streams its own registry model, but the -transport a native surface needs is derived onto the *compiled* model — so the -registered provider swaps in `catalog.model` (the resolved model with only `api` -replaced, so cost and context window are untouched) and adds the incoming -native-call plan that normalizes `computer_call`-style items and drives -Anthropic's browser-beta fallback. pi's resolved credential rides along in -`options.apiKey`. +### The menu + +Eight entries, one per capability. Availability is per model, and `/cua-tools` +tells you which apply to the one you selected. + +| entry | tools | works on | +| --- | --- | --- | +| `browser` | CDP browser primitives plus the one-call `browser_batch` form | every provider | +| `computer` | canonical computer primitives plus `computer_batch` | every provider | +| `browser-act` | `browser_act`, the verified-plan tool | every provider except Moonshot, which rejects its schema size | +| `playwright` | `playwright_execute` | every provider | +| `anthropic-computer` | Anthropic's native computer tool | Anthropic only | +| `anthropic-browser` | Anthropic's native browser tool | Anthropic only | +| `openai-computer` | OpenAI's native computer tool | OpenAI only | +| `google-browser` | Google's predefined browser action set | Google only | + +`anthropic-browser` and `anthropic-computer` cannot be selected together: +Anthropic rejects the pair because the browser tool addresses a viewport +coordinate frame and the computer tool a display frame. The catalog compiler +refuses it before the request goes out, and `/cua-tools` reports it as a conflict +rather than as unavailability. `--cua-coordinates` selects `pixels` (default) or `normalized-1000` for the -computer toolset's coordinate contract. +`computer` entry's coordinate contract. ### Commands @@ -73,6 +71,11 @@ deactivates it with a reason rather than failing at request time. Switching models re-checks, and restores a previously forced-off selection when the new model can take it. +In TUI mode the reason appears in the status line. In print and RPC modes there +is no status line, so the reason is written to **stderr** — once per distinct +reason. Without that, a deactivated selection is invisible: the tools are gone, +no browser is created, and the model answers from memory with exit 0. + ### Browser | flag | effect | diff --git a/packages/pi-extension/src/index.ts b/packages/pi-extension/src/index.ts index aa482f5f..61efe77a 100644 --- a/packages/pi-extension/src/index.ts +++ b/packages/pi-extension/src/index.ts @@ -39,6 +39,7 @@ export default function cuaPiExtension(pi: ExtensionAPI): void { let browserOptions: BrowserOptions = defaultBrowserOptions(); let activeNames = new Set(); let compatibilityError: string | undefined; + let warnedError: string | undefined; let initialized = false; let forcedInactive = false; let sessionActive = false; @@ -118,8 +119,17 @@ export default function cuaPiExtension(pi: ExtensionAPI): void { pi.setActiveTools(current.filter((name) => !allSpecs.has(name))); } initialized = true; - if (ctx.mode === "tui") + if (ctx.mode === "tui") { ctx.ui.setStatus("cua", statusText(selection.selectors, [...activeNames], runtime?.getStatus() ?? {}, compatibilityError)); + } else if (compatibilityError && compatibilityError !== warnedError) { + // Print and RPC have no status line, and silence here is the worst failure + // this extension can produce: the tools vanish, no browser is created, and + // the model answers from memory with exit 0. Say so on stderr, once per + // distinct reason so a multi-turn run does not repeat itself. + process.stderr.write(`cua: no browser tool is active — ${compatibilityError}\n`); + warnedError = compatibilityError; + } + if (!compatibilityError) warnedError = undefined; } /** * The compiled catalog for the model pi is about to stream with, or undefined @@ -191,7 +201,7 @@ export default function cuaPiExtension(pi: ExtensionAPI): void { ctx.ui.notify("cua: no pi model is selected", "error"); return; } - ctx.ui.notify(availabilityText(selectorAvailability(ctx.model, selection), selection.selectors), "info"); + ctx.ui.notify(availabilityText(selectorAvailability(ctx.model, selection)), "info"); return; } selection = parseSelection(args === "none" ? undefined : args, selection.coordinates); diff --git a/packages/pi-extension/src/render.ts b/packages/pi-extension/src/render.ts index b7ed0860..4c8e74fa 100644 --- a/packages/pi-extension/src/render.ts +++ b/packages/pi-extension/src/render.ts @@ -10,11 +10,12 @@ export function statusText(selectors: readonly string[], active: readonly string } /** One line per selector, so an unavailable one carries the compiler's own reason. */ -export function availabilityText(entries: readonly SelectorAvailability[], selected: readonly string[]): string { - const chosen = new Set(selected); +export function availabilityText(entries: readonly SelectorAvailability[]): string { const lines = entries.map((entry) => { - const mark = chosen.has(entry.selector) ? "*" : " "; - return entry.available ? `${mark} ${entry.selector}` : `${mark} ${entry.selector} — unavailable: ${entry.reason ?? "unknown"}`; + const mark = entry.selected ? "*" : " "; + if (!entry.available) return `${mark} ${entry.selector} — unavailable: ${entry.reason ?? "unknown"}`; + const conflict = entry.conflictsWith.length ? ` (cannot combine with ${entry.conflictsWith.join(", ")})` : ""; + return `${mark} ${entry.selector}${conflict}`; }); return ["cua selectors for this model (* = selected):", ...lines].join("\n"); } diff --git a/packages/pi-extension/src/selection.ts b/packages/pi-extension/src/selection.ts index 47cce42f..8b816a2f 100644 --- a/packages/pi-extension/src/selection.ts +++ b/packages/pi-extension/src/selection.ts @@ -1,45 +1,6 @@ import type { Api, Model } from "@earendil-works/pi-ai"; import { compileCuaToolCatalog, cua, cuaToolMenu, type CuaToolCatalog, type CuaToolSpec } from "@onkernel/cua-ai"; -export const BROWSER_BATCH_ACTIONS = [ - "snapshot", - "text", - "find", - "click", - "hover", - "drag", - "fill", - "scroll_to", - "scroll", - "type", - "key", - "navigate", - "list_tabs", - "new_tab", - "screenshot", - "evaluate", - "wait_for", -] as const; -export const COMPUTER_BATCH_ACTIONS = [ - "click", - "double_click", - "mouse_down", - "mouse_up", - "type", - "keypress", - "scroll", - "move", - "drag", - "wait", - "screenshot", - "zoom", - "goto", - "back", - "forward", - "url", - "cursor_position", -] as const; - type Coordinates = "pixels" | "normalized-1000"; type CoordinateSystem = ReturnType | ReturnType; @@ -48,76 +9,44 @@ export interface CuaSelection { coordinates: Coordinates; } -const generalTools = Object.freeze({ - browser_snapshot: () => cua.tools.browser.snapshot(), - browser_text: () => cua.tools.browser.text(), - browser_find: () => cua.tools.browser.find(), - browser_click: () => cua.tools.browser.click(), - browser_hover: () => cua.tools.browser.hover(), - browser_drag: () => cua.tools.browser.drag(), - browser_fill: () => cua.tools.browser.fill(), - browser_scroll_to: () => cua.tools.browser.scrollTo(), - browser_scroll: () => cua.tools.browser.scroll(), - browser_type: () => cua.tools.browser.type(), - browser_key: () => cua.tools.browser.key(), - browser_navigate: () => cua.tools.browser.navigate(), - browser_list_tabs: () => cua.tools.browser.listTabs(), - browser_new_tab: () => cua.tools.browser.newTab(), - browser_screenshot: () => cua.tools.browser.screenshot(), - browser_evaluate: () => cua.tools.browser.evaluate(), - browser_wait_for: () => cua.tools.browser.waitFor(), - browser_act: () => cua.tools.browser.act(), - playwright_execute: () => cua.tools.playwright(), -}); - -const computerTools = Object.freeze({ - computer_click: (coordinates: CoordinateSystem) => cua.tools.computer.click({ coordinates }), - computer_double_click: (coordinates: CoordinateSystem) => cua.tools.computer.doubleClick({ coordinates }), - computer_mouse_down: (coordinates: CoordinateSystem) => cua.tools.computer.mouseDown({ coordinates }), - computer_mouse_up: (coordinates: CoordinateSystem) => cua.tools.computer.mouseUp({ coordinates }), - computer_type: (coordinates: CoordinateSystem) => cua.tools.computer.type({ coordinates }), - computer_keypress: (coordinates: CoordinateSystem) => cua.tools.computer.keypress({ coordinates }), - computer_scroll: (coordinates: CoordinateSystem) => cua.tools.computer.scroll({ coordinates }), - computer_move: (coordinates: CoordinateSystem) => cua.tools.computer.move({ coordinates }), - computer_drag: (coordinates: CoordinateSystem) => cua.tools.computer.drag({ coordinates }), - computer_wait: (coordinates: CoordinateSystem) => cua.tools.computer.wait({ coordinates }), - computer_screenshot: (coordinates: CoordinateSystem) => cua.tools.computer.screenshot({ coordinates }), - computer_zoom: (coordinates: CoordinateSystem) => cua.tools.computer.zoom({ coordinates }), - computer_goto: (coordinates: CoordinateSystem) => cua.tools.computer.goto({ coordinates }), - computer_back: (coordinates: CoordinateSystem) => cua.tools.computer.back({ coordinates }), - computer_forward: (coordinates: CoordinateSystem) => cua.tools.computer.forward({ coordinates }), - computer_url: (coordinates: CoordinateSystem) => cua.tools.computer.url({ coordinates }), - computer_cursor_position: (coordinates: CoordinateSystem) => cua.tools.computer.cursorPosition({ coordinates }), -}); +const BROWSER_BATCH_ACTIONS = [ + "snapshot", "text", "find", "click", "hover", "drag", "fill", "scroll_to", "scroll", + "type", "key", "navigate", "list_tabs", "new_tab", "screenshot", "evaluate", "wait_for", +] as const; +const COMPUTER_BATCH_ACTIONS = [ + "click", "double_click", "mouse_down", "mouse_up", "type", "keypress", "scroll", "move", + "drag", "wait", "screenshot", "zoom", "goto", "back", "forward", "url", "cursor_position", +] as const; /** - * Provider-native surfaces, selected as a unit under one selector each. + * The menu: one entry per capability a caller would actually choose between. * - * These reach the wire because this extension owns the stream for the providers - * it registers: it swaps pi's registry model for the compiled catalog's model, - * which carries the transport the selected tools derive, and passes the incoming - * native-call plan. Without that, `requiresApi` would never take effect and - * native calls would arrive unnormalized. + * Entries are capabilities, not packaging. Earlier revisions also offered `mixed`, + * the two batch tools on their own, and all 37 individual tool names — which made + * the menu long without offering anything the entries below do not already cover. + * The batch tool now ships inside its generic entry, so selecting `browser` gets + * both the primitives and the one-call batch form of them. + * + * Provider-native entries reach the wire because the extension owns the stream for + * the providers it registers: it swaps pi's registry model for the compiled + * catalog's model, which carries the transport the selected tools derive, and + * passes the incoming native-call plan. */ -const nativeToolsets = Object.freeze({ +const MENU: Readonly CuaToolSpec[]>> = Object.freeze({ + browser: () => [...cua.toolsets.browser(), cua.tools.browser.batch({ actions: BROWSER_BATCH_ACTIONS })], + computer: (coordinates) => [ + ...cua.toolsets.computer({ coordinates }), + cua.tools.computer.batch({ actions: COMPUTER_BATCH_ACTIONS, coordinates }), + ], + "browser-act": () => [cua.tools.browser.act()], + playwright: () => [cua.tools.playwright()], "anthropic-computer": () => [cua.providers.anthropic.tools.computer({ version: "20260701", enableZoom: true })], "anthropic-browser": () => [cua.providers.anthropic.tools.browser({ version: "20260701", javascript: true })], "openai-computer": () => [cua.providers.openai.tools.computer()], "google-browser": () => cua.providers.google.toolsets.browser(), }); -export const CUA_TOOL_NAMES = Object.freeze([...Object.keys(generalTools), ...Object.keys(computerTools)]); -export const CUA_SELECTORS = Object.freeze([ - "browser", - "computer", - "mixed", - "browser-act", - "browser-batch", - "computer-batch", - "playwright", - ...Object.keys(nativeToolsets), - ...CUA_TOOL_NAMES, -]); +export const CUA_SELECTORS: readonly string[] = Object.freeze(Object.keys(MENU)); export function parseSelection(value: string | undefined, coordinates: string | undefined): CuaSelection { const coordinateMode = coordinates ?? "pixels"; @@ -136,7 +65,13 @@ export function parseSelection(value: string | undefined, coordinates: string | return Object.freeze({ selectors: Object.freeze(selectors), coordinates: coordinateMode }); } -/** Every function tool that can be selected, with declarations for one coordinate mode. */ +/** + * Every tool any menu entry can contribute, for the up-front registration pi + * requires before a tool can be activated. Keyed by model-facing name, so the + * two providers that both call their native tool `computer` collapse to one + * registration — harmless, because a native declaration is replaced by the + * catalog's payload transform and only the selected spec is ever executed. + */ export function allSelectableSpecs(coordinates: Coordinates): CuaToolSpec[] { const result = new Map(); for (const selector of CUA_SELECTORS) { @@ -149,36 +84,9 @@ export function expandSelection(selection: CuaSelection): CuaToolSpec[] { const coordinates = selection.coordinates === "pixels" ? cua.coordinates.pixels() : cua.coordinates.normalized([0, 1000]); const result: CuaToolSpec[] = []; for (const selector of selection.selectors) { - const native = nativeToolsets[selector as keyof typeof nativeToolsets]; - if (native) { - result.push(...native()); - continue; - } - switch (selector) { - case "browser": - result.push(...cua.toolsets.browser()); - break; - case "computer": - result.push(...cua.toolsets.computer({ coordinates })); - break; - case "mixed": - result.push(...cua.toolsets.mixed({ coordinates })); - break; - case "browser-act": - result.push(cua.tools.browser.act()); - break; - case "browser-batch": - result.push(cua.tools.browser.batch({ actions: BROWSER_BATCH_ACTIONS })); - break; - case "computer-batch": - result.push(cua.tools.computer.batch({ actions: COMPUTER_BATCH_ACTIONS, coordinates })); - break; - case "playwright": - result.push(cua.tools.playwright()); - break; - default: - result.push(createIndividualTool(selector, coordinates)); - } + const entry = MENU[selector]; + if (!entry) throw new Error(`unknown CUA tool selector "${selector}"`); + result.push(...entry(coordinates)); } const identities = new Set(); for (const spec of result) { @@ -188,14 +96,6 @@ export function expandSelection(selection: CuaSelection): CuaToolSpec[] { return result; } -function createIndividualTool(name: string, coordinates: CoordinateSystem): CuaToolSpec { - const createComputerTool = computerTools[name as keyof typeof computerTools]; - if (createComputerTool) return createComputerTool(coordinates); - const createGeneralTool = generalTools[name as keyof typeof generalTools]; - if (createGeneralTool) return createGeneralTool(); - throw new Error(`unknown CUA tool selector "${name}"`); -} - /** * Compile a selection for a model. Declaration-only and browser-free, which is * what lets the extension validate a selection and generate headers before any @@ -210,32 +110,52 @@ export interface SelectorAvailability { readonly tools: readonly string[]; readonly available: boolean; readonly reason?: string; + readonly selected: boolean; + /** Selectors this one cannot be combined with for this model. */ + readonly conflictsWith: readonly string[]; } /** - * Every selector marked available or not for a model, decided by compiling the - * candidate catalog rather than by restating the compiler's rules. Native - * surfaces are reported through `cuaToolMenu`, whose verdicts are pairwise - * against the current selection; the rest compile on their own. + * Every selector marked available or not for a model, decided by compiling that + * selector *on its own*. + * + * Standalone is the right question here, and getting it wrong was a real bug: an + * earlier version passed the current selection to `cuaToolMenu`, whose verdicts + * are deliberately pairwise — relative to what is already selected. When the + * current selection itself failed to compile, that failure became the reason on + * every row, including rows that then activated fine. The one command whose job + * is "tell me what this model can take" misled precisely when it was needed. + * + * Pairwise conflicts still exist — Anthropic's native browser and computer cannot + * coexist, and two providers' natives never can — so `conflictsWith` reports what + * a selector cannot be *combined* with, separately from whether it is available. */ export function selectorAvailability(model: Model, selection: CuaSelection): SelectorAvailability[] { - const menu = cuaToolMenu(model, expandSelection(selection)); - const reasonByIdentity = new Map(menu.map((entry) => [entry.key, entry.available ? undefined : entry.unavailableReason])); + const selected = new Set(selection.selectors); return CUA_SELECTORS.map((selector) => { - let specs: CuaToolSpec[]; - try { - specs = expandSelection({ selectors: [selector], coordinates: selection.coordinates }); - } catch (error) { - return { selector, tools: [], available: false, reason: message(error) }; - } + const specs = expandSelection({ selectors: [selector], coordinates: selection.coordinates }); const tools = specs.map((spec) => spec.name); - const menuReason = specs.map((spec) => reasonByIdentity.get(spec.identity)).find(Boolean); - if (menuReason) return { selector, tools, available: false, reason: menuReason }; + const conflictsWith = CUA_SELECTORS.filter((other) => { + if (other === selector) return false; + try { + compileSpecs(model, expandSelection({ selectors: [selector, other], coordinates: selection.coordinates })); + return false; + } catch { + // Only a genuine pairwise conflict counts: if `other` cannot compile on + // its own, the pair failing says nothing about this selector. + try { + compileSpecs(model, expandSelection({ selectors: [other], coordinates: selection.coordinates })); + return true; + } catch { + return false; + } + } + }); try { compileSpecs(model, specs); - return { selector, tools, available: true }; + return { selector, tools, available: true, selected: selected.has(selector), conflictsWith }; } catch (error) { - return { selector, tools, available: false, reason: message(error) }; + return { selector, tools, available: false, reason: message(error), selected: selected.has(selector), conflictsWith }; } }); } diff --git a/packages/pi-extension/test/extension.test.ts b/packages/pi-extension/test/extension.test.ts index 6c4bdc48..1a57db97 100644 --- a/packages/pi-extension/test/extension.test.ts +++ b/packages/pi-extension/test/extension.test.ts @@ -5,7 +5,7 @@ import { getCuaModel } from "@onkernel/cua-ai"; import { describe, expect, it, vi } from "vitest"; import { CuaBrowserRuntime } from "../src/browser-runtime"; -import { allSelectableSpecs } from "../src/selection"; +import { allSelectableSpecs, expandSelection, parseSelection } from "../src/selection"; import extension from "../src/index"; type Handler = (event: unknown, ctx: ExtensionContext) => unknown; @@ -97,7 +97,7 @@ const anthropicCtx = { ...ctx, model: getCuaModel("anthropic:claude-fable-5") } describe("pi extension activation", () => { it("reads parsed flags at session_start, installs selectable batch tools, and preserves unrelated tools", async () => { const pi = makePi({ - "cua-tools": "browser-batch", + "cua-tools": "browser", "cua-coordinates": "pixels", "cua-browser-timeout": "300", "cua-profile-save-changes": false, @@ -105,7 +105,7 @@ describe("pi extension activation", () => { extension(pi.api); await getHandler(pi, "session_start")({}, ctx); expect(pi.tools.map((tool) => tool.name)).toEqual(expect.arrayContaining(allSelectableSpecs("pixels").map((tool) => tool.name))); - expect(pi.active).toEqual(["bash", "browser_batch"]); + expect(pi.active).toEqual(["bash", ...expandSelection(parseSelection("browser", "pixels")).map((spec) => spec.name)]); }); it("rejects invalid parsed flags instead of silently activating no tools", () => { @@ -171,7 +171,7 @@ describe("pi extension activation", () => { it("applies provider transforms only for the active CUA subset", async () => { const pi = makePi({ - "cua-tools": "browser_snapshot", + "cua-tools": "playwright", "cua-coordinates": "pixels", "cua-browser-timeout": "300", "cua-profile-save-changes": false, @@ -191,7 +191,7 @@ describe("pi extension activation", () => { it("does not persist a flag baseline and restores only command-origin selections", async () => { const pi = makePi({ - "cua-tools": "browser_snapshot", + "cua-tools": "playwright", "cua-coordinates": "pixels", "cua-browser-timeout": "300", "cua-profile-save-changes": false, @@ -211,7 +211,7 @@ describe("pi extension activation", () => { ]); const resumed = makePi({ - "cua-tools": "browser_snapshot", + "cua-tools": "playwright", "cua-coordinates": "pixels", "cua-browser-timeout": "300", "cua-profile-save-changes": false, @@ -223,7 +223,7 @@ describe("pi extension activation", () => { expect(resumed.active).not.toContain("browser_snapshot"); const legacy = makePi({ - "cua-tools": "browser_snapshot", + "cua-tools": "playwright", "cua-coordinates": "pixels", "cua-browser-timeout": "300", "cua-profile-save-changes": false, @@ -242,7 +242,7 @@ describe("pi extension activation", () => { } as unknown as ExtensionContext; extension(legacy.api); await getHandler(legacy, "session_start")({}, legacyCtx); - expect(legacy.active).toContain("browser_snapshot"); + expect(legacy.active).toContain("playwright_execute"); expect(legacy.active).not.toContain("computer_click"); }); @@ -277,7 +277,7 @@ describe("pi extension activation", () => { it("keeps an ordinary function tool active on a model the registry does not carry", async () => { const pi = makePi({ - "cua-tools": "browser_snapshot", + "cua-tools": "playwright", "cua-coordinates": "pixels", "cua-browser-timeout": "300", "cua-profile-save-changes": false, @@ -292,13 +292,43 @@ describe("pi extension activation", () => { // Removing the model allowlist made this the expected outcome: a plain // function tool has no provider binding to violate, so it stays selected. await getHandler(pi, "before_provider_request")({ payload: { tools: [] } }, unlisted); - expect(pi.active).toContain("browser_snapshot"); + expect(pi.active).toContain("playwright_execute"); + }); + + it("warns on stderr when a selection deactivates outside TUI mode", async () => { + const written: string[] = []; + const write = vi.spyOn(process.stderr, "write").mockImplementation(((chunk: string) => { + written.push(String(chunk)); + return true; + }) as never); + try { + const pi = makePi({ + "cua-tools": "anthropic-computer", + "cua-coordinates": "pixels", + "cua-browser-timeout": "300", + "cua-profile-save-changes": false, + }); + extension(pi.api); + // An OpenAI model cannot take Anthropic's native computer tool. Without this + // warning the tools vanish, no browser is created, and the model answers + // from memory with exit 0 — the worst failure this extension can produce. + await getHandler(pi, "session_start")({}, ctx); + + expect(pi.active).not.toContain("computer"); + expect(written.join("")).toMatch(/cua: no browser tool is active — .*requires a anthropic model/); + // One warning per distinct reason, not once per reconcile. + const before = written.length; + await getHandler(pi, "before_agent_start")({}, ctx); + expect(written.length).toBe(before); + } finally { + write.mockRestore(); + } }); it("lists selector availability without changing the selection, and clears it only on request", async () => { const notices: string[] = []; const pi = makePi({ - "cua-tools": "browser_snapshot", + "cua-tools": "playwright", "cua-coordinates": "pixels", "cua-browser-timeout": "300", "cua-profile-save-changes": false, @@ -314,15 +344,15 @@ describe("pi extension activation", () => { await getCommand(pi, "cua-tools").handler("", listingCtx); const listing = notices.at(-1) ?? ""; - expect(listing).toContain("* browser_snapshot"); + expect(listing).toContain("* playwright"); // The reason comes from the catalog compiler, not from a rule restated here. expect(listing).toMatch(/anthropic-computer — unavailable: .*requires a anthropic model/); // Listing is not a mutation: an empty argument must not clear the selection. - expect(pi.active).toContain("browser_snapshot"); + expect(pi.active).toContain("playwright_execute"); expect(pi.entries).toEqual([]); await getCommand(pi, "cua-tools").handler("none", listingCtx); - expect(pi.active).not.toContain("browser_snapshot"); + expect(pi.active).not.toContain("playwright_execute"); }); it("re-registers declarations when a new session changes coordinate mode", async () => { diff --git a/packages/pi-extension/test/selection.test.ts b/packages/pi-extension/test/selection.test.ts index 8d61722c..6c56493c 100644 --- a/packages/pi-extension/test/selection.test.ts +++ b/packages/pi-extension/test/selection.test.ts @@ -1,18 +1,9 @@ import { getCuaModel } from "@onkernel/cua-ai"; import { describe, expect, it } from "vitest"; -import { - BROWSER_BATCH_ACTIONS, - compileSpecs, - COMPUTER_BATCH_ACTIONS, - CUA_SELECTORS, - CUA_TOOL_NAMES, - expandSelection, - parseSelection, - selectorAvailability, -} from "../src/selection"; +import { compileSpecs, CUA_SELECTORS, expandSelection, parseSelection, selectorAvailability } from "../src/selection"; describe("CUA pi selectors", () => { - it("has stable exact browser and computer preset membership", () => { + it("has stable exact browser and computer entry membership, batch included", () => { expect(expandSelection(parseSelection("browser", "pixels")).map((tool) => tool.name)).toEqual([ "browser_snapshot", "browser_text", @@ -31,6 +22,7 @@ describe("CUA pi selectors", () => { "browser_screenshot", "browser_evaluate", "browser_wait_for", + "browser_batch", ]); expect(expandSelection(parseSelection("computer", "normalized-1000")).map((tool) => tool.name)).toEqual([ "computer_click", @@ -49,15 +41,29 @@ describe("CUA pi selectors", () => { "computer_forward", "computer_url", "computer_cursor_position", + "computer_batch", ]); }); - it("expands special selectors without native provider tools", () => { - expect( - expandSelection(parseSelection("browser-act,browser-batch,computer-batch,playwright", "pixels")).map((tool) => tool.name), - ).toEqual(["browser_act", "browser_batch", "computer_batch", "playwright_execute"]); - expect(BROWSER_BATCH_ACTIONS).toHaveLength(17); - expect(COMPUTER_BATCH_ACTIONS).toHaveLength(17); - expect(CUA_TOOL_NAMES).not.toContain("computer"); + it("offers exactly the eight menu entries", () => { + expect([...CUA_SELECTORS]).toEqual([ + "browser", + "computer", + "browser-act", + "playwright", + "anthropic-computer", + "anthropic-browser", + "openai-computer", + "google-browser", + ]); + // Packaging variants are gone: the batch tool ships inside its generic entry, + // and the 37 individual tool names are no longer selectable on their own. + for (const retired of ["mixed", "browser-batch", "computer-batch", "browser_snapshot", "computer_click"]) { + expect(() => parseSelection(retired, "pixels")).toThrow(/unknown CUA tool selector/); + } + expect(expandSelection(parseSelection("browser-act,playwright", "pixels")).map((tool) => tool.name)).toEqual([ + "browser_act", + "playwright_execute", + ]); }); it("compiles Anthropic native computer use only for supported Anthropic models", () => { const specs = expandSelection(parseSelection("anthropic-computer", "pixels")); @@ -106,6 +112,35 @@ describe("CUA pi selectors", () => { expect(() => parseSelection("browser,browser", "pixels")).toThrow("duplicate"); expect(() => parseSelection("native-openai", "pixels")).toThrow("unknown"); expect(() => parseSelection("browser", "screen")).toThrow("coordinates"); - expect(() => expandSelection(parseSelection("browser,browser_snapshot", "pixels"))).toThrow("duplicate tool identity"); + }); + + it("rejects Anthropic's two native surfaces together, before the API does", () => { + // Anthropic answers 400: the browser tool's viewport coordinate frame is + // incompatible with the computer tool's display frame. + const both = expandSelection(parseSelection("anthropic-computer,anthropic-browser", "pixels")); + expect(() => compileSpecs(getCuaModel("anthropic:claude-opus-5"), both)).toThrow(/cannot be selected alongside/); + }); + + it("reports that pairwise conflict as a conflict, not as unavailability", () => { + const byName = new Map( + selectorAvailability(getCuaModel("anthropic:claude-opus-5"), parseSelection(undefined, "pixels")).map((e) => [e.selector, e]), + ); + expect(byName.get("anthropic-computer")?.available).toBe(true); + expect(byName.get("anthropic-computer")?.conflictsWith).toContain("anthropic-browser"); + expect(byName.get("anthropic-browser")?.conflictsWith).toContain("anthropic-computer"); + expect(byName.get("playwright")?.conflictsWith).toEqual([]); + }); + + it("no longer marks every row unavailable when the current selection fails to compile", () => { + // The regression this replaces: a failing selection's error became the reason + // on every row, including rows that then activated fine. + const model = getCuaModel("anthropic:claude-opus-5"); + const failing = parseSelection("anthropic-computer,anthropic-browser", "pixels"); + expect(() => compileSpecs(model, expandSelection(failing))).toThrow(); + + const byName = new Map(selectorAvailability(model, failing).map((e) => [e.selector, e])); + expect(byName.get("playwright")?.available).toBe(true); + expect(byName.get("browser")?.available).toBe(true); + expect(byName.get("playwright")?.reason).toBeUndefined(); }); }); From 254b18055526d72022de67dabbb504327863abe5 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:00:43 +0000 Subject: [PATCH 4/7] Fix computer_call_output rejecting every request after a failed action Found by running OpenAI's native computer path end to end for the first time. A computer result with no screenshot put its failure text in an error key inside computer_call_output.output, which the Responses API refuses outright: 400 Unknown parameter: 'input[4].output.error' So a single failed action poisoned the rest of the conversation. The output now always carries a valid computer_screenshot and the failure text follows as a user message, so the model still learns what happened. A 1x1 placeholder is not enough: the Responses API rejects it even though the vision endpoint accepts one, hence 64x64. --- packages/ai/CHANGELOG.md | 19 +++++++++++ packages/ai/src/providers/openai/provider.ts | 29 ++++++++++++++-- .../ai/test/openai-native-provider.test.ts | 33 +++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 8510796c..7a7045c7 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,25 @@ ## Unreleased +- Fix OpenAI's native computer transport rejecting every request after a + screenshot-less action. A `computer_call_output` whose result carried no image + put the failure text in an `error` key, which the Responses API refuses outright + (`400 Unknown parameter: 'input[N].output.error'`), so one failed action poisoned + the rest of the conversation. The output now always carries a valid + `computer_screenshot`, and the failure text follows as a user message so the + model still learns what happened. A 1x1 placeholder is not enough — the + Responses API rejects it even though the vision endpoint accepts one. +- Anthropic's native browser and native computer tools can no longer be selected + together. Anthropic answers 400 because the browser tool addresses a viewport + coordinate frame and the computer tool a display frame; the catalog now refuses + the pair at compile time instead of on the wire. +- Google no longer carries a schema quirk. The Gemini API rejects the JSON Schema + keywords `const` and `additionalProperties` outright rather than ignoring them, + so a payload transform rewrites both for Google — `const: x` becomes a + single-value `enum`, which means the same thing. Gemini now accepts every + function tool CUA offers, including `browser_act` and `browser_wait_for`, which + the removed quirk had marked unavailable. + - Add `cuaToolMenu(model, selected)`: every tool CUA can offer for a model, each marked available or not with the compiler's own reason when it is not. It decides availability by compiling the candidate catalog rather than restating diff --git a/packages/ai/src/providers/openai/provider.ts b/packages/ai/src/providers/openai/provider.ts index 96a2ab0c..4221b12a 100644 --- a/packages/ai/src/providers/openai/provider.ts +++ b/packages/ai/src/providers/openai/provider.ts @@ -28,6 +28,13 @@ import type { CuaSimpleStreamOptions } from "../common"; /** CUA-owned api id for OpenAI's native computer tool, derived onto the model by compileCuaToolCatalog when that tool is selected. */ export const OPENAI_CUA_COMPUTER_API = "openai-cua-computer"; +/** + * 64x64 black PNG, used when a computer action produced no screenshot so the + * `computer_screenshot` output stays valid. A 1x1 image is rejected by the + * Responses API even though the vision endpoint accepts it. + */ +const BLANK_SCREENSHOT_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAIklEQVR4nO3BAQ0AAADCoPdPbQ8HFAAAAAAAAAAAAAAA8G4wQAABiwCo9wAAAABJRU5ErkJggg=="; + export interface OpenAIResponsesOptions extends PiOpenAIResponsesOptions { /** @internal Identity-addressed native dispatch compiled from selected tools. */ cuaIncomingToolPlan?: CuaIncomingToolPlan; @@ -425,13 +432,29 @@ function convertMessages(messages: readonly Context["messages"][number][], nativ input.push({ type: "computer_call_output", call_id: message.toolCallId, - output: image - ? { type: "computer_screenshot", image_url: `data:${image.mimeType};base64,${image.data}` } - : { type: "computer_screenshot", error: message.isError ? text || "tool execution failed" : text || "no screenshot" }, + // `computer_screenshot` accepts image_url or file_id and nothing else. + // An earlier version put the failure text in an `error` key here, which + // the Responses API rejects outright — `400 Unknown parameter: + // 'input[N].output.error'` — so one screenshot-less action poisoned every + // later request in the conversation. A blank screenshot keeps the item + // valid; the text follows as a user message so the model still learns + // what happened. + output: { + type: "computer_screenshot", + image_url: image ? `data:${image.mimeType};base64,${image.data}` : BLANK_SCREENSHOT_DATA_URL, + }, ...(acknowledgedSafetyChecks(messages, message.toolCallId).length ? { acknowledged_safety_checks: acknowledgedSafetyChecks(messages, message.toolCallId) } : {}), }); + if (!image) { + const detail = text || (message.isError ? "tool execution failed" : "no screenshot was captured"); + input.push({ + type: "message", + role: "user", + content: [{ type: "input_text", text: `[computer action produced no screenshot] ${detail}` }], + }); + } } else { input.push({ type: "function_call_output", call_id: message.toolCallId, output: message.isError ? `Error: ${text}` : text || "ok" }); } diff --git a/packages/ai/test/openai-native-provider.test.ts b/packages/ai/test/openai-native-provider.test.ts index 9a61d37c..9a8f49f7 100644 --- a/packages/ai/test/openai-native-provider.test.ts +++ b/packages/ai/test/openai-native-provider.test.ts @@ -170,4 +170,37 @@ describe("OpenAI native computer Responses adapter", () => { expect(payload.store).toBe(false); expect(payload.previous_response_id).toBeUndefined(); }); + + +}); +describe("computer_call_output serialization", () => { + async function sendWithResult(content: Array<{ type: string; [k: string]: unknown }>, isError: boolean) { + responsesCreate.mockReturnValueOnce({ id: "resp_ser", usage: {}, output: [] }); + await openai.streamOpenAICuaComputer(nativeModel, { + messages: [ + { role: "assistant", content: [{ type: "toolCall", id: "c1", name: "computer", arguments: {} }], stopReason: "toolUse" }, + { role: "toolResult", toolCallId: "c1", toolName: "computer", content, isError, timestamp: 1 }, + ] as never, + tools: [{ name: "computer", description: "placeholder", parameters: { type: "object" } as never }], + }, { apiKey: "test", cuaIncomingToolPlan: incoming }).result(); + return JSON.stringify(responsesCreate.mock.calls.at(-1)?.[0]); + } + + it("never emits an error key, and always carries a valid screenshot", async () => { + // Verified live: putting the failure text in an `error` key here makes the + // Responses API answer 400 `Unknown parameter: 'input[N].output.error'`, which + // poisoned every later request in the conversation. + const sent = await sendWithResult([{ type: "text", text: "click failed" }], true); + expect(sent).not.toContain('\\"error\\"'); + expect(sent).toContain("computer_screenshot"); + expect(sent).toContain("image_url"); + // The text still reaches the model, as a message rather than an invalid field. + expect(sent).toContain("computer action produced no screenshot"); + }); + + it("uses the real screenshot when the result carries one", async () => { + const sent = await sendWithResult([{ type: "image", data: "aW1n", mimeType: "image/png" }], false); + expect(sent).toContain("data:image/png;base64,aW1n"); + expect(sent).not.toContain("produced no screenshot"); + }); }); From e1d065c4b6416cba63366138e68f8f6b345b0a9c Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:54:20 +0000 Subject: [PATCH 5/7] Fail browser_act fast when a step never dispatched An unresolvable ref throws before any input is sent, but the step's expect was still awaited afterwards, so a model that invented a ref burned a full global timeout per attempt -- 5 calls and ~5 minutes where the primitives took 2 calls and 15 seconds. Scoped to actions that provably never dispatched. Two existing tests caught a broader first attempt: when delivery is merely uncertain, a lost acknowledgement may mean the input landed, and the expectation is how that gets discovered, so those still wait. --- packages/agent/CHANGELOG.md | 8 ++ packages/agent/src/translator/browser-act.ts | 19 +++- .../agent/test/browser-act-fail-fast.test.ts | 88 +++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 packages/agent/test/browser-act-fail-fast.test.ts diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 34b8c225..ad16c599 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +- `browser_act` no longer spends a plan's whole deadline waiting for the effect of + a step whose action failed. An unresolvable ref throws immediately, but the + step's `expect` was still awaited afterwards, so a model that invented a ref + burned a full global timeout per attempt instead of being told to snapshot + first. A step whose action never dispatched now skips its own expectation and + stops the plan. Uncertain *delivery* still waits, because a lost acknowledgement + may mean the input landed and the expectation is how that is discovered. + Breaking: `CuaAgent` and `CuaAgentHarness` are removed. cua-agent hands back plain pi objects; the caller constructs the agent. diff --git a/packages/agent/src/translator/browser-act.ts b/packages/agent/src/translator/browser-act.ts index 64edacd9..9c6d4685 100644 --- a/packages/agent/src/translator/browser-act.ts +++ b/packages/agent/src/translator/browser-act.ts @@ -95,7 +95,14 @@ export async function runBrowserAct(action: CuaActionBrowserAct, runtime: Browse let waitResult: BrowserWaitForResult | undefined; let after: BrowserObservation | undefined; let afterTargets: string[] | undefined; - if (!timeout) { + // A step's `expect` verifies the effect of that step's action, so it is still + // worth awaiting when delivery is merely *uncertain* — a lost acknowledgement + // may mean the input landed anyway, and the expectation is how that is + // discovered. A stale or unresolvable ref is different: it throws before + // anything is dispatched, so there is no effect that could arrive, and waiting + // spends the whole plan deadline on an outcome that cannot happen. + const undispatched = isStale(actionError); + if (!timeout && !undispatched) { try { if (step.expect) { waitResult = await beforeDeadline( @@ -113,10 +120,18 @@ export async function runBrowserAct(action: CuaActionBrowserAct, runtime: Browse timeout = timeoutReason(error); diagnostics.push(timeout ? message(error) : `post-action observation failed: ${message(error)}`); } + } else if (!timeout && undispatched) { + try { + after = await beforeDeadline(() => runtime.observe(action.tab_id), deadline); + afterTargets = await beforeDeadline(() => runtime.targetIds(), deadline); + } catch (error) { + timeout = timeoutReason(error); + diagnostics.push(timeout ? message(error) : `post-action observation failed: ${message(error)}`); + } } timedOut ||= timeout !== undefined; - const stale = isStale(actionError) || waitResult?.reason === "stale_ref" || expectation?.status === "unverifiable" && expectation.reason === "stale_ref"; + const stale = undispatched || waitResult?.reason === "stale_ref" || expectation?.status === "unverifiable" && expectation.reason === "stale_ref"; const outcome = stepOutcome(expectation, actionError, stale); if (expectation) diagnostics.push(`expectation ${expectation.status}`); steps.push(stepResult(index, step, outcome, diagnostics, expectation)); diff --git a/packages/agent/test/browser-act-fail-fast.test.ts b/packages/agent/test/browser-act-fail-fast.test.ts new file mode 100644 index 00000000..bff55f8e --- /dev/null +++ b/packages/agent/test/browser-act-fail-fast.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "vitest"; +import { runBrowserAct, type BrowserActRuntime } from "../src/translator/browser-act"; + +// `boundary()` compares the observation's navigation epoch and per-frame +// generations against what the runtime reports live, so a fixture has to agree +// with the stub's liveNavigationEpoch/liveGeneration or every step reports a +// spurious navigation. +const observation = { + targetId: "target-1", + url: "https://example.com/", + title: "Example", + navigationEpoch: 1, + generations: new Map([["frame-1", 1]]), + incompleteFrames: [], + frames: [], + nodes: [], +} as unknown as Awaited>; + +/** Records what the plan asked for, and makes any wait take the whole deadline. */ +function stubRuntime(overrides: Partial = {}) { + const calls = { waits: 0, steps: 0, observes: 0 }; + const runtime: BrowserActRuntime = { + observe: async () => { + calls.observes += 1; + return observation; + }, + targetIds: async () => ["target-1"], + dialogCount: () => 0, + liveGeneration: () => 1, + liveNavigationEpoch: () => 1, + executeStep: async () => { + calls.steps += 1; + }, + wait: async (_expect, _baseline, _targetId, _tabId, timeoutMs) => { + calls.waits += 1; + // Stand in for a condition that can never become true: consume the budget. + await new Promise((resolve) => setTimeout(resolve, Math.min(timeoutMs ?? 0, 300))); + return { status: "timed_out", reason: "timeout", details: [], initial: { truth: false, details: [] } } as never; + }, + evaluate: () => ({ truth: undefined, details: [] }) as never, + present: () => ({}) as never, + render: () => "", + ...overrides, + }; + return { runtime, calls }; +} + +describe("browser_act failure handling", () => { + it("does not wait for the effect of an action that failed", async () => { + // The reported symptom: the model invents a ref it never snapshotted, the click + // throws immediately, and the plan then spends its whole deadline waiting for + // the effect of an action that never happened. + const { runtime, calls } = stubRuntime({ + executeStep: async () => { + throw new Error('ref "e3" is stale'); + }, + }); + const started = Date.now(); + const result = await runBrowserAct( + { + type: "act", + steps: [{ type: "click", ref: "e3", expect: { type: "text", text: "never appears" } }], + timeout_ms: 5000, + } as never, + runtime, + ); + + expect(calls.waits).toBe(0); + expect(result.stop_reason).toBe("stale_ref"); + expect(Date.now() - started).toBeLessThan(2000); + }); + + it("still waits for the effect of an action that succeeded", async () => { + const { runtime, calls } = stubRuntime(); + const result = await runBrowserAct( + { + type: "act", + steps: [{ type: "click", ref: "e1", expect: { type: "text", text: "never appears" } }], + timeout_ms: 800, + } as never, + runtime, + ); + + expect(calls.steps).toBe(1); + expect(calls.waits).toBe(1); + expect(result.stop_reason).toBeDefined(); + }); +}); From e451e5f809f7af2f6d3d42db508e0d32ac545817 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:57:03 +0000 Subject: [PATCH 6/7] Address BugBot: flat Google tools, resume with retired selectors, vacuous assertion The Gemini schema narrowing only walked tools[].functionDeclarations, but the Interactions transport serializes function tools flat as { type: "function", parameters }. Selecting a native surface alongside a function tool derives that transport, so const and additionalProperties still reached the API as a 400 -- after the menu said the selection was fine. It now narrows both shapes. A session persisted before the menu shrank can name a selector that no longer exists, and restore ran it straight through parseSelection, so the session threw instead of starting. Retired selectors are now dropped with a stderr note. The new computer_call_output guard searched for a backslash-escaped "error", which JSON.stringify never emits, so reintroducing the invalid field would have passed. That assertion was worthless; it now fails against the old shape. --- packages/ai/src/tool-catalog.ts | 17 ++++++--- .../ai/test/openai-native-provider.test.ts | 2 +- packages/ai/test/tool-catalog.test.ts | 16 ++++++++ packages/pi-extension/src/index.ts | 14 ++++++- packages/pi-extension/test/extension.test.ts | 38 +++++++++++++++++++ 5 files changed, 80 insertions(+), 7 deletions(-) diff --git a/packages/ai/src/tool-catalog.ts b/packages/ai/src/tool-catalog.ts index 4fa70dec..60b7e1d2 100644 --- a/packages/ai/src/tool-catalog.ts +++ b/packages/ai/src/tool-catalog.ts @@ -578,13 +578,20 @@ function createGeminiSchemaTransform(): CuaPayloadTransform { phase: "tool-declarations", apply(payload) { if (!isRecord(payload) || !Array.isArray(payload.tools)) return payload; + // Google serializes function tools two ways: the Generative Language API + // nests them under `functionDeclarations`, while the Interactions transport + // emits flat `{ type: "function", parameters }` entries. Narrow both, because + // selecting a native surface alongside a function tool derives the second + // shape and a shape-specific transform would silently skip it. return { ...payload, - tools: payload.tools.map((tool) => - isRecord(tool) && Array.isArray(tool.functionDeclarations) - ? { ...tool, functionDeclarations: tool.functionDeclarations.map(narrowToGeminiSchema) } - : tool, - ), + tools: payload.tools.map((tool) => { + if (!isRecord(tool)) return tool; + if (Array.isArray(tool.functionDeclarations)) { + return { ...tool, functionDeclarations: tool.functionDeclarations.map(narrowToGeminiSchema) }; + } + return "parameters" in tool ? { ...tool, parameters: narrowToGeminiSchema(tool.parameters) } : tool; + }), }; }, }; diff --git a/packages/ai/test/openai-native-provider.test.ts b/packages/ai/test/openai-native-provider.test.ts index 9a8f49f7..2371af1d 100644 --- a/packages/ai/test/openai-native-provider.test.ts +++ b/packages/ai/test/openai-native-provider.test.ts @@ -191,7 +191,7 @@ describe("computer_call_output serialization", () => { // Responses API answer 400 `Unknown parameter: 'input[N].output.error'`, which // poisoned every later request in the conversation. const sent = await sendWithResult([{ type: "text", text: "click failed" }], true); - expect(sent).not.toContain('\\"error\\"'); + expect(sent).not.toContain('"error"'); expect(sent).toContain("computer_screenshot"); expect(sent).toContain("image_url"); // The text still reaches the model, as a message rather than an invalid field. diff --git a/packages/ai/test/tool-catalog.test.ts b/packages/ai/test/tool-catalog.test.ts index bc70d81f..d4e380c6 100644 --- a/packages/ai/test/tool-catalog.test.ts +++ b/packages/ai/test/tool-catalog.test.ts @@ -409,6 +409,22 @@ describe("Gemini function-declaration schema", () => { expect(sent).toContain('"enum":["text"]'); }); + it("narrows the flat tool shape the Interactions transport emits", async () => { + // Selecting a native surface alongside a function tool derives the Interactions + // transport, which serializes tools flat instead of under functionDeclarations. + const catalog = compileCuaToolCatalog({ + model: getCuaModel("google:gemini-3.6-flash"), + requestedTools: [...cua.providers.google.toolsets.browser(), cua.tools.browser.waitFor()], + }); + const flat = { + tools: [{ type: "function", name: "browser_wait_for", parameters: cua.tools.browser.waitFor().declaration.parameters }], + }; + const sent = JSON.stringify(await catalog.payload.apply(flat, catalog.model)); + expect(JSON.stringify(flat)).toContain('"const"'); + expect(sent).not.toContain('"const"'); + expect(sent).not.toContain('"additionalProperties"'); + }); + it("leaves other providers' declarations untouched", async () => { const catalog = compileCuaToolCatalog({ model: getCuaModel("openai:gpt-5.6-sol"), diff --git a/packages/pi-extension/src/index.ts b/packages/pi-extension/src/index.ts index 61efe77a..f3da31fb 100644 --- a/packages/pi-extension/src/index.ts +++ b/packages/pi-extension/src/index.ts @@ -13,6 +13,7 @@ import { import { allSelectableSpecs, compileSpecs, + CUA_SELECTORS, expandSelection, parseSelection, selectorAvailability, @@ -218,7 +219,18 @@ export default function cuaPiExtension(pi: ExtensionAPI): void { selection = flags.selection; browserOptions = flags.browserOptions; const saved = restoreConfig(ctx.sessionManager.getBranch()); - if (saved) selection = parseSelection(saved.selectors.join(","), saved.coordinates); + if (saved) { + // A session persisted before the menu shrank can name a selector that no + // longer exists. Restoring must not throw: drop what is gone, keep the rest, + // and say so — a resumed session that refuses to start is worse than one + // that starts with fewer tools. + const known = saved.selectors.filter((selector) => CUA_SELECTORS.includes(selector)); + const dropped = saved.selectors.filter((selector) => !CUA_SELECTORS.includes(selector)); + if (dropped.length) { + process.stderr.write(`cua: ignoring retired tool selector(s) from this session: ${dropped.join(", ")}\n`); + } + if (known.length) selection = parseSelection(known.join(","), saved.coordinates); + } configureDeclarations(); installTools(); initialized = false; diff --git a/packages/pi-extension/test/extension.test.ts b/packages/pi-extension/test/extension.test.ts index 1a57db97..6ee4791a 100644 --- a/packages/pi-extension/test/extension.test.ts +++ b/packages/pi-extension/test/extension.test.ts @@ -295,6 +295,44 @@ describe("pi extension activation", () => { expect(pi.active).toContain("playwright_execute"); }); + it("resumes a session whose persisted selection names a retired selector", async () => { + const written: string[] = []; + const write = vi.spyOn(process.stderr, "write").mockImplementation(((chunk: string) => { + written.push(String(chunk)); + return true; + }) as never); + try { + const pi = makePi({ + "cua-tools": "playwright", + "cua-coordinates": "pixels", + "cua-browser-timeout": "300", + "cua-profile-save-changes": false, + }); + extension(pi.api); + // `browser-batch` was a selector before the menu shrank to eight entries. A + // session persisted then must still start, not throw during restore. + const resumedCtx = { + ...ctx, + sessionManager: { + getBranch: () => [ + { + type: "custom", + customType: "cua-pi-config-v1", + data: { version: 1, origin: "command", selectors: ["browser-batch", "computer"], coordinates: "pixels" }, + }, + ], + }, + } as unknown as ExtensionContext; + + await getHandler(pi, "session_start")({}, resumedCtx); + + expect(pi.active).toContain("computer_click"); + expect(written.join("")).toMatch(/ignoring retired tool selector\(s\).*browser-batch/); + } finally { + write.mockRestore(); + } + }); + it("warns on stderr when a selection deactivates outside TUI mode", async () => { const written: string[] = []; const write = vi.spyOn(process.stderr, "write").mockImplementation(((chunk: string) => { From ee413ab889cb6d2ee5747314e9b8de790124a7b7 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:09:34 +0000 Subject: [PATCH 7/7] Apply a restored selection even when nothing survives A persisted selection comes from /cua-tools, which deliberately overrides the flags. Falling back to the flags when every persisted selector had been retired re-enabled tools that session had explicitly replaced, and made the stderr note misleading: it said retired selectors were ignored while the whole selection had been. --- packages/pi-extension/src/index.ts | 6 +++- packages/pi-extension/test/extension.test.ts | 38 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/pi-extension/src/index.ts b/packages/pi-extension/src/index.ts index f3da31fb..f758076e 100644 --- a/packages/pi-extension/src/index.ts +++ b/packages/pi-extension/src/index.ts @@ -229,7 +229,11 @@ export default function cuaPiExtension(pi: ExtensionAPI): void { if (dropped.length) { process.stderr.write(`cua: ignoring retired tool selector(s) from this session: ${dropped.join(", ")}\n`); } - if (known.length) selection = parseSelection(known.join(","), saved.coordinates); + // Always apply what was restored, even when nothing survives. A persisted + // selection came from `/cua-tools`, which deliberately overrides the flags, + // so falling back to them would re-enable tools this session had replaced. + // An empty selection with the note above is the honest outcome. + selection = parseSelection(known.join(",") || undefined, saved.coordinates); } configureDeclarations(); installTools(); diff --git a/packages/pi-extension/test/extension.test.ts b/packages/pi-extension/test/extension.test.ts index 6ee4791a..35d7957f 100644 --- a/packages/pi-extension/test/extension.test.ts +++ b/packages/pi-extension/test/extension.test.ts @@ -333,6 +333,44 @@ describe("pi extension activation", () => { } }); + it("does not fall back to flags when every persisted selector is retired", async () => { + const written: string[] = []; + const write = vi.spyOn(process.stderr, "write").mockImplementation(((chunk: string) => { + written.push(String(chunk)); + return true; + }) as never); + try { + const pi = makePi({ + "cua-tools": "playwright", + "cua-coordinates": "pixels", + "cua-browser-timeout": "300", + "cua-profile-save-changes": false, + }); + extension(pi.api); + const resumedCtx = { + ...ctx, + sessionManager: { + getBranch: () => [ + { + type: "custom", + customType: "cua-pi-config-v1", + data: { version: 1, origin: "command", selectors: ["browser-batch"], coordinates: "pixels" }, + }, + ], + }, + } as unknown as ExtensionContext; + + await getHandler(pi, "session_start")({}, resumedCtx); + + // The persisted selection came from /cua-tools, which overrides the flags. + // Reviving `playwright` here would re-enable a tool this session replaced. + expect(pi.active).not.toContain("playwright_execute"); + expect(written.join("")).toMatch(/ignoring retired tool selector\(s\).*browser-batch/); + } finally { + write.mockRestore(); + } + }); + it("warns on stderr when a selection deactivates outside TUI mode", async () => { const written: string[] = []; const write = vi.spyOn(process.stderr, "write").mockImplementation(((chunk: string) => {