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..19c82e9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: - name: Pi extension unit tests run: npm test --workspace @onkernel/cua-pi-extension - cli-unit: + typecheck-and-ptywright: runs-on: ubuntu-latest timeout-minutes: 15 env: @@ -99,36 +99,14 @@ jobs: 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 - - 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 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..4a7f3b02 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": { @@ -25,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" @@ -45,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", @@ -60,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", @@ -74,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" @@ -83,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", @@ -94,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", @@ -119,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", @@ -138,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", @@ -154,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", @@ -172,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", @@ -186,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", @@ -210,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", @@ -227,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", @@ -249,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", @@ -265,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", @@ -283,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", @@ -300,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", @@ -317,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", @@ -332,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", @@ -347,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", @@ -365,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", @@ -384,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", @@ -398,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", @@ -413,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", @@ -430,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", @@ -443,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" @@ -455,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", @@ -468,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" @@ -477,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": { @@ -495,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": { @@ -505,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": { @@ -515,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": { @@ -531,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" @@ -540,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": { @@ -554,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", @@ -570,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", @@ -595,9 +522,7 @@ }, "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", "dependencies": { "@earendil-works/pi-agent-core": "^0.83.0", @@ -631,8 +556,7 @@ }, "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", "dependencies": { "json-schema-to-ts": "^3.1.1" @@ -651,8 +575,7 @@ }, "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", "dependencies": { "@aws-crypto/util": "^5.2.0", @@ -665,8 +588,7 @@ }, "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", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", @@ -680,8 +602,7 @@ }, "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", "dependencies": { "@aws-crypto/util": "^5.2.0", @@ -694,8 +615,7 @@ }, "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", "dependencies": { "tslib": "^2.6.2" @@ -703,8 +623,7 @@ }, "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", "dependencies": { "@aws-sdk/types": "^3.222.0", @@ -714,8 +633,7 @@ }, "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", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -739,8 +657,7 @@ }, "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", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -758,8 +675,7 @@ }, "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", "dependencies": { "@aws-sdk/core": "^3.974.11", @@ -774,8 +690,7 @@ }, "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", "dependencies": { "@aws-sdk/core": "^3.974.11", @@ -792,8 +707,7 @@ }, "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", "dependencies": { "@aws-sdk/core": "^3.974.11", @@ -816,8 +730,7 @@ }, "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", "dependencies": { "@aws-sdk/core": "^3.974.11", @@ -833,8 +746,7 @@ }, "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", "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.37", @@ -855,8 +767,7 @@ }, "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", "dependencies": { "@aws-sdk/core": "^3.974.11", @@ -871,8 +782,7 @@ }, "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", "dependencies": { "@aws-sdk/core": "^3.974.11", @@ -889,8 +799,7 @@ }, "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", "dependencies": { "@aws-sdk/core": "^3.974.11", @@ -906,8 +815,7 @@ }, "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", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -921,8 +829,7 @@ }, "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", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -936,8 +843,7 @@ }, "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", "dependencies": { "@aws-sdk/core": "^3.974.11", @@ -954,8 +860,7 @@ }, "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", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -975,8 +880,7 @@ }, "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", "dependencies": { "@aws-sdk/types": "^3.973.8", @@ -991,8 +895,7 @@ }, "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", "dependencies": { "@aws-sdk/core": "^3.974.11", @@ -1008,8 +911,7 @@ }, "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", "dependencies": { "@smithy/types": "^4.14.1", @@ -1021,8 +923,7 @@ }, "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", "dependencies": { "tslib": "^2.6.2" @@ -1033,8 +934,7 @@ }, "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", "dependencies": { "@nodable/entities": "2.1.0", @@ -1048,8 +948,7 @@ }, "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", "engines": { "node": ">=18.0.0" @@ -1057,8 +956,7 @@ }, "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", "engines": { "node": ">=6.9.0" @@ -1066,7 +964,7 @@ }, "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", "dependencies": { "@earendil-works/pi-ai": "^0.83.0", @@ -1081,7 +979,7 @@ }, "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", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -1105,7 +1003,7 @@ }, "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", "dependencies": { "get-east-asian-width": "1.6.0", @@ -1117,8 +1015,7 @@ }, "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", "dependencies": { @@ -1141,8 +1038,7 @@ }, "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, "engines": { @@ -1161,106 +1057,12 @@ "@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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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": [ @@ -1272,11 +1074,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": [ @@ -1286,42 +1087,9 @@ "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" - ], - "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" - ], - "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", "dependencies": { "@opentelemetry/semantic-conventions": "^1.40.0", @@ -1340,8 +1108,7 @@ }, "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", @@ -1352,8 +1119,7 @@ }, "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", "engines": { "node": ">=8.0.0" @@ -1361,8 +1127,7 @@ }, "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", "engines": { "node": ">=14" @@ -1370,32 +1135,27 @@ }, "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==", + "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==", + "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==", + "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==", + "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", "dependencies": { "@protobufjs/aspromise": "^1.1.1" @@ -1403,38 +1163,32 @@ }, "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==", + "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==", + "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==", + "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==", + "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==", + "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", "dependencies": { "@aws-crypto/crc32": "5.2.0", @@ -1447,8 +1201,7 @@ }, "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", "dependencies": { "@smithy/core": "^3.24.3", @@ -1461,8 +1214,7 @@ }, "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", "dependencies": { "@smithy/core": "^3.24.3", @@ -1475,8 +1227,7 @@ }, "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", "dependencies": { "tslib": "^2.6.2" @@ -1487,8 +1238,7 @@ }, "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", "dependencies": { "@smithy/core": "^3.24.3", @@ -1501,8 +1251,7 @@ }, "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", "dependencies": { "@smithy/core": "^3.24.3", @@ -1515,8 +1264,7 @@ }, "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", "dependencies": { "tslib": "^2.6.2" @@ -1527,8 +1275,7 @@ }, "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", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", @@ -1540,8 +1287,7 @@ }, "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", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", @@ -1553,8 +1299,7 @@ }, "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", "dependencies": { "undici-types": "~6.21.0" @@ -1562,8 +1307,7 @@ }, "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", "engines": { "node": ">= 14" @@ -1571,8 +1315,7 @@ }, "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", "engines": { "node": "18 || 20 || >=22" @@ -1580,8 +1323,7 @@ }, "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", @@ -1600,8 +1342,7 @@ }, "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", "engines": { "node": "*" @@ -1609,14 +1350,12 @@ }, "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==", + "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", "dependencies": { "balanced-match": "^4.0.2" @@ -1627,14 +1366,12 @@ }, "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==", + "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", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -1645,8 +1382,7 @@ }, "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", "dependencies": { "path-key": "^3.1.0", @@ -1659,8 +1395,7 @@ }, "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", "engines": { "node": ">= 12" @@ -1668,8 +1403,7 @@ }, "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", "dependencies": { "ms": "^2.1.3" @@ -1685,8 +1419,7 @@ }, "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", "engines": { "node": ">=0.3.1" @@ -1694,8 +1427,7 @@ }, "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", "dependencies": { "safe-buffer": "^5.0.1" @@ -1703,14 +1435,12 @@ }, "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==", + "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", @@ -1725,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", @@ -1746,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", @@ -1769,8 +1497,7 @@ }, "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", "dependencies": { "fetch-blob": "^3.1.2" @@ -1781,8 +1508,7 @@ }, "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", "dependencies": { "extend": "^3.0.2", @@ -1795,8 +1521,7 @@ }, "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", "dependencies": { "gaxios": "^7.0.0", @@ -1809,8 +1534,7 @@ }, "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", "engines": { "node": ">=18" @@ -1821,8 +1545,7 @@ }, "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", "dependencies": { "minimatch": "^10.2.2", @@ -1838,8 +1561,7 @@ }, "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", "dependencies": { "base64-js": "^1.3.0", @@ -1855,8 +1577,7 @@ }, "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", "engines": { "node": ">=14" @@ -1864,14 +1585,12 @@ }, "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==", + "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", "engines": { "node": "*" @@ -1879,8 +1598,7 @@ }, "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", "dependencies": { "lru-cache": "^11.1.0" @@ -1891,8 +1609,7 @@ }, "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", "dependencies": { "agent-base": "^7.1.0", @@ -1904,8 +1621,7 @@ }, "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", "dependencies": { "agent-base": "^7.1.2", @@ -1917,8 +1633,7 @@ }, "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", "engines": { "node": ">= 4" @@ -1926,14 +1641,12 @@ }, "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==", + "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", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -1941,8 +1654,7 @@ }, "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", "dependencies": { "bignumber.js": "^9.0.0" @@ -1950,8 +1662,7 @@ }, "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", "dependencies": { "@babel/runtime": "^7.18.3", @@ -1963,8 +1674,7 @@ }, "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", "dependencies": { "buffer-equal-constant-time": "^1.0.1", @@ -1974,8 +1684,7 @@ }, "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", "dependencies": { "jwa": "^2.0.1", @@ -1984,14 +1693,12 @@ }, "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==", + "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", "engines": { "node": "20 || >=22" @@ -1999,8 +1706,7 @@ }, "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", "bin": { "marked": "bin/marked.js" @@ -2011,8 +1717,7 @@ }, "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", "dependencies": { "brace-expansion": "^5.0.5" @@ -2026,8 +1731,7 @@ }, "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", "engines": { "node": ">=16 || 14 >=14.17" @@ -2035,15 +1739,12 @@ }, "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==", + "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", @@ -2061,8 +1762,7 @@ }, "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", "dependencies": { "data-uri-to-buffer": "^4.0.0", @@ -2079,8 +1779,7 @@ }, "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", "bin": { "openai": "bin/cli" @@ -2100,8 +1799,7 @@ }, "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", "dependencies": { "@types/retry": "0.12.0", @@ -2113,20 +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==", + "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==", + "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", @@ -2140,8 +1835,7 @@ }, "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", "engines": { "node": ">=8" @@ -2149,8 +1843,7 @@ }, "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", "dependencies": { "lru-cache": "^11.0.0", @@ -2165,8 +1858,7 @@ }, "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", "dependencies": { "graceful-fs": "^4.2.4", @@ -2176,8 +1868,7 @@ }, "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", "engines": { "node": ">= 4" @@ -2185,8 +1876,7 @@ }, "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", "dependencies": { @@ -2208,8 +1898,7 @@ }, "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", "engines": { "node": ">= 4" @@ -2217,8 +1906,7 @@ }, "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", @@ -2237,8 +1925,7 @@ }, "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", "bin": { "semver": "bin/semver.js" @@ -2249,8 +1936,7 @@ }, "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", "dependencies": { "shebang-regex": "^3.0.0" @@ -2261,8 +1947,7 @@ }, "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", "engines": { "node": ">=8" @@ -2270,14 +1955,12 @@ }, "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==", + "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", @@ -2288,26 +1971,22 @@ }, "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==", + "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==", + "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==", + "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", "engines": { "node": ">=22.19.0" @@ -2315,14 +1994,12 @@ }, "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==", + "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", "engines": { "node": ">= 8" @@ -2330,8 +2007,7 @@ }, "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", "dependencies": { "isexe": "^2.0.0" @@ -2345,8 +2021,7 @@ }, "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", "engines": { "node": ">=10.0.0" @@ -2366,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", @@ -2381,8 +2055,7 @@ }, "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", "bin": { "yaml": "bin.mjs" @@ -2396,8 +2069,7 @@ }, "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", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -2405,1742 +2077,267 @@ }, "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", "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", - "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" - ], - "engines": { - "node": ">=18" - } - }, - "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, - "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==", - "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", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "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", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "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", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "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/@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/@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/@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/@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/@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/@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==", - "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_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-cli": { - "resolved": "packages/cli", - "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" - ], + "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": "^20.19.0 || >=22.12.0" + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { + "node_modules/@img/colour": { "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": ">=18" } }, - "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==", + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", "cpu": [ - "ppc64" + "x64" ], - "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "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==", + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", "cpu": [ - "s390x" + "x64" ], - "dev": true, - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "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==", + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" } }, - "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==", + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" } }, - "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" - ], + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "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" - ], + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", "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": ">=6.0.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==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } + "license": "MIT" }, - "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" - ], + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "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/@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/@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/@onkernel/cua-agent": { + "resolved": "packages/agent", + "link": true }, - "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/@onkernel/cua-ai": { + "resolved": "packages/ai", + "link": true }, - "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/@onkernel/cua-pi-extension": { + "resolved": "packages/pi-extension", + "link": true }, - "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/@onkernel/ptywright": { + "resolved": "packages/ptywright", + "link": true }, - "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/@onkernel/sdk": { + "version": "0.49.0", + "license": "Apache-2.0" }, - "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/@opentelemetry/api": { + "version": "1.9.0", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } }, - "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" ], @@ -4149,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" ], @@ -4176,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" ], @@ -4232,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" ], @@ -4246,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", @@ -4264,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", @@ -4278,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", @@ -4292,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" @@ -4304,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", @@ -4318,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", @@ -4332,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" @@ -4344,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", @@ -4357,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", @@ -4368,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": { @@ -4392,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" }, @@ -4420,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": { @@ -4443,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": { @@ -4470,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": { @@ -4483,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": { @@ -4498,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": { @@ -4513,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": { @@ -4526,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": { @@ -4541,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" @@ -4550,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": { @@ -4560,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": { @@ -4570,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": { @@ -4588,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", @@ -4608,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": "*" @@ -4617,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": { @@ -4627,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": { @@ -4649,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": { @@ -4666,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": { @@ -4676,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" @@ -4700,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": { @@ -4710,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" @@ -4726,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" @@ -4735,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": { @@ -4756,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" @@ -4765,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": { @@ -4775,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", @@ -4824,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": { @@ -4834,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": { @@ -4844,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": { @@ -4868,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", @@ -4891,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" @@ -4901,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", @@ -4932,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", @@ -4944,22 +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", - "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", @@ -4975,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" @@ -4984,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", @@ -5004,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", @@ -5024,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": { @@ -5037,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": { @@ -5057,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" @@ -5066,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", @@ -5079,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", @@ -5090,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", @@ -5100,47 +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", - "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": [ { @@ -5158,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" @@ -5167,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", @@ -5187,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", @@ -5205,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": { @@ -5215,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", @@ -5254,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", @@ -5267,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": { @@ -5290,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": { @@ -5310,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": [ { @@ -5339,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": { @@ -5362,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": [ { @@ -5379,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": { @@ -5389,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" @@ -5398,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": { @@ -5432,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": { @@ -5476,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": { @@ -5492,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": { @@ -5537,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", @@ -5557,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" @@ -5569,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", @@ -5618,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": { @@ -5635,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" }, @@ -5647,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": { @@ -5660,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": { @@ -5691,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": { @@ -5701,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": { @@ -5711,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": { @@ -5721,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": { @@ -5731,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": { @@ -5810,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": { @@ -5820,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": { @@ -5834,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": { @@ -5853,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": { @@ -5871,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": { @@ -5889,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": { @@ -5964,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": { @@ -5987,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": { @@ -6060,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" @@ -6069,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": { @@ -6086,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" @@ -6107,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" @@ -6129,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" @@ -6171,30 +4032,6 @@ "node": ">=22.19.0" } }, - "packages/cli": { - "name": "@onkernel/cua-cli", - "version": "0.9.0", - "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", @@ -6205,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": { @@ -6213,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/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/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/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/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/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/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(); + }); +}); 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/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/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/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/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/src/tool-catalog.ts b/packages/ai/src/tool-catalog.ts index 9a1bd476..60b7e1d2 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}`; @@ -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,60 @@ 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; + // 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) => { + 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; + }), + }; + }, + }; +} + +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/openai-native-provider.test.ts b/packages/ai/test/openai-native-provider.test.ts index 9a61d37c..2371af1d 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"); + }); }); diff --git a/packages/ai/test/tool-catalog.test.ts b/packages/ai/test/tool-catalog.test.ts index 10d04403..d4e380c6 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,59 @@ 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("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"), + 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/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..7c9925d6 100644 --- a/packages/pi-extension/CHANGELOG.md +++ b/packages/pi-extension/CHANGELOG.md @@ -2,11 +2,27 @@ ## 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 - 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/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" } } diff --git a/packages/pi-extension/src/index.ts b/packages/pi-extension/src/index.ts index aa482f5f..f758076e 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, @@ -39,6 +40,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 +120,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 +202,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); @@ -208,7 +219,22 @@ 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`); + } + // 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(); initialized = false; 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..35d7957f 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,119 @@ 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("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("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) => { + 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 +420,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(); }); }); 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" } ] }