From 9d7da51b5f27121e29ab3dbf38d7da3285c53a02 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:32:40 +0000 Subject: [PATCH 1/2] PR 3: purge legacy pi 0.67 wiring from cua-cli Delete the dead pre-harness source files (agent.ts, agent-prompt.ts, old models.ts, config.ts, old sessions.ts, skills.ts, old named-sessions.ts, old action/runner.ts, old output/jsonl.ts) now that the harness paths from PR 1 and PR 2 cover every CLI surface. Drop @mariozechner/*, the @onkernel/cua-{translator,openai,anthropic,gemini,tzafon,yutori} provider packages, and smol-toml from packages/cua-cli/package.json so npm ls on cua-cli no longer mentions any of them. Remove the --config-profile flag, the cua config init|show subcommand, and the now-dead configProfile plumbing through StartNamedSessionOptions / NamedSessionMetadata. Update HELP, the cua-cli README (env-var auth, no TOML config, provider:model refs), and the package description to match. Drop the deleted-package tsconfig references from packages/cua-cli/tsconfig.json. Acceptance per docs/cua-cli-harness-migration.md PR 3: npm ls cua-cli has no removed packages; full monorepo build + cua-cli vitest suite are green. --- package-lock.json | 881 ++---------------- packages/cua-cli/README.md | 104 +-- packages/cua-cli/package.json | 15 +- packages/cua-cli/src/action/runner.ts | 251 ----- packages/cua-cli/src/agent-prompt.ts | 72 -- packages/cua-cli/src/agent.ts | 384 -------- packages/cua-cli/src/cli.ts | 49 +- packages/cua-cli/src/config.ts | 600 ------------ packages/cua-cli/src/harness-browser.ts | 3 +- .../cua-cli/src/harness-named-sessions.ts | 10 +- packages/cua-cli/src/models.ts | 211 ----- packages/cua-cli/src/named-sessions.ts | 253 ----- packages/cua-cli/src/output/jsonl.ts | 160 ---- packages/cua-cli/src/sessions.ts | 129 --- packages/cua-cli/src/skills.ts | 124 --- packages/cua-cli/tsconfig.json | 6 - 16 files changed, 117 insertions(+), 3135 deletions(-) delete mode 100644 packages/cua-cli/src/action/runner.ts delete mode 100644 packages/cua-cli/src/agent-prompt.ts delete mode 100644 packages/cua-cli/src/agent.ts delete mode 100644 packages/cua-cli/src/config.ts delete mode 100644 packages/cua-cli/src/models.ts delete mode 100644 packages/cua-cli/src/named-sessions.ts delete mode 100644 packages/cua-cli/src/output/jsonl.ts delete mode 100644 packages/cua-cli/src/sessions.ts delete mode 100644 packages/cua-cli/src/skills.ts diff --git a/package-lock.json b/package-lock.json index 19740aff..b81a6724 100644 --- a/package-lock.json +++ b/package-lock.json @@ -572,14 +572,6 @@ "node": "^22.18.0 || >=24.11.0" } }, - "node_modules/@borewit/text-codec": { - "version": "0.2.2", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/@earendil-works/pi-agent-core": { "version": "0.79.1", "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.1.tgz", @@ -3450,54 +3442,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mariozechner/clipboard": { - "version": "0.3.2", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@mariozechner/clipboard-darwin-arm64": "0.3.2", - "@mariozechner/clipboard-darwin-universal": "0.3.2", - "@mariozechner/clipboard-darwin-x64": "0.3.2", - "@mariozechner/clipboard-linux-arm64-gnu": "0.3.2", - "@mariozechner/clipboard-linux-arm64-musl": "0.3.2", - "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.2", - "@mariozechner/clipboard-linux-x64-gnu": "0.3.2", - "@mariozechner/clipboard-linux-x64-musl": "0.3.2", - "@mariozechner/clipboard-win32-arm64-msvc": "0.3.2", - "@mariozechner/clipboard-win32-x64-msvc": "0.3.2" - } - }, - "node_modules/@mariozechner/clipboard-linux-x64-gnu": { - "version": "0.3.2", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@mariozechner/jiti": { - "version": "2.6.5", - "license": "MIT", - "dependencies": { - "std-env": "^3.10.0", - "yoctocolors": "^2.1.2" - }, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/@mariozechner/pi-agent-core": { "version": "0.67.6", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@mariozechner/pi-ai": "^0.67.6" }, @@ -3508,6 +3457,8 @@ "node_modules/@mariozechner/pi-ai": { "version": "0.67.6", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@anthropic-ai/sdk": "^0.90.0", "@aws-sdk/client-bedrock-runtime": "^3.1030.0", @@ -3530,61 +3481,10 @@ "node": ">=20.0.0" } }, - "node_modules/@mariozechner/pi-coding-agent": { - "version": "0.67.6", - "license": "MIT", - "dependencies": { - "@mariozechner/jiti": "^2.6.2", - "@mariozechner/pi-agent-core": "^0.67.6", - "@mariozechner/pi-ai": "^0.67.6", - "@mariozechner/pi-tui": "^0.67.6", - "@silvia-odwyer/photon-node": "^0.3.4", - "ajv": "^8.17.1", - "chalk": "^5.5.0", - "cli-highlight": "^2.1.11", - "diff": "^8.0.2", - "extract-zip": "^2.0.1", - "file-type": "^21.1.1", - "glob": "^13.0.1", - "hosted-git-info": "^9.0.2", - "ignore": "^7.0.5", - "marked": "^15.0.12", - "minimatch": "^10.2.3", - "proper-lockfile": "^4.1.2", - "strip-ansi": "^7.1.0", - "undici": "^7.19.1", - "uuid": "^11.1.0", - "yaml": "^2.8.2" - }, - "bin": { - "pi": "dist/cli.js" - }, - "engines": { - "node": ">=20.6.0" - }, - "optionalDependencies": { - "@mariozechner/clipboard": "^0.3.2" - } - }, - "node_modules/@mariozechner/pi-tui": { - "version": "0.67.6", - "license": "MIT", - "dependencies": { - "@types/mime-types": "^2.1.4", - "chalk": "^5.5.0", - "get-east-asian-width": "^1.3.0", - "marked": "^15.0.12", - "mime-types": "^3.0.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "optionalDependencies": { - "koffi": "^2.9.0" - } - }, "node_modules/@mistralai/mistralai": { "version": "1.14.1", + "optional": true, + "peer": true, "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", @@ -4347,10 +4247,6 @@ "win32" ] }, - "node_modules/@silvia-odwyer/photon-node": { - "version": "0.3.4", - "license": "Apache-2.0" - }, "node_modules/@sinclair/typebox": { "version": "0.34.49", "license": "MIT" @@ -4475,28 +4371,11 @@ "node": ">=14.0.0" } }, - "node_modules/@tokenizer/inflate": { - "version": "0.4.1", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "license": "MIT" - }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", @@ -4541,10 +4420,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mime-types": { - "version": "2.1.4", - "license": "MIT" - }, "node_modules/@types/node": { "version": "22.18.4", "license": "MIT", @@ -4556,14 +4431,6 @@ "version": "0.12.0", "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@tzafon/lightcone": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/@tzafon/lightcone/-/lightcone-0.7.2.tgz", @@ -4695,6 +4562,8 @@ "node_modules/ajv": { "version": "8.18.0", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4709,6 +4578,8 @@ "node_modules/ajv-formats": { "version": "3.0.1", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -4721,29 +4592,6 @@ } } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/ansis": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz", @@ -4754,10 +4602,6 @@ "node": ">=14" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "license": "MIT" - }, "node_modules/anynum": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.0.tgz", @@ -4801,6 +4645,8 @@ "node_modules/ast-types": { "version": "0.13.4", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "tslib": "^2.0.1" }, @@ -4808,13 +4654,6 @@ "node": ">=4" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/base64-js": { "version": "1.5.1", "funding": [ @@ -4836,6 +4675,8 @@ "node_modules/basic-ftp": { "version": "5.3.0", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" } @@ -4863,23 +4704,6 @@ "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, - "node_modules/brace-expansion": { - "version": "5.0.5", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "license": "BSD-3-Clause" @@ -4914,6 +4738,8 @@ "node_modules/chalk": { "version": "5.6.2", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -4931,79 +4757,6 @@ "node": ">= 16" } }, - "node_modules/cli-highlight": { - "version": "2.1.11", - "license": "ISC", - "dependencies": { - "chalk": "^4.0.0", - "highlight.js": "^10.7.1", - "mz": "^2.4.0", - "parse5": "^5.1.1", - "parse5-htmlparser2-tree-adapter": "^6.0.0", - "yargs": "^16.0.0" - }, - "bin": { - "highlight": "bin/highlight" - }, - "engines": { - "node": ">=8.0.0", - "npm": ">=5.0.0" - } - }, - "node_modules/cli-highlight/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cliui": { - "version": "7.0.4", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "license": "MIT", @@ -5046,6 +4799,8 @@ "node_modules/degenerator": { "version": "5.0.1", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", @@ -5064,13 +4819,6 @@ "node": ">=8" } }, - "node_modules/diff": { - "version": "8.0.4", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dts-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-3.0.0.tgz", @@ -5099,10 +4847,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, "node_modules/empathic": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", @@ -5113,13 +4857,6 @@ "node": ">=14" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -5169,16 +4906,11 @@ "@esbuild/win32-x64": "0.27.7" } }, - "node_modules/escalade": { - "version": "3.2.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escodegen": { "version": "2.1.0", "license": "BSD-2-Clause", + "optional": true, + "peer": true, "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -5198,6 +4930,8 @@ "node_modules/esprima": { "version": "4.0.1", "license": "BSD-2-Clause", + "optional": true, + "peer": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -5209,6 +4943,8 @@ "node_modules/estraverse": { "version": "5.3.0", "license": "BSD-2-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=4.0" } @@ -5226,6 +4962,8 @@ "node_modules/esutils": { "version": "2.0.3", "license": "BSD-2-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5244,27 +4982,11 @@ "version": "3.0.2", "license": "MIT" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/fast-uri": { "version": "3.1.0", @@ -5278,7 +5000,9 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true, + "peer": true }, "node_modules/fast-xml-builder": { "version": "1.2.0", @@ -5317,13 +5041,6 @@ "fxparser": "src/cli/cli.js" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -5363,22 +5080,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/file-type": { - "version": "21.3.4", - "license": "MIT", - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "license": "MIT", @@ -5428,13 +5129,6 @@ "node": ">=18" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "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", @@ -5447,19 +5141,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-tsconfig": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", @@ -5476,6 +5157,8 @@ "node_modules/get-uri": { "version": "6.0.5", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", @@ -5488,28 +5171,15 @@ "node_modules/get-uri/node_modules/data-uri-to-buffer": { "version": "6.0.2", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 14" } }, - "node_modules/glob": { - "version": "13.0.6", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/google-auth-library": { - "version": "10.6.2", - "license": "Apache-2.0", + "node_modules/google-auth-library": { + "version": "10.6.2", + "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", @@ -5529,24 +5199,6 @@ "node": ">=14" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, "node_modules/hookable": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", @@ -5554,23 +5206,6 @@ "dev": true, "license": "MIT" }, - "node_modules/hosted-git-info": { - "version": "9.0.2", - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.3.5", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "license": "MIT", @@ -5593,24 +5228,6 @@ "node": ">= 14" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "7.0.5", "license": "MIT", @@ -5634,17 +5251,12 @@ "node_modules/ip-address": { "version": "10.1.0", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 12" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -5685,7 +5297,9 @@ }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/jwa": { "version": "2.0.1", @@ -5704,15 +5318,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/koffi": { - "version": "2.16.0", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://liberapay.com/Koromix" - } - }, "node_modules/long": { "version": "5.3.2", "license": "Apache-2.0" @@ -5727,6 +5332,8 @@ "node_modules/lru-cache": { "version": "7.18.3", "license": "ISC", + "optional": true, + "peer": true, "engines": { "node": ">=12" } @@ -5751,60 +5358,10 @@ "node": ">= 18" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/ms": { "version": "2.1.3", "license": "MIT" }, - "node_modules/mz": { - "version": "2.7.0", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", @@ -5827,6 +5384,8 @@ "node_modules/netmask": { "version": "2.1.1", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.4.0" } @@ -5889,13 +5448,6 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/obug": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", @@ -5910,13 +5462,6 @@ "node": ">=12.20.0" } }, - "node_modules/once": { - "version": "1.4.0", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/openai": { "version": "6.26.0", "license": "Apache-2.0", @@ -5950,6 +5495,8 @@ "node_modules/pac-proxy-agent": { "version": "7.2.0", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", @@ -5967,6 +5514,8 @@ "node_modules/pac-resolver": { "version": "7.0.1", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" @@ -5975,21 +5524,6 @@ "node": ">= 14" } }, - "node_modules/parse5": { - "version": "5.1.1", - "license": "MIT" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "6.0.1", - "license": "MIT" - }, "node_modules/partial-json": { "version": "0.1.7", "license": "MIT" @@ -6009,27 +5543,6 @@ "node": ">=14.0.0" } }, - "node_modules/path-scurry": { - "version": "2.0.2", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.3.5", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -6047,10 +5560,6 @@ "node": ">= 14.16" } }, - "node_modules/pend": { - "version": "1.2.0", - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6100,22 +5609,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/retry": { - "version": "0.12.0", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/protobufjs": { "version": "7.5.5", "hasInstallScript": true, @@ -6141,6 +5634,8 @@ "node_modules/proxy-agent": { "version": "6.5.0", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -6157,15 +5652,9 @@ }, "node_modules/proxy-from-env": { "version": "1.1.0", - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.4", "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } + "optional": true, + "peer": true }, "node_modules/quansync": { "version": "1.0.0", @@ -6184,16 +5673,11 @@ ], "license": "MIT" }, - "node_modules/require-directory": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6435,13 +5919,11 @@ "dev": true, "license": "ISC" }, - "node_modules/signal-exit": { - "version": "3.0.7", - "license": "ISC" - }, "node_modules/smart-buffer": { "version": "4.2.0", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -6450,6 +5932,8 @@ "node_modules/socks": { "version": "2.8.7", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" @@ -6462,6 +5946,8 @@ "node_modules/socks-proxy-agent": { "version": "8.0.5", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -6475,6 +5961,7 @@ "version": "0.6.1", "license": "BSD-3-Clause", "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6498,50 +5985,9 @@ }, "node_modules/std-env": { "version": "3.10.0", + "dev": true, "license": "MIT" }, - "node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -6570,47 +6016,6 @@ "anynum": "^1.0.0" } }, - "node_modules/strtok3": { - "version": "10.3.5", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -6672,22 +6077,6 @@ "node": ">=14.0.0" } }, - "node_modules/token-types": { - "version": "6.1.2", - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.2.1", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -6837,16 +6226,6 @@ "node": ">=14.17" } }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/unconfig-core": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz", @@ -6864,6 +6243,8 @@ "node_modules/undici": { "version": "7.25.0", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=20.18.1" } @@ -6872,17 +6253,6 @@ "version": "6.21.0", "license": "MIT" }, - "node_modules/uuid": { - "version": "11.1.0", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/vite": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", @@ -7078,42 +6448,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "license": "ISC" - }, "node_modules/ws": { "version": "8.20.0", "license": "MIT", @@ -7148,13 +6482,6 @@ "node": ">=16.0.0" } }, - "node_modules/y18n": { - "version": "5.0.8", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", @@ -7170,47 +6497,6 @@ "url": "https://github.com/sponsors/eemeli" } }, - "node_modules/yargs": { - "version": "16.2.0", - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { "version": "4.3.6", "license": "MIT", @@ -7282,20 +6568,9 @@ "dependencies": { "@earendil-works/pi-coding-agent": "0.79.1", "@earendil-works/pi-tui": "0.79.1", - "@mariozechner/pi-agent-core": "0.67.6", - "@mariozechner/pi-ai": "0.67.6", - "@mariozechner/pi-coding-agent": "0.67.6", - "@mariozechner/pi-tui": "0.67.6", "@onkernel/cua-agent": "0.3.3", "@onkernel/cua-ai": "0.3.0", - "@onkernel/cua-anthropic": "0.1.0", - "@onkernel/cua-gemini": "0.1.0", - "@onkernel/cua-openai": "0.1.0", - "@onkernel/cua-translator": "0.1.0", - "@onkernel/cua-tzafon": "0.1.0", - "@onkernel/cua-yutori": "0.1.0", - "@onkernel/sdk": "0.49.0", - "smol-toml": "1.5.1" + "@onkernel/sdk": "0.49.0" }, "bin": { "cua": "dist/cli.js" @@ -7305,18 +6580,6 @@ "vitest": "^3.2.4" } }, - "packages/cua-cli/node_modules/smol-toml": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.5.1.tgz", - "integrity": "sha512-zVvhyJEiM22SjjlsXWATuMmI4Ebhk3OUZzb1OdgyeB7onDyhwKnkSv1tech201X5YLXMsYIA89r0AmRk0M1YGg==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 18" - }, - "funding": { - "url": "https://github.com/sponsors/cyyynthia" - } - }, "packages/cua-gemini": { "name": "@onkernel/cua-gemini", "version": "0.1.0", diff --git a/packages/cua-cli/README.md b/packages/cua-cli/README.md index f67b36c0..5176972c 100644 --- a/packages/cua-cli/README.md +++ b/packages/cua-cli/README.md @@ -1,14 +1,11 @@ # `@onkernel/cua-cli` The CLI / TUI binary for the [`cua`](../../README.md) monorepo. Wires -[`@onkernel/cua-translator`](../cua-translator), -[`@onkernel/cua-openai`](../cua-openai), -[`@onkernel/cua-anthropic`](../cua-anthropic), and -[`@onkernel/cua-gemini`](../cua-gemini), and -[`@onkernel/cua-yutori`](../cua-yutori) -into a [`pi-agent-core`](https://www.npmjs.com/package/@earendil-works/pi-agent-core) -agent with a [`pi-tui`](https://www.npmjs.com/package/@earendil-works/pi-tui) -interactive front-end. +[`@onkernel/cua-agent`](../agent)'s `CuaAgentHarness` 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 (from the monorepo) @@ -41,9 +38,10 @@ 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 claude-opus-4-7 "..." -cua --print --model gemini-3-flash-preview "..." -cua --print --model n1.5-latest "..." +cua --print --model openai:gpt-5.5 "..." +cua --print --model anthropic:claude-opus-4-7 "..." +cua --print --model google:gemini-3-flash-preview "..." +cua --print --model yutori:n1.5-latest "..." # Named sessions (browser stays alive across calls): cua session start login # provisions Kernel browser @@ -65,80 +63,38 @@ cua --session abc12345 # by id prefix 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 gemini`, or +`cua models -p anthropic`, `cua models -p google` (alias: `gemini`), or `cua models -p yutori`. -CUA routes by exact model id from this supported model table. Unknown model -ids fail fast with a pointer to `cua models`. - -Recommended Gemini model id (from -[Google's Computer Use docs](https://ai.google.dev/gemini-api/docs/computer-use)): - -- `gemini-3-flash-preview` — Gemini 3 Flash with built-in computer use. +`-m` / `--model` accepts a provider-qualified `provider:model` ref (e.g. +`openai:gpt-5.5`) or a bare model id when it matches exactly one catalog +entry. The default is `openai:gpt-5.5`. ## Configuration -```bash -cua config init # interactive, writes ~/.config/cua/config.toml -cua config show # masked dump of the resolved config -``` - -Example `~/.config/cua/config.toml`: - -```toml -default_profile = "default" - -[profiles.default] -openai_api_key = "sk-..." -anthropic_api_key = "sk-ant-..." -google_api_key = "..." -yutori_api_key = "yt_..." -kernel_api_key = "kk_..." - -[profiles.default.openai.default] -reasoning_effort = "low" -tool_preamble = true -compact_threshold = 100000 +Configuration is by environment variable. There is no config file. -[profiles.default.anthropic.default] -reasoning_effort = "low" -tool_preamble = true - -[profiles.default.gemini.default] -reasoning_effort = "low" -tool_preamble = true - -[profiles.default.yutori.default] -reasoning_effort = "low" -tool_preamble = true - -[[profiles.default.openai.models]] -name = "gpt-5.5" -reasoning_effort = "medium" -``` - -Per-model blocks resolve in order: exact match → longest prefix match -→ default block. - -Env var overrides: - -| Env | Maps to | +| Env | Used for | | -------------------- | ---------------------------------------------- | -| `OPENAI_API_KEY` | `openai_api_key` | -| `OPENAI_BASE_URL` | `openai_base_url` | -| `ANTHROPIC_API_KEY` | `anthropic_api_key` | -| `ANTHROPIC_BASE_URL` | `anthropic_base_url` | -| `GOOGLE_API_KEY` | `google_api_key` | +| `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` | -| `GOOGLE_BASE_URL` | `google_base_url` | -| `YUTORI_API_KEY` | `yutori_api_key` | -| `YUTORI_BASE_URL` | `yutori_base_url` | -| `KERNEL_API_KEY` | `kernel_api_key` | -| `KERNEL_BASE_URL` | `kernel_base_url` | -| `XDG_CONFIG_HOME` | config dir base (defaults to `~/.config`) | +| `TZAFON_API_KEY` | Tzafon API key (required when `-m tzafon:…`) | +| `YUTORI_API_KEY` | Yutori API key (required when `-m yutori:…`) | +| `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 | +| `TZAFON_BASE_URL` | override Tzafon base URL | +| `YUTORI_BASE_URL` | override Yutori 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`, +default `low`) for providers that support reasoning effort. + ## Output formats `--print` defaults to streaming text. Pass `-o jsonl` for one diff --git a/packages/cua-cli/package.json b/packages/cua-cli/package.json index 11f9c127..dd0be2cb 100644 --- a/packages/cua-cli/package.json +++ b/packages/cua-cli/package.json @@ -1,7 +1,7 @@ { "name": "@onkernel/cua-cli", "version": "0.1.0", - "description": "Kernel-cloud-browser computer-use TUI: provider-agnostic CLI integrating @onkernel/cua-{translator,openai,anthropic,gemini,tzafon}", + "description": "Kernel-cloud-browser computer-use TUI built on @onkernel/cua-agent and pi-tui", "type": "module", "main": "./dist/cli.js", "types": "./dist/cli.d.ts", @@ -20,20 +20,9 @@ "dependencies": { "@earendil-works/pi-coding-agent": "0.79.1", "@earendil-works/pi-tui": "0.79.1", - "@mariozechner/pi-agent-core": "0.67.6", - "@mariozechner/pi-ai": "0.67.6", - "@mariozechner/pi-coding-agent": "0.67.6", - "@mariozechner/pi-tui": "0.67.6", "@onkernel/cua-agent": "0.3.3", "@onkernel/cua-ai": "0.3.0", - "@onkernel/cua-anthropic": "0.1.0", - "@onkernel/cua-gemini": "0.1.0", - "@onkernel/cua-openai": "0.1.0", - "@onkernel/cua-translator": "0.1.0", - "@onkernel/cua-tzafon": "0.1.0", - "@onkernel/cua-yutori": "0.1.0", - "@onkernel/sdk": "0.49.0", - "smol-toml": "1.5.1" + "@onkernel/sdk": "0.49.0" }, "devDependencies": { "@onkernel/ptywright": "0.1.0", diff --git a/packages/cua-cli/src/action/runner.ts b/packages/cua-cli/src/action/runner.ts deleted file mode 100644 index ce9e93ec..00000000 --- a/packages/cua-cli/src/action/runner.ts +++ /dev/null @@ -1,251 +0,0 @@ -import type { Agent, AgentEvent } from "@mariozechner/pi-agent-core"; -import type { SessionManager } from "@mariozechner/pi-coding-agent"; -import { type BrowserSession, ComputerTranslator } from "@onkernel/cua-translator"; -import { writeFile } from "node:fs/promises"; -import { stderr, stdout } from "node:process"; -import { promptWithScreenshot } from "../agent-prompt"; -import { type CuaAgentHandle, createCuaAgent } from "../agent"; -import type { Config } from "../config"; -import { persistAgentEvents, seedAgentFromSession } from "../sessions"; -import { type ActionRequest, buildPrompt, DEFAULT_MAX_TURNS } from "./prompts"; -import { type ActionEventInfo, type ActionResult, exitCodeFor, formatCompact, parseResult } from "./result"; - -export interface RunOptions { - cwd: string; - browser: BrowserSession; - config: Config; - modelId?: string; - verbose?: boolean; - maxTurns?: number; - /** - * Optional SessionManager. When supplied, prior turns are seeded into - * the agent transcript and new messages are persisted as they emit. - * Used when chaining action subcommands via `-s `. - */ - sessionManager?: SessionManager; -} - -export interface ScreenshotOutput { - out: string; // path or "-" for stdout -} - -export interface RunActionResult { - result: ActionResult; - exitCode: number; -} - -/** - * Run a single action subcommand against an existing browser session and - * return the parsed result + exit code. - */ -export async function runAction( - req: ActionRequest, - opts: RunOptions, - screenshot?: ScreenshotOutput, -): Promise { - const startedAt = Date.now(); - - if (req.action === "screenshot") { - const png = await screenshotPath(opts, screenshot ?? { out: "screenshot.png" }); - const elapsed = Date.now() - startedAt; - const result = parseResult("screenshot", "", [], elapsed); - result.text = png; - return { result, exitCode: 0 }; - } - - const prompt = buildPrompt(req); - const handle = createCuaAgent({ - cwd: opts.cwd, - browser: opts.browser, - config: opts.config, - modelId: opts.modelId, - sessionId: opts.browser.sessionId, - }); - - let unsubscribePersist: (() => void) | undefined; - let resumed = false; - if (opts.sessionManager) { - seedAgentFromSession(handle.agent, opts.sessionManager); - unsubscribePersist = persistAgentEvents(handle.agent, opts.sessionManager); - resumed = handle.agent.state.messages.some((m) => m.role === "user" || m.role === "assistant"); - } - const initialMessageCount = handle.agent.state.messages.length; - - const events: ActionEventInfo[] = []; - const maxTurns = req.maxTurns ?? opts.maxTurns ?? DEFAULT_MAX_TURNS; - let turns = 0; - let assistantText = ""; - const unsubscribe = handle.agent.subscribe((event: AgentEvent) => { - if (event.type === "tool_execution_start") { - collectEvent(event, events); - return; - } - if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") { - assistantText += event.assistantMessageEvent.delta; - return; - } - if (event.type === "turn_end") { - turns += 1; - if (turns >= maxTurns) { - handle.agent.abort(); - } - } - }); - - let runError: Error | undefined; - try { - await promptWithScreenshot({ - agent: handle.agent, - translator: handle.translator, - prompt, - options: { skipInitialScreenshot: resumed }, - }); - } catch (err) { - runError = err instanceof Error ? err : new Error(String(err)); - } finally { - unsubscribe(); - unsubscribePersist?.(); - } - - const elapsed = Date.now() - startedAt; - - if (assistantText.trim() === "") { - const messages = handle.agent.state.messages; - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (msg && msg.role === "assistant") { - for (const block of msg.content) { - if (block.type === "text") assistantText += block.text; - } - break; - } - } - } - - if (runError) { - const result: ActionResult = { - action: req.action, - status: "error", - text: runError.message, - elapsedMs: elapsed, - timestamp: Date.now(), - }; - return { result, exitCode: exitCodeFor(result) }; - } - - const toolError = extractLatestToolError(handle.agent.state.messages.slice(initialMessageCount)); - const result = parseResult(req.action, assistantText, events, elapsed, toolError); - return { result, exitCode: exitCodeFor(result) }; -} - -interface ToolResultLike { - role: "toolResult"; - isError?: boolean; - content?: Array<{ type?: string; text?: string }>; - details?: { error?: unknown }; -} - -function isToolErrorMessage(message: unknown): message is ToolResultLike { - if (!message || typeof message !== "object") return false; - const candidate = message as { role?: unknown; isError?: unknown }; - return candidate.role === "toolResult" && candidate.isError === true; -} - -function extractLatestToolError(messages: readonly unknown[]): string | undefined { - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i]; - if (!isToolErrorMessage(msg)) continue; - const detailError = typeof msg.details?.error === "string" ? msg.details.error.trim() : ""; - if (detailError) return detailError; - const textParts = Array.isArray(msg.content) - ? msg.content - .filter( - (block): block is { type: "text"; text: string } => - block.type === "text" && typeof block.text === "string", - ) - .map((block) => block.text.trim()) - .filter((text) => text.length > 0) - : []; - if (textParts.length > 0) return textParts.join("\n"); - return "tool execution failed"; - } - return undefined; -} - -function collectEvent( - event: { type: "tool_execution_start"; toolName: string; args: any }, - events: ActionEventInfo[], -): void { - switch (event.toolName) { - case "batch_computer_actions": - case "computer_use_extra": { - const args = event.args as { actions?: Array<{ type?: string; x?: number; y?: number }>; action?: string; url?: string; x?: number; y?: number } | undefined; - const actions = args?.actions ?? []; - for (const a of actions) { - const t = typeof a.type === "string" ? a.type : ""; - if (t === "click" || t === "double_click") { - events.push({ - actionType: t, - x: typeof a.x === "number" ? a.x : undefined, - y: typeof a.y === "number" ? a.y : undefined, - }); - } - } - return; - } - case "computer": { - // Anthropic single-action shape - const args = event.args as { action?: string; coordinate?: [number, number] } | undefined; - const a = args?.action ?? ""; - const coord = args?.coordinate; - const isClick = - a === "left_click" || - a === "right_click" || - a === "middle_click" || - a === "double_click" || - a === "triple_click"; - if (isClick && Array.isArray(coord) && coord.length >= 2) { - events.push({ actionType: a, x: Number(coord[0]), y: Number(coord[1]) }); - } - return; - } - case "click_at": { - // Gemini per-action tool with normalized 0-1000 coords - const args = event.args as { x?: number; y?: number } | undefined; - if (typeof args?.x === "number" && typeof args?.y === "number") { - events.push({ actionType: "click_at", x: args.x, y: args.y }); - } - return; - } - default: - return; - } -} - -async function screenshotPath(opts: RunOptions, out: ScreenshotOutput): Promise { - const translator = new ComputerTranslator({ - client: opts.browser.client, - sessionId: opts.browser.sessionId, - }); - const png = await translator.screenshotRaw(); - if (out.out === "-") { - stdout.write(png); - return "(stdout)"; - } - await writeFile(out.out, png); - return out.out; -} - -/** 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; -} - -export function disposeHandle(handle: CuaAgentHandle): Promise { - return handle.dispose(); -} diff --git a/packages/cua-cli/src/agent-prompt.ts b/packages/cua-cli/src/agent-prompt.ts deleted file mode 100644 index d09ffb6e..00000000 --- a/packages/cua-cli/src/agent-prompt.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { Agent, AgentMessage } from "@mariozechner/pi-agent-core"; -import type { ImageContent, TextContent } from "@mariozechner/pi-ai"; -import type { ComputerTranslator } from "@onkernel/cua-translator"; - -export interface PromptOptions { - /** - * If true (default), the first user prompt of a fresh agent transcript - * also carries an `image` content part with a freshly captured - * screenshot. Mirrors `internal/agent/agent.go:Run` first-turn - * behaviour from the cua reference implementation. - */ - includeInitialScreenshot?: boolean; - /** - * Force-skip the initial-screenshot injection even on a fresh - * transcript. Used when resuming a persisted session: the agent - * already has prior conversation context and the new browser is on a - * blank tab, so adding a blank-page screenshot is just noise. - */ - skipInitialScreenshot?: boolean; -} - -function hasPriorTurn(agent: Agent): boolean { - for (const msg of agent.state.messages) { - if (msg.role === "user" || msg.role === "assistant") return true; - } - return false; -} - -/** - * Send a user prompt to the agent. On the first turn of a fresh - * transcript the helper also captures and attaches a screenshot, giving - * the model immediate visual context for the browser. Subsequent turns - * rely on tool-result screenshots flowing through the message history. - */ -export async function promptWithScreenshot(args: { - agent: Agent; - translator: ComputerTranslator; - prompt: string; - options?: PromptOptions; -}): Promise { - const { agent, translator, prompt } = args; - const includeShot = args.options?.includeInitialScreenshot !== false; - const skipShot = args.options?.skipInitialScreenshot === true; - const isFirst = !hasPriorTurn(agent); - - if (!isFirst || !includeShot || skipShot) { - await agent.prompt(prompt); - return; - } - - let screenshot: Buffer | undefined; - try { - screenshot = await translator.screenshotRaw(); - } catch { - // Fall back to text-only first turn if screenshot fails. - } - - const content: (TextContent | ImageContent)[] = [{ type: "text", text: prompt }]; - if (screenshot) { - content.push({ - type: "image", - data: screenshot.toString("base64"), - mimeType: "image/png", - }); - } - const message: AgentMessage = { - role: "user", - content, - timestamp: Date.now(), - }; - await agent.prompt(message); -} diff --git a/packages/cua-cli/src/agent.ts b/packages/cua-cli/src/agent.ts deleted file mode 100644 index 86c30d07..00000000 --- a/packages/cua-cli/src/agent.ts +++ /dev/null @@ -1,384 +0,0 @@ -import { - Agent, - type AgentEvent, - type AgentMessage, - type AgentTool, - type StreamFn, - type ThinkingLevel, -} from "@mariozechner/pi-agent-core"; -import { - type Api, - type Model, - registerApiProvider, - streamOpenAICompletions, - streamGoogle, - streamOpenAIResponses, - streamSimpleOpenAICompletions, - streamSimple, - streamSimpleGoogle, - streamSimpleOpenAIResponses, -} from "@mariozechner/pi-ai"; -import { - createBashTool, - createEditTool, - createFindTool, - createGrepTool, - createLsTool, - createReadTool, - createWriteTool, -} from "@mariozechner/pi-coding-agent"; -import { - buildAnthropicSystemPrompt, -} from "@onkernel/cua-anthropic"; -import { - anthropicComputerOnPayload, - composeOnPayload, - createAnthropicContextManagementOnPayload, - createAnthropicComputerTools, - registerAnthropicProvider, - wrapAnthropicStream, -} from "@onkernel/cua-anthropic/pi"; -import { - buildGeminiSystemPrompt, -} from "@onkernel/cua-gemini"; -import { createGeminiComputerTools } from "@onkernel/cua-gemini/pi"; -import { - OPENAI_BATCH_INSTRUCTIONS, -} from "@onkernel/cua-openai"; -import { createOpenAIComputerTools } from "@onkernel/cua-openai/pi"; -import { - type BrowserSession, - ComputerTranslator, -} from "@onkernel/cua-translator"; -import { - buildTzafonSystemPrompt, -} from "@onkernel/cua-tzafon"; -import { - createTzafonComputerTools, - registerTzafonProvider, -} from "@onkernel/cua-tzafon/pi"; -import { - buildYutoriSystemPrompt, -} from "@onkernel/cua-yutori"; -import { - createYutoriComputerTools, - registerYutoriProvider, - yutoriBuiltinToolsOnPayload, -} from "@onkernel/cua-yutori/pi"; -import { - type AnthropicModelConfig, - type Config, - type GeminiModelConfig, - type OpenAIModelConfig, - type TzafonModelConfig, - type YutoriModelConfig, - resolveAnthropicModelConfig, - resolveGeminiModelConfig, - resolveOpenAIModelConfig, - resolveTzafonModelConfig, - resolveYutoriModelConfig, -} from "./config"; -import { - DEFAULT_MODEL_ID, - type ProviderId, - loadModel as loadSupportedModel, -} from "./models"; -import { appendSkillsToSystemPrompt, type Skill } from "./skills"; - -let providersRegistered = false; - -/** - * Eagerly register the providers we use. pi-ai's lazy dynamic-import - * registration breaks under bundlers, so we wire them up at module load. - */ -export function registerProviders(): void { - if (providersRegistered) return; - registerApiProvider({ - api: "openai-responses", - stream: streamOpenAIResponses, - streamSimple: streamSimpleOpenAIResponses, - }); - registerApiProvider({ - api: "openai-completions", - stream: streamOpenAICompletions, - streamSimple: streamSimpleOpenAICompletions, - }); - registerAnthropicProvider(); - registerApiProvider({ - api: "google-generative-ai", - stream: streamGoogle, - streamSimple: streamSimpleGoogle, - }); - registerYutoriProvider(); - registerTzafonProvider(); - providersRegistered = true; -} - -function mapReasoningEffort(effort: string | undefined): ThinkingLevel { - const v = (effort ?? "low").trim().toLowerCase(); - switch (v) { - case "": - case "low": - return "low"; - case "none": - return "off"; - case "minimal": - return "minimal"; - case "medium": - return "medium"; - case "high": - return "high"; - case "xhigh": - return "xhigh"; - default: - return "low"; - } -} - -export interface CuaAgentOptions { - cwd: string; - browser: BrowserSession; - config: Config; - modelId?: string; // default DEFAULT_MODEL_ID - additionalSystemPrompt?: string; - skills?: Skill[]; - sessionId?: string; - skipCodingTools?: boolean; -} - -export interface CuaAgentHandle { - agent: Agent; - translator: ComputerTranslator; - model: Model; - provider: ProviderId; - modelConfig: OpenAIModelConfig | AnthropicModelConfig | GeminiModelConfig | TzafonModelConfig | YutoriModelConfig; - thinkingLevel: ThinkingLevel; - dispose(): Promise; -} - -/** - * Build a fully wired pi-agent-core Agent for cua: provider registered, - * tools loaded, system prompt set, reasoning effort + auto-compaction - * applied via the model config. - */ -export function createCuaAgent(opts: CuaAgentOptions): CuaAgentHandle { - registerProviders(); - - const modelId = opts.modelId ?? DEFAULT_MODEL_ID; - const { provider, model: loadedModel } = loadSupportedModel(modelId); - - const modelConfig = resolveModelConfigForProvider(provider, opts.config, modelId); - const model = applyProviderBaseUrl(provider, loadedModel, opts.config); - const thinkingLevel = mapReasoningEffort(modelConfig.reasoningEffort); - const toolPreamble = modelConfig.toolPreamble ?? true; - const compactThreshold = (modelConfig as OpenAIModelConfig | AnthropicModelConfig).compactThreshold; - - const translator = new ComputerTranslator({ - client: opts.browser.client, - sessionId: opts.browser.sessionId, - }); - - const tools = buildAgentTools({ - cwd: opts.cwd, - translator, - provider, - skipCodingTools: opts.skipCodingTools, - }); - - const baseSystemPrompt = buildSystemPromptForProvider(provider, { - toolPreamble, - additionalSystemPrompt: opts.additionalSystemPrompt, - }); - const systemPrompt = appendSkillsToSystemPrompt(baseSystemPrompt, opts.skills ?? []); - - const onPayload = composeOnPayload( - // OpenAI auto-compaction (no-op for Anthropic/Gemini per the model.api guard). - typeof compactThreshold === "number" && compactThreshold > 0 - ? (payload, m) => - m.api === "openai-responses" ? injectContextManagement(payload, compactThreshold) : undefined - : undefined, - provider === "anthropic" - ? createAnthropicContextManagementOnPayload({ - compactThreshold: (modelConfig as AnthropicModelConfig).compactThreshold, - }) - : undefined, - // Anthropic computer-tool spec injection. - anthropicComputerOnPayload, - provider === "yutori" ? yutoriBuiltinToolsOnPayload : undefined, - ); - - const agent = new Agent({ - initialState: { - systemPrompt, - model, - tools, - thinkingLevel, - }, - sessionId: opts.sessionId, - getApiKey: () => resolveApiKey(provider, opts.config), - streamFn: wrapAnthropicStream(streamSimple as unknown as StreamFn, { - compactThreshold: provider === "anthropic" ? (modelConfig as AnthropicModelConfig).compactThreshold : undefined, - }), - onPayload, - }); - - return { - agent, - translator, - model, - provider, - modelConfig, - thinkingLevel, - async dispose(): Promise { - await opts.browser.close(); - }, - }; -} - -// ─── Tool factory ───────────────────────────────────────────────────────── - -interface ToolFactoryOptions { - cwd: string; - translator: ComputerTranslator; - provider: ProviderId; - skipCodingTools?: boolean; -} - -function buildAgentTools(opts: ToolFactoryOptions): AgentTool[] { - const tools: AgentTool[] = []; - - switch (opts.provider) { - case "anthropic": - tools.push(...createAnthropicComputerTools(opts.translator)); - break; - case "gemini": - tools.push(...createGeminiComputerTools(opts.translator)); - break; - case "tzafon": - tools.push(...createTzafonComputerTools(opts.translator)); - break; - case "yutori": - tools.push(...createYutoriComputerTools(opts.translator)); - break; - case "openai": - default: - tools.push(...createOpenAIComputerTools(opts.translator)); - break; - } - - if (!opts.skipCodingTools) { - tools.push( - createBashTool(opts.cwd), - createReadTool(opts.cwd), - createEditTool(opts.cwd), - createWriteTool(opts.cwd), - createGrepTool(opts.cwd), - createFindTool(opts.cwd), - createLsTool(opts.cwd), - ); - } - - return tools; -} - -// ─── System prompt selection ───────────────────────────────────────────── - -const TOOL_INSTRUCTIONS = `Use bash for shell work. Use read, write, edit, grep, find, and ls for workspace files.`; -const TOOL_PREAMBLE_LINE = `Before every tool call, first output a single short sentence describing what you are about to do.`; - -interface SystemPromptOptions { - toolPreamble?: boolean; - additionalSystemPrompt?: string; -} - -function buildSystemPromptForProvider(provider: ProviderId, opts: SystemPromptOptions): string { - let preamble: string; - switch (provider) { - case "anthropic": - preamble = buildAnthropicSystemPrompt(); - break; - case "gemini": - preamble = buildGeminiSystemPrompt(); - break; - case "tzafon": - preamble = buildTzafonSystemPrompt(); - break; - case "yutori": - preamble = buildYutoriSystemPrompt({ toolPreamble: false }); - break; - case "openai": - default: - preamble = OPENAI_BATCH_INSTRUCTIONS; - break; - } - - const sections: string[] = [preamble, TOOL_INSTRUCTIONS]; - if (opts.toolPreamble !== false) sections.push(TOOL_PREAMBLE_LINE); - const extra = (opts.additionalSystemPrompt ?? "").trim(); - if (extra) sections.push(extra); - return sections.join("\n\n"); -} - -// ─── Per-provider config resolution + API key routing ──────────────────── - -function resolveModelConfigForProvider( - provider: ProviderId, - cfg: Config, - modelId: string, -): OpenAIModelConfig | AnthropicModelConfig | GeminiModelConfig | TzafonModelConfig | YutoriModelConfig { - switch (provider) { - case "anthropic": - return resolveAnthropicModelConfig(cfg, modelId); - case "gemini": - return resolveGeminiModelConfig(cfg, modelId); - case "tzafon": - return resolveTzafonModelConfig(cfg, modelId); - case "yutori": - return resolveYutoriModelConfig(cfg, modelId); - case "openai": - default: - return resolveOpenAIModelConfig(cfg, modelId); - } -} - -function resolveApiKey(provider: ProviderId, cfg: Config): string | undefined { - switch (provider) { - case "openai": - return cfg.openaiApiKey || process.env.OPENAI_API_KEY || undefined; - case "anthropic": - return cfg.anthropicApiKey || process.env.ANTHROPIC_API_KEY || undefined; - case "gemini": - return cfg.googleApiKey || process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY || undefined; - case "tzafon": - return cfg.tzafonApiKey || process.env.TZAFON_API_KEY || undefined; - case "yutori": - return cfg.yutoriApiKey || process.env.YUTORI_API_KEY || undefined; - } -} - -function applyProviderBaseUrl(provider: ProviderId, model: Model, cfg: Config): Model { - if (provider === "yutori" && cfg.yutoriBaseUrl) { - return { ...model, baseUrl: cfg.yutoriBaseUrl }; - } - return model; -} - -/** - * onPayload hook that injects `context_management: [{type:"compaction", compact_threshold:N}]` - * into the OpenAI Responses request payload. - */ -function injectContextManagement(payload: unknown, compactThreshold: number): unknown { - if (!payload || typeof payload !== "object") return undefined; - const next = { ...(payload as Record) }; - next.context_management = [ - { - type: "compaction", - compact_threshold: compactThreshold, - }, - ]; - return next; -} - -export type { Agent, AgentEvent, AgentMessage }; -export { DEFAULT_MODEL_ID }; -export type { ProviderId }; diff --git a/packages/cua-cli/src/cli.ts b/packages/cua-cli/src/cli.ts index 7506deb7..b3fae701 100644 --- a/packages/cua-cli/src/cli.ts +++ b/packages/cua-cli/src/cli.ts @@ -10,7 +10,6 @@ import { runSessionSubcommand as runSessionSubcommandHarness, type HarnessCliFlags, } from "./cli-harness"; -import * as configMod from "./config"; import { DEFAULT_CUA_MODEL_REF } from "./harness-models"; const HELP = `cua — Kernel-cloud-browser computer-use agent @@ -28,7 +27,6 @@ Usage: cua do "" cua models [-p provider] cua session start [name] | stop | list | show - cua config init|show Options: -p, --print Run a single prompt and exit @@ -43,7 +41,6 @@ Options: yutori: yutori:n1.5-latest --thinking Thinking level: off | minimal | low | medium | high | xhigh (default: low; applies to providers that support it) - --config-profile

Legacy TOML config profile to load (only used by \`cua config show\`) --profile Kernel browser profile to load --profile-no-save-changes Do not persist changes back to the profile --browser-timeout Browser inactivity timeout in seconds (default 300) @@ -69,18 +66,19 @@ Options: -h, --help Show this help Environment: - OPENAI_API_KEY Overrides the profile's OpenAI key - ANTHROPIC_API_KEY Overrides the profile's Anthropic key - GOOGLE_API_KEY Overrides the profile's Google (Gemini) key + 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 - TZAFON_API_KEY Overrides the profile's Tzafon key - YUTORI_API_KEY Overrides the profile's Yutori key - KERNEL_API_KEY Overrides the profile's Kernel key + TZAFON_API_KEY Tzafon API key (required when -m tzafon:…) + YUTORI_API_KEY Yutori API key (required when -m yutori:…) + 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 + TZAFON_BASE_URL Override Tzafon base URL YUTORI_BASE_URL Override Yutori base URL - KERNEL_BASE_URL Override Kernel 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\`) @@ -100,7 +98,6 @@ interface CliFlags { jsonlIncludeImages: boolean; model?: string; thinking?: string; - configProfile?: string; browserProfile?: string; browserTimeout?: number; maxSteps?: number; @@ -129,7 +126,6 @@ function parseCliArgs(argv: string[]): CliFlags { verbose: { type: "boolean", short: "v", default: false }, model: { type: "string", short: "m" }, thinking: { type: "string" }, - "config-profile": { type: "string" }, profile: { type: "string" }, "profile-no-save-changes": { type: "boolean", default: false }, "browser-timeout": { type: "string" }, @@ -182,7 +178,6 @@ function parseCliArgs(argv: string[]): CliFlags { debugTui: !!parsed.values["debug-tui"], model: parsed.values.model as string | undefined, thinking: parsed.values.thinking as string | undefined, - configProfile: parsed.values["config-profile"] as string | undefined, browserProfile: parsed.values.profile as string | undefined, browserTimeout: Number.isFinite(browserTimeout) ? browserTimeout : undefined, maxSteps: Number.isFinite(maxSteps) ? maxSteps : undefined, @@ -225,25 +220,6 @@ function toHarnessFlags(flags: CliFlags): HarnessCliFlags { }; } -async function runConfigSubcommand(args: string[], profile?: string): Promise { - const sub = args[0]; - if (!sub || sub === "help" || sub === "--help" || sub === "-h") { - stdout.write("cua config init|show\n"); - return 0; - } - if (sub === "init") { - await configMod.init(); - return 0; - } - if (sub === "show") { - const text = await configMod.show(profile); - stdout.write(text); - return 0; - } - stderr.write(`unknown config subcommand: ${sub}\n`); - return 2; -} - const SUBCOMMANDS = new Set(["open", "click", "type", "press", "observe", "url", "screenshot", "do"]); export async function main(argv: string[]): Promise { @@ -267,15 +243,6 @@ export async function main(argv: string[]): Promise { const positionals = flags.positionals; const first = positionals[0]; - if (first === "config") { - try { - return await runConfigSubcommand(positionals.slice(1), flags.configProfile); - } catch (err) { - stderr.write(`config error: ${(err as Error).message}\n`); - return 2; - } - } - if (first === "session") { try { return await runSessionSubcommandHarness(positionals.slice(1), toHarnessFlags(flags)); diff --git a/packages/cua-cli/src/config.ts b/packages/cua-cli/src/config.ts deleted file mode 100644 index 4efcd9e7..00000000 --- a/packages/cua-cli/src/config.ts +++ /dev/null @@ -1,600 +0,0 @@ -import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; -import { homedir } from "node:os"; -import { dirname, join } from "node:path"; -import { stdin, stdout } from "node:process"; -import { createInterface } from "node:readline/promises"; -import { parse as parseToml, stringify as stringifyToml } from "smol-toml"; - -export interface OpenAIModelConfig { - name?: string; - reasoningEffort?: string; - toolPreamble?: boolean; - compactThreshold?: number | false; -} - -export interface OpenAIConfig { - default: OpenAIModelConfig; - models: OpenAIModelConfig[]; -} - -export interface AnthropicModelConfig { - name?: string; - reasoningEffort?: string; - toolPreamble?: boolean; - compactThreshold?: number | false; -} - -export interface AnthropicConfig { - default: AnthropicModelConfig; - models: AnthropicModelConfig[]; -} - -export interface GeminiModelConfig { - name?: string; - reasoningEffort?: string; - toolPreamble?: boolean; -} - -export interface GeminiConfig { - default: GeminiModelConfig; - models: GeminiModelConfig[]; -} - -export interface YutoriModelConfig { - name?: string; - reasoningEffort?: string; - toolPreamble?: boolean; -} - -export interface YutoriConfig { - default: YutoriModelConfig; - models: YutoriModelConfig[]; -} - -export interface TzafonModelConfig { - name?: string; - reasoningEffort?: string; - toolPreamble?: boolean; -} - -export interface TzafonConfig { - default: TzafonModelConfig; - models: TzafonModelConfig[]; -} - -export interface Config { - openaiApiKey: string; - openaiBaseUrl: string; - anthropicApiKey: string; - anthropicBaseUrl: string; - googleApiKey: string; - googleBaseUrl: string; - tzafonApiKey: string; - yutoriApiKey: string; - yutoriBaseUrl: string; - kernelApiKey: string; - kernelBaseUrl: string; - openai: OpenAIConfig; - anthropic: AnthropicConfig; - gemini: GeminiConfig; - tzafon: TzafonConfig; - yutori: YutoriConfig; -} - -export function configDir(): string { - const xdg = process.env.XDG_CONFIG_HOME; - if (xdg) return join(xdg, "cua"); - return join(homedir(), ".config", "cua"); -} - -export function configFilePath(): string { - return join(configDir(), "config.toml"); -} - -const ENV_KEYS: Array<{ env: string; key: keyof Config }> = [ - { env: "OPENAI_API_KEY", key: "openaiApiKey" }, - { env: "OPENAI_BASE_URL", key: "openaiBaseUrl" }, - { env: "ANTHROPIC_API_KEY", key: "anthropicApiKey" }, - { env: "ANTHROPIC_BASE_URL", key: "anthropicBaseUrl" }, - { env: "GOOGLE_API_KEY", key: "googleApiKey" }, - { env: "GEMINI_API_KEY", key: "googleApiKey" }, - { env: "GOOGLE_BASE_URL", key: "googleBaseUrl" }, - { env: "TZAFON_API_KEY", key: "tzafonApiKey" }, - { env: "YUTORI_API_KEY", key: "yutoriApiKey" }, - { env: "YUTORI_BASE_URL", key: "yutoriBaseUrl" }, - { env: "KERNEL_API_KEY", key: "kernelApiKey" }, - { env: "KERNEL_BASE_URL", key: "kernelBaseUrl" }, -]; - -function emptyConfig(): Config { - return { - openaiApiKey: "", - openaiBaseUrl: "", - anthropicApiKey: "", - anthropicBaseUrl: "", - googleApiKey: "", - googleBaseUrl: "", - tzafonApiKey: "", - yutoriApiKey: "", - yutoriBaseUrl: "", - kernelApiKey: "", - kernelBaseUrl: "", - openai: { default: {}, models: [] }, - anthropic: { default: {}, models: [] }, - gemini: { default: {}, models: [] }, - tzafon: { default: {}, models: [] }, - yutori: { default: {}, models: [] }, - }; -} - -function readModelConfig(raw: any): OpenAIModelConfig { - const out: OpenAIModelConfig = {}; - if (typeof raw?.name === "string") out.name = raw.name; - if (typeof raw?.reasoning_effort === "string") out.reasoningEffort = raw.reasoning_effort; - if (typeof raw?.tool_preamble === "boolean") out.toolPreamble = raw.tool_preamble; - if (typeof raw?.compact_threshold === "number" || raw?.compact_threshold === false) { - out.compactThreshold = raw.compact_threshold; - } - return out; -} - -function readOpenAIConfig(raw: any): OpenAIConfig { - const out: OpenAIConfig = { default: {}, models: [] }; - if (raw && typeof raw === "object") { - if (raw.default) out.default = readModelConfig(raw.default); - if (Array.isArray(raw.models)) out.models = raw.models.map(readModelConfig); - } - return out; -} - -function readPlainModelConfig(raw: any): { name?: string; reasoningEffort?: string; toolPreamble?: boolean; compactThreshold?: number | false } { - const out: { name?: string; reasoningEffort?: string; toolPreamble?: boolean; compactThreshold?: number | false } = {}; - if (typeof raw?.name === "string") out.name = raw.name; - if (typeof raw?.reasoning_effort === "string") out.reasoningEffort = raw.reasoning_effort; - if (typeof raw?.tool_preamble === "boolean") out.toolPreamble = raw.tool_preamble; - if (typeof raw?.compact_threshold === "number" || raw?.compact_threshold === false) { - out.compactThreshold = raw.compact_threshold; - } - return out; -} - -function readAnthropicConfig(raw: any): AnthropicConfig { - const out: AnthropicConfig = { default: {}, models: [] }; - if (raw && typeof raw === "object") { - if (raw.default) out.default = readPlainModelConfig(raw.default); - if (Array.isArray(raw.models)) out.models = raw.models.map(readPlainModelConfig); - } - return out; -} - -function readGeminiConfig(raw: any): GeminiConfig { - const out: GeminiConfig = { default: {}, models: [] }; - if (raw && typeof raw === "object") { - if (raw.default) out.default = readPlainModelConfig(raw.default); - if (Array.isArray(raw.models)) out.models = raw.models.map(readPlainModelConfig); - } - return out; -} - -function readYutoriConfig(raw: any): YutoriConfig { - const out: YutoriConfig = { default: {}, models: [] }; - if (raw && typeof raw === "object") { - if (raw.default) out.default = readPlainModelConfig(raw.default); - if (Array.isArray(raw.models)) out.models = raw.models.map(readPlainModelConfig); - } - return out; -} - -function readTzafonConfig(raw: any): TzafonConfig { - const out: TzafonConfig = { default: {}, models: [] }; - if (raw && typeof raw === "object") { - if (raw.default) out.default = readPlainModelConfig(raw.default); - if (Array.isArray(raw.models)) out.models = raw.models.map(readPlainModelConfig); - } - return out; -} - -function readProfile(profile: any): { cfg: Config; explicit: Set } { - const cfg = emptyConfig(); - const explicit = new Set(); - if (typeof profile?.openai_api_key === "string") { - cfg.openaiApiKey = profile.openai_api_key; - explicit.add("openaiApiKey"); - } - if (typeof profile?.openai_base_url === "string") { - cfg.openaiBaseUrl = profile.openai_base_url; - explicit.add("openaiBaseUrl"); - } - if (typeof profile?.anthropic_api_key === "string") { - cfg.anthropicApiKey = profile.anthropic_api_key; - explicit.add("anthropicApiKey"); - } - if (typeof profile?.anthropic_base_url === "string") { - cfg.anthropicBaseUrl = profile.anthropic_base_url; - explicit.add("anthropicBaseUrl"); - } - if (typeof profile?.google_api_key === "string") { - cfg.googleApiKey = profile.google_api_key; - explicit.add("googleApiKey"); - } - if (typeof profile?.google_base_url === "string") { - cfg.googleBaseUrl = profile.google_base_url; - explicit.add("googleBaseUrl"); - } - if (typeof profile?.tzafon_api_key === "string") { - cfg.tzafonApiKey = profile.tzafon_api_key; - explicit.add("tzafonApiKey"); - } - if (typeof profile?.yutori_api_key === "string") { - cfg.yutoriApiKey = profile.yutori_api_key; - explicit.add("yutoriApiKey"); - } - if (typeof profile?.yutori_base_url === "string") { - cfg.yutoriBaseUrl = profile.yutori_base_url; - explicit.add("yutoriBaseUrl"); - } - if (typeof profile?.kernel_api_key === "string") { - cfg.kernelApiKey = profile.kernel_api_key; - explicit.add("kernelApiKey"); - } - if (typeof profile?.kernel_base_url === "string") { - cfg.kernelBaseUrl = profile.kernel_base_url; - explicit.add("kernelBaseUrl"); - } - if (profile?.openai) cfg.openai = readOpenAIConfig(profile.openai); - if (profile?.anthropic) cfg.anthropic = readAnthropicConfig(profile.anthropic); - if (profile?.gemini) cfg.gemini = readGeminiConfig(profile.gemini); - if (profile?.google) cfg.gemini = readGeminiConfig(profile.google); - if (profile?.tzafon) cfg.tzafon = readTzafonConfig(profile.tzafon); - if (profile?.yutori) cfg.yutori = readYutoriConfig(profile.yutori); - return { cfg, explicit }; -} - -function applyEnvFallback(cfg: Config, explicit: Set): void { - for (const { env, key } of ENV_KEYS) { - if (explicit.has(key)) continue; - const value = process.env[env]; - if (value && value.trim().length > 0) { - (cfg[key] as string) = value; - } - } -} - -async function fileExists(path: string): Promise { - try { - await stat(path); - return true; - } catch { - return false; - } -} - -/** - * Resolve configuration from a selected profile plus environment fallbacks. - * Explicit values in the selected profile win. Environment variables only fill - * in fields the profile does not define. A profile must be resolved either via - * the explicit argument or via default_profile in the config file. - */ -export async function load(profileArg?: string): Promise { - const cfgPath = configFilePath(); - const exists = await fileExists(cfgPath); - if (!exists) { - if (profileArg) { - throw new Error(`config file not found at ${cfgPath} (run \`cua config init\`)`); - } - throw new Error(`no config file found at ${cfgPath} and no profile specified (run \`cua config init\`)`); - } - - const raw = parseToml(await readFile(cfgPath, "utf8")) as any; - let resolvedProfile = profileArg; - if (!resolvedProfile && typeof raw?.default_profile === "string") { - resolvedProfile = raw.default_profile; - } - if (!resolvedProfile) { - throw new Error( - `no profile specified and no default_profile set in ${cfgPath} (use -c or run \`cua config init\`)`, - ); - } - - const profileBlock = raw?.profiles?.[resolvedProfile]; - if (!profileBlock || typeof profileBlock !== "object") { - throw new Error(`profile "${resolvedProfile}" not found in ${cfgPath}`); - } - - const { cfg, explicit } = readProfile(profileBlock); - applyEnvFallback(cfg, explicit); - return cfg; -} - -export function loadFromEnv(): Config { - const cfg = emptyConfig(); - applyEnvFallback(cfg, new Set()); - return cfg; -} - -/** - * Resolve per-model OpenAI settings. - * - * Resolution order: - * 1. Exact model key match - * 2. Longest prefix match (for snapshot-style names) - * 3. The "default" block - */ -export function resolveOpenAIModelConfig(cfg: Config, model: string): OpenAIModelConfig { - const trimmed = model.trim(); - const resolved: OpenAIModelConfig = { ...cfg.openai.default }; - if (cfg.openai.models.length === 0) return resolved; - - for (const entry of cfg.openai.models) { - if ((entry.name ?? "").trim() === trimmed) { - return mergeOpenAIModelConfig(resolved, entry); - } - } - - let longestPrefix = ""; - let prefixCfg: OpenAIModelConfig | undefined; - for (const entry of cfg.openai.models) { - const key = (entry.name ?? "").trim(); - if (!key) continue; - if (trimmed.startsWith(key) && key.length > longestPrefix.length) { - longestPrefix = key; - prefixCfg = entry; - } - } - if (prefixCfg) return mergeOpenAIModelConfig(resolved, prefixCfg); - return resolved; -} - -function mergeOpenAIModelConfig(base: OpenAIModelConfig, override: OpenAIModelConfig): OpenAIModelConfig { - const out: OpenAIModelConfig = { ...base }; - if ((override.reasoningEffort ?? "").trim().length > 0) out.reasoningEffort = override.reasoningEffort; - if (override.toolPreamble !== undefined) out.toolPreamble = override.toolPreamble; - if (override.compactThreshold !== undefined) out.compactThreshold = override.compactThreshold; - return out; -} - -export function resolveAnthropicModelConfig(cfg: Config, model: string): AnthropicModelConfig { - const trimmed = model.trim(); - const resolved: AnthropicModelConfig = { ...cfg.anthropic.default }; - if (cfg.anthropic.models.length === 0) return resolved; - - for (const entry of cfg.anthropic.models) { - if ((entry.name ?? "").trim() === trimmed) { - return mergePlainModelConfig(resolved, entry); - } - } - - let longestPrefix = ""; - let prefixCfg: AnthropicModelConfig | undefined; - for (const entry of cfg.anthropic.models) { - const key = (entry.name ?? "").trim(); - if (!key) continue; - if (trimmed.startsWith(key) && key.length > longestPrefix.length) { - longestPrefix = key; - prefixCfg = entry; - } - } - if (prefixCfg) return mergePlainModelConfig(resolved, prefixCfg); - return resolved; -} - -export function resolveGeminiModelConfig(cfg: Config, model: string): GeminiModelConfig { - const trimmed = model.trim(); - const resolved: GeminiModelConfig = { ...cfg.gemini.default }; - if (cfg.gemini.models.length === 0) return resolved; - - for (const entry of cfg.gemini.models) { - if ((entry.name ?? "").trim() === trimmed) { - return mergePlainModelConfig(resolved, entry); - } - } - - let longestPrefix = ""; - let prefixCfg: GeminiModelConfig | undefined; - for (const entry of cfg.gemini.models) { - const key = (entry.name ?? "").trim(); - if (!key) continue; - if (trimmed.startsWith(key) && key.length > longestPrefix.length) { - longestPrefix = key; - prefixCfg = entry; - } - } - if (prefixCfg) return mergePlainModelConfig(resolved, prefixCfg); - return resolved; -} - -export function resolveTzafonModelConfig(cfg: Config, model: string): TzafonModelConfig { - const trimmed = model.trim(); - const resolved: TzafonModelConfig = { ...cfg.tzafon.default }; - if (cfg.tzafon.models.length === 0) return resolved; - - for (const entry of cfg.tzafon.models) { - if ((entry.name ?? "").trim() === trimmed) { - return mergePlainModelConfig(resolved, entry); - } - } - - let longestPrefix = ""; - let prefixCfg: TzafonModelConfig | undefined; - for (const entry of cfg.tzafon.models) { - const key = (entry.name ?? "").trim(); - if (!key) continue; - if (trimmed.startsWith(key) && key.length > longestPrefix.length) { - longestPrefix = key; - prefixCfg = entry; - } - } - if (prefixCfg) return mergePlainModelConfig(resolved, prefixCfg); - return resolved; -} - -export function resolveYutoriModelConfig(cfg: Config, model: string): YutoriModelConfig { - const trimmed = model.trim(); - const resolved: YutoriModelConfig = { ...cfg.yutori.default }; - if (cfg.yutori.models.length === 0) return resolved; - - for (const entry of cfg.yutori.models) { - if ((entry.name ?? "").trim() === trimmed) { - return mergePlainModelConfig(resolved, entry); - } - } - - let longestPrefix = ""; - let prefixCfg: YutoriModelConfig | undefined; - for (const entry of cfg.yutori.models) { - const key = (entry.name ?? "").trim(); - if (!key) continue; - if (trimmed.startsWith(key) && key.length > longestPrefix.length) { - longestPrefix = key; - prefixCfg = entry; - } - } - if (prefixCfg) return mergePlainModelConfig(resolved, prefixCfg); - return resolved; -} - -function mergePlainModelConfig( - base: T, - override: T, -): T { - const out: T = { ...base }; - if ((override.reasoningEffort ?? "").trim().length > 0) out.reasoningEffort = override.reasoningEffort; - if (override.toolPreamble !== undefined) out.toolPreamble = override.toolPreamble; - if (override.compactThreshold !== undefined) out.compactThreshold = override.compactThreshold; - return out; -} - -function maskSecret(value: string): string { - if (!value) return "(not set)"; - if (value.length <= 8) return "****"; - return value.slice(0, 4) + "..." + value.slice(-4); -} - -function maskRecord(node: any): any { - if (!node || typeof node !== "object") return node; - if (Array.isArray(node)) return node.map(maskRecord); - const out: Record = {}; - for (const [key, value] of Object.entries(node)) { - if (typeof value === "string" && key.endsWith("_api_key")) { - out[key] = maskSecret(value); - } else if (value && typeof value === "object") { - out[key] = maskRecord(value); - } else { - out[key] = value; - } - } - return out; -} - -/** Return a human-readable representation of the masked config file. */ -export async function show(profile?: string): Promise { - await load(profile); - const cfgPath = configFilePath(); - const text = await readFile(cfgPath, "utf8"); - const parsed = parseToml(text); - const masked = maskRecord(parsed); - return `Config file: ${cfgPath}\n\nMasked config file:\n${stringifyToml(masked as any)}\n`; -} - -async function prompt(rl: ReturnType, label: string): Promise { - const answer = await rl.question(label); - return answer.trim(); -} - -/** Interactively create or update a config file with a named profile. */ -export async function init(): Promise { - const cfgPath = configFilePath(); - const rl = createInterface({ input: stdin, output: stdout }); - try { - let tree: Record = {}; - if (await fileExists(cfgPath)) { - tree = parseToml(await readFile(cfgPath, "utf8")) as Record; - } - - const profileName = await prompt(rl, 'Profile name, e.g. "default": '); - if (!profileName) throw new Error("profile name is required"); - - tree.profiles ??= {}; - if (tree.profiles[profileName]) { - const overwrite = await prompt(rl, `Profile "${profileName}" already exists. Overwrite? [y/N] `); - if (overwrite.toLowerCase() !== "y") { - console.log("Aborted."); - return; - } - } - - const openaiKey = await prompt(rl, "OpenAI API key (leave blank to skip): "); - const anthropicKey = await prompt(rl, "Anthropic API key (leave blank to skip): "); - const googleKey = await prompt(rl, "Google API key (leave blank to skip): "); - const tzafonKey = await prompt(rl, "Tzafon API key (leave blank to skip): "); - const yutoriKey = await prompt(rl, "Yutori API key (leave blank to skip): "); - const kernelKey = await prompt(rl, "Kernel API key: "); - if (!openaiKey && !anthropicKey && !googleKey && !tzafonKey && !yutoriKey) { - throw new Error("at least one of OpenAI, Anthropic, Google, Tzafon, or Yutori API key is required"); - } - if (!kernelKey) { - throw new Error("Kernel API key is required"); - } - - const profileBlock: Record = { - kernel_api_key: kernelKey, - }; - if (openaiKey) { - profileBlock.openai_api_key = openaiKey; - profileBlock.openai = { - default: { - reasoning_effort: "low", - tool_preamble: true, - }, - }; - } - if (anthropicKey) { - profileBlock.anthropic_api_key = anthropicKey; - profileBlock.anthropic = { - default: { - reasoning_effort: "low", - tool_preamble: true, - }, - }; - } - if (googleKey) { - profileBlock.google_api_key = googleKey; - profileBlock.gemini = { - default: { - reasoning_effort: "low", - tool_preamble: true, - }, - }; - } - if (tzafonKey) { - profileBlock.tzafon_api_key = tzafonKey; - profileBlock.tzafon = { - default: { - reasoning_effort: "low", - tool_preamble: true, - }, - }; - } - if (yutoriKey) { - profileBlock.yutori_api_key = yutoriKey; - profileBlock.yutori = { - default: { - reasoning_effort: "low", - tool_preamble: true, - }, - }; - } - tree.profiles[profileName] = profileBlock; - - const setDefault = await prompt(rl, `Set "${profileName}" as the default profile? [Y/n] `); - if (setDefault === "" || setDefault.toLowerCase() === "y" || setDefault.toLowerCase() === "yes") { - tree.default_profile = profileName; - } - - await mkdir(dirname(cfgPath), { recursive: true }); - await writeFile(cfgPath, stringifyToml(tree as any), { mode: 0o600 }); - console.log(`\nProfile "${profileName}" written to ${cfgPath}`); - } finally { - rl.close(); - } -} diff --git a/packages/cua-cli/src/harness-browser.ts b/packages/cua-cli/src/harness-browser.ts index 268ca74b..ede47b9c 100644 --- a/packages/cua-cli/src/harness-browser.ts +++ b/packages/cua-cli/src/harness-browser.ts @@ -33,8 +33,7 @@ function looksLikeProfileId(selector: string): boolean { /** * 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 (same - * semantics as the legacy `cua-translator.browserSession.open` path). + * 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(); diff --git a/packages/cua-cli/src/harness-named-sessions.ts b/packages/cua-cli/src/harness-named-sessions.ts index 2791d22f..9b703d3d 100644 --- a/packages/cua-cli/src/harness-named-sessions.ts +++ b/packages/cua-cli/src/harness-named-sessions.ts @@ -7,9 +7,10 @@ import { createKernelClient, resolveProfileId } from "./harness-browser"; /** * Named sessions: durable, slug-keyed pointers to a Kernel cloud browser - * session that can be reused across `cua` invocations. The metadata file - * format and path are preserved from the legacy implementation; only the - * Kernel calls move from `cua-translator.browserSession` to the SDK. + * 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 { @@ -18,7 +19,6 @@ export interface NamedSessionMetadata { live_url?: string; profile_id?: string; transcript_path?: string; - config_profile?: string; created_at: number; } @@ -95,7 +95,6 @@ export interface StartNamedSessionOptions { name: string; apiKey: string; baseUrl?: string; - configProfile?: string; browserTimeoutSeconds?: number; /** Profile id or name (created if missing). Same semantics as `--profile`. */ profileSelector?: string; @@ -139,7 +138,6 @@ export async function startNamedSession(opts: StartNamedSessionOptions): Promise kernel_session_id: browser.session_id, live_url: browser.browser_live_view_url, profile_id: profileId, - config_profile: opts.configProfile, created_at: Date.now(), }; const metadataPath = await writeNamedSession(meta); diff --git a/packages/cua-cli/src/models.ts b/packages/cua-cli/src/models.ts deleted file mode 100644 index 650b2bd1..00000000 --- a/packages/cua-cli/src/models.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { - type Api, - type Model, - getModel, - getModels, -} from "@mariozechner/pi-ai"; - -export type ProviderId = "openai" | "anthropic" | "gemini" | "tzafon" | "yutori"; -export const SUPPORTED_PROVIDERS: ProviderId[] = ["openai", "anthropic", "gemini", "tzafon", "yutori"]; -export const DEFAULT_MODEL_ID = "gpt-5.5"; - -export interface SupportedModel { - provider: ProviderId; - model: string; - name: string; - origin: "cua-override" | "pi-ai-registry"; - default?: boolean; -} - -const CUA_MODEL_OVERRIDES: Record = { - openai: [ - { provider: "openai", model: "gpt-5.5", name: "GPT-5.5", origin: "cua-override", default: true }, - { provider: "openai", model: "gpt-5.5-2026-04-23", name: "GPT-5.5 (2026-04-23)", origin: "cua-override" }, - ], - anthropic: [], - gemini: [ - { provider: "gemini", model: "gemini-2.5-computer-use-preview-10-2025", name: "Gemini 2.5 Computer Use Preview", origin: "cua-override" }, - ], - tzafon: [ - { provider: "tzafon", model: "tzafon.northstar-cua-fast", name: "Tzafon Northstar CUA Fast", origin: "cua-override" }, - ], - yutori: [ - { provider: "yutori", model: "n1.5-latest", name: "Yutori Navigator n1.5", origin: "cua-override" }, - { provider: "yutori", model: "n1.5-20260428", name: "Yutori Navigator n1.5 (2026-04-28)", origin: "cua-override" }, - { provider: "yutori", model: "n1-latest", name: "Yutori Navigator n1", origin: "cua-override" }, - { provider: "yutori", model: "n1-20260203", name: "Yutori Navigator n1 (2026-02-03)", origin: "cua-override" }, - ], -}; - -export function listSupportedModels(provider?: ProviderId): SupportedModel[] { - const providers = provider ? [provider] : SUPPORTED_PROVIDERS; - const byKey = new Map(); - - for (const p of providers) { - for (const entry of CUA_MODEL_OVERRIDES[p]) { - byKey.set(modelKey(entry.provider, entry.model), entry); - } - - for (const model of getModels(piProviderFor(p) as never) as Model[]) { - if (!supportsCuaProvider(p, model.id)) continue; - const key = modelKey(p, model.id); - if (byKey.has(key)) continue; - byKey.set(key, { - provider: p, - model: model.id, - name: model.name, - origin: "pi-ai-registry", - default: model.id === DEFAULT_MODEL_ID, - }); - } - } - - return [...byKey.values()].sort(compareSupportedModels); -} - -export function resolveProvider(modelId: string): ProviderId { - const match = listSupportedModels().find((model) => model.model === modelId); - if (!match) { - throw new Error(`unsupported model "${modelId}" (run \`cua models\` to list supported -m/--model values)`); - } - return match.provider; -} - -export function loadModel(modelId: string): { provider: ProviderId; model: Model } { - const provider = resolveProvider(modelId); - const piProvider = piProviderFor(provider); - const fromRegistry = getModel(piProvider as never, modelId as never) as Model | undefined; - if (fromRegistry) return { provider, model: fromRegistry }; - - // Provider model lists can expose working model IDs before pi-ai's generated - // registry catches up. Use conservative metadata so the request can still - // reach the provider; cost telemetry stays zero until the registry updates. - return { provider, model: dynamicModel(provider, modelId) }; -} - -export function piProviderFor(provider: ProviderId): string { - switch (provider) { - case "openai": - return "openai"; - case "anthropic": - return "anthropic"; - case "gemini": - return "google"; - case "tzafon": - return "tzafon"; - case "yutori": - return "yutori"; - } -} - -function supportsCuaProvider(provider: ProviderId, modelId: string): boolean { - const id = modelId.toLowerCase(); - switch (provider) { - case "openai": - return /^gpt-5\.(4|5)(?:-|$)/.test(id); - case "anthropic": - return ( - id.startsWith("claude-3-7-sonnet") || - id.startsWith("claude-opus-4") || - id.startsWith("claude-sonnet-4") || - id.startsWith("claude-haiku-4") - ); - case "gemini": - return id === "gemini-3-flash-preview" || id === "gemini-2.5-computer-use-preview-10-2025"; - case "tzafon": - return id === "tzafon.northstar-cua-fast"; - case "yutori": - return id === "n1-latest" || id === "n1-20260203" || id === "n1.5-latest" || id === "n1.5-20260428"; - } -} - -function dynamicModel(provider: ProviderId, modelId: string): Model { - const piProvider = piProviderFor(provider); - switch (provider) { - case "anthropic": - return { - id: modelId, - name: modelId, - api: "anthropic-messages", - provider: piProvider, - baseUrl: "https://api.anthropic.com", - reasoning: true, - input: ["text", "image"], - cost: zeroCost(), - contextWindow: 200_000, - maxTokens: 64_000, - }; - case "gemini": - return { - id: modelId, - name: modelId, - api: "google-generative-ai", - provider: piProvider, - baseUrl: "https://generativelanguage.googleapis.com/v1beta", - reasoning: true, - input: ["text", "image"], - cost: zeroCost(), - contextWindow: 1_048_576, - maxTokens: 65_536, - }; - case "tzafon": - return { - id: modelId, - name: modelId, - api: "tzafon-responses", - provider: piProvider, - baseUrl: "https://api.lightcone.ai", - reasoning: false, - input: ["text", "image"], - cost: zeroCost(), - contextWindow: 128_000, - maxTokens: 4_096, - }; - case "yutori": - return { - id: modelId, - name: modelId, - api: "yutori-chat-completions", - provider: piProvider, - baseUrl: "https://api.yutori.com/v1", - reasoning: false, - input: ["text", "image"], - cost: zeroCost(), - contextWindow: 128_000, - maxTokens: 4_096, - }; - case "openai": - default: - return { - id: modelId, - name: modelId, - api: "openai-responses", - provider: piProvider, - baseUrl: "https://api.openai.com/v1", - reasoning: true, - input: ["text", "image"], - cost: zeroCost(), - contextWindow: 400_000, - maxTokens: 32_768, - }; - } -} - -function zeroCost(): Model["cost"] { - return { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }; -} - -function modelKey(provider: ProviderId, model: string): string { - return `${provider}:${model}`; -} - -function compareSupportedModels(a: SupportedModel, b: SupportedModel): number { - if (a.provider !== b.provider) return SUPPORTED_PROVIDERS.indexOf(a.provider) - SUPPORTED_PROVIDERS.indexOf(b.provider); - if (a.default !== b.default) return a.default ? -1 : 1; - return a.model.localeCompare(b.model); -} diff --git a/packages/cua-cli/src/named-sessions.ts b/packages/cua-cli/src/named-sessions.ts deleted file mode 100644 index 576d9fb2..00000000 --- a/packages/cua-cli/src/named-sessions.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { browserSession, type BrowserSession } from "@onkernel/cua-translator"; -import { mkdir, readdir, readFile, stat, unlink, writeFile } from "node:fs/promises"; -import { homedir } from "node:os"; -import { join } from "node:path"; -import type { Config } from "./config"; - -/** - * 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 browser itself lives server-side on Kernel. We don't run a local - * daemon — when the slug is referenced again we just call - * `client.browsers.retrieve(...)` to verify the session is still alive - * and then use its id directly. - */ - -export interface NamedSessionMetadata { - name: string; - kernel_session_id: string; - live_url?: string; - profile_id?: string; - transcript_path?: string; - config_profile?: 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); - if (!(await fileExists(path))) return false; - await unlink(path); - return true; -} - -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")) continue; - try { - const raw = await readFile(join(dir, entry), "utf8"); - out.push(JSON.parse(raw) as NamedSessionMetadata); - } catch { - // skip unreadable / malformed entries - } - } - out.sort((a, b) => b.created_at - a.created_at); - return out; -} - -/** - * Provision a fresh Kernel browser session and persist a named-session - * metadata file pointing at it. The transcript path is recorded later - * by the caller (after `openSession` decides where to write). - */ -export async function startNamedSession(args: { - name: string; - cfg: Config; - configProfile?: string; - browserProfile?: string; - browserTimeoutSeconds?: number; - saveProfileChanges?: boolean; -}): Promise<{ meta: NamedSessionMetadata; metadataPath: string; browser: BrowserSession }> { - validateSlug(args.name); - const existing = await readNamedSession(args.name); - if (existing) { - throw new Error( - `named session "${args.name}" already exists (kernel_session_id=${existing.kernel_session_id}). Run \`cua session stop ${args.name}\` first.`, - ); - } - - const browser = await browserSession.open({ - apiKey: args.cfg.kernelApiKey, - baseUrl: args.cfg.kernelBaseUrl || undefined, - timeoutSeconds: args.browserTimeoutSeconds, - profileSelector: args.browserProfile, - saveChanges: args.saveProfileChanges, - }); - - const meta: NamedSessionMetadata = { - name: args.name, - kernel_session_id: browser.sessionId, - live_url: browser.liveUrl, - profile_id: browser.profileId, - config_profile: args.configProfile, - created_at: Date.now(), - }; - const metadataPath = await writeNamedSession(meta); - return { meta, metadataPath, browser }; -} - -/** - * Attach to a previously-started named session. Performs a Kernel - * liveness check (`client.browsers.retrieve`) before returning so the - * caller can fail fast with a clear error if the server-side session has - * timed out or been deleted. - */ -export async function attachNamedSession(args: { - name: string; - cfg: Config; -}): Promise<{ meta: NamedSessionMetadata; browser: BrowserSession }> { - const meta = await readNamedSession(args.name); - if (!meta) { - throw new Error( - `unknown named session "${args.name}". Run \`cua session list\` to see available sessions, or \`cua session start ${args.name}\` to create one.`, - ); - } - - const tentative = browserSession.attach({ - apiKey: args.cfg.kernelApiKey, - baseUrl: args.cfg.kernelBaseUrl || undefined, - sessionId: meta.kernel_session_id, - liveUrl: meta.live_url, - profileId: meta.profile_id, - }); - - let alive = false; - let liveError: Error | undefined; - try { - const fresh = await tentative.client.browsers.retrieve(meta.kernel_session_id); - const deletedAt = (fresh as { deleted_at?: unknown } | undefined)?.deleted_at; - alive = !deletedAt; - } catch (err) { - liveError = err instanceof Error ? err : new Error(String(err)); - const status = (liveError as { status?: unknown }).status; - if (status === 404) { - alive = false; - } else { - throw new Error( - `liveness check for named session "${args.name}" failed: ${liveError.message}`, - { cause: liveError }, - ); - } - } - - if (!alive) { - throw new Error( - `named session "${args.name}" is no longer alive on Kernel (browser timed out or was deleted). Run \`cua session stop ${args.name} && cua session start ${args.name}\` to provision a fresh one.`, - ); - } - - return { meta, browser: tentative }; -} - -/** - * Tear down a named session: delete the Kernel browser server-side and - * remove the local metadata file. Returns true if the metadata existed. - */ -export async function stopNamedSession(args: { - name: string; - cfg: Config; -}): Promise<{ existed: boolean; kernelDeleted: boolean }> { - const meta = await readNamedSession(args.name); - if (!meta) return { existed: false, kernelDeleted: false }; - - const handle = browserSession.attach({ - apiKey: args.cfg.kernelApiKey, - baseUrl: args.cfg.kernelBaseUrl || undefined, - sessionId: meta.kernel_session_id, - }); - - let kernelDeleted = false; - try { - await handle.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 "${args.name}": ${(err as Error).message}`, - { cause: err }, - ); - } - // 404 → server-side session already gone, that's fine. - } - - await deleteNamedSession(args.name); - return { existed: true, kernelDeleted }; -} - -/** - * Update the persisted `transcript_path` field on a named session, called - * after `openSession` resolves where it will actually write the JSONL. - */ -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); -} - -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/cua-cli/src/output/jsonl.ts b/packages/cua-cli/src/output/jsonl.ts deleted file mode 100644 index b0fe5e34..00000000 --- a/packages/cua-cli/src/output/jsonl.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { Agent, AgentEvent } from "@mariozechner/pi-agent-core"; -import type { BrowserSession } from "@onkernel/cua-translator"; - -export interface JsonlSinkOptions { - browser: BrowserSession; - modelId: string; - provider: string; - /** Where to write each line (default: process.stdout). */ - write?: (line: string) => void; - /** When false (default), assistant_text_delta events are suppressed. */ - includeDeltas?: boolean; - /** When true, include base64 image content for screenshots in tool_result events. Default false. */ - includeImages?: boolean; -} - -interface JsonlEventBase { - type: string; - ts: number; -} - -/** - * Subscribe to an agent's lifecycle and emit one JSON object per line for - * downstream tooling. The schema is intentionally compact and stable so - * shell scripts can consume it without extra transformation. - */ -export function attachJsonlSink(agent: Agent, 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", - model: opts.modelId, - provider: opts.provider, - ts: Date.now(), - }); - emit({ - type: "browser_created", - browser_session_id: opts.browser.sessionId, - live_url: opts.browser.liveUrl, - profile_id: opts.browser.profileId, - ts: Date.now(), - }); - - let turn = 0; - const includeDeltas = opts.includeDeltas === true; - const includeImages = opts.includeImages === true; - - return agent.subscribe((event: AgentEvent) => { - 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() }); - } - 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; - } - }); -} - -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/cua-cli/src/sessions.ts b/packages/cua-cli/src/sessions.ts deleted file mode 100644 index 08307675..00000000 --- a/packages/cua-cli/src/sessions.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { Agent, AgentMessage } from "@mariozechner/pi-agent-core"; -import { SessionManager, type SessionInfo } from "@mariozechner/pi-coding-agent"; -import type { BrowserSession } from "@onkernel/cua-translator"; -import { homedir } from "node:os"; -import { join } from "node:path"; - -/** Custom entry types we persist alongside the LLM transcript. */ -export const CUA_BROWSER_ENTRY = "cua-browser"; - -export interface CuaBrowserMetadata { - sessionId: string; - liveUrl?: string; - profileId?: string; - createdAt: number; -} - -/** Resolve the default sessions directory: $XDG_DATA_HOME/cua/sessions or ~/.local/share/cua/sessions. */ -export function defaultSessionDir(): string { - const xdg = process.env.XDG_DATA_HOME; - if (xdg) return join(xdg, "cua", "sessions"); - return join(homedir(), ".local", "share", "cua", "sessions"); -} - -export interface OpenSessionOptions { - cwd: string; - /** Override the directory where session files live. */ - sessionDir?: string; - /** Resolved path of an existing session to attach to. */ - sessionPath?: string; - /** Skip persistence entirely (in-memory only). */ - ephemeral?: boolean; -} - -/** - * Open a SessionManager for the requested mode. - * - * ephemeral=true → in-memory, never touches disk. - * sessionPath set → attach to an existing session file. - * default → create a fresh session in the resolved sessionDir. - */ -export function openSession(opts: OpenSessionOptions): SessionManager { - if (opts.ephemeral) { - return SessionManager.inMemory(opts.cwd); - } - const dir = opts.sessionDir ?? defaultSessionDir(); - const sm = SessionManager.create(opts.cwd, dir); - if (opts.sessionPath) { - sm.setSessionFile(opts.sessionPath); - } - return sm; -} - -/** List sessions for a cwd from the resolved sessions directory. */ -export function listSessions(cwd: string, sessionDir?: string): Promise { - const dir = sessionDir ?? defaultSessionDir(); - return SessionManager.list(cwd, dir); -} - -/** - * Resolve a session reference. Accepts: - * - absolute or relative path to an existing session file - * - `latest` -> most-recent session for cwd - * - any other string -> matched as a prefix against session ids - */ -export async function resolveSessionPath(input: string, cwd: string, sessionDir?: string): Promise { - const trimmed = input.trim(); - if (!trimmed) throw new Error("session reference is empty"); - if (trimmed.includes("/") || trimmed.endsWith(".jsonl")) return trimmed; - const sessions = await listSessions(cwd, sessionDir); - if (sessions.length === 0) throw new Error("no sessions found"); - if (trimmed === "latest") { - const latest = [...sessions].sort((a, b) => b.modified.getTime() - a.modified.getTime())[0]; - return latest!.path; - } - 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]!.path; -} - -/** Find the most recent session for cwd, or undefined when none exists. */ -export async function findLatestSession(cwd: string, sessionDir?: string): Promise { - const sessions = await listSessions(cwd, sessionDir); - if (sessions.length === 0) return undefined; - return [...sessions].sort((a, b) => b.modified.getTime() - a.modified.getTime())[0]; -} - -/** - * Subscribe to the agent and persist user/assistant/toolResult messages - * to the SessionManager as they complete. Returns an unsubscribe. - */ -export function persistAgentEvents(agent: Agent, sm: SessionManager): () => void { - return agent.subscribe((event) => { - if (event.type !== "message_end") return; - const msg = event.message as AgentMessage; - if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "toolResult") return; - try { - sm.appendMessage(msg as never); - } catch { - // ignore single-message persistence failures - } - }); -} - -/** Replay a previously persisted session into a fresh agent's transcript. */ -export function seedAgentFromSession(agent: Agent, sm: SessionManager): void { - const ctx = sm.buildSessionContext(); - if (ctx.messages.length === 0) return; - agent.state.messages = ctx.messages; -} - -/** Record browser metadata so resume can show what the previous session ran against. */ -export function appendBrowserMetadata(sm: SessionManager, browser: BrowserSession): void { - const meta: CuaBrowserMetadata = { - sessionId: browser.sessionId, - liveUrl: browser.liveUrl, - profileId: browser.profileId, - createdAt: Date.now(), - }; - try { - sm.appendCustomEntry(CUA_BROWSER_ENTRY, meta); - } catch { - // non-fatal - } -} - -export type { SessionInfo }; diff --git a/packages/cua-cli/src/skills.ts b/packages/cua-cli/src/skills.ts deleted file mode 100644 index ad73f4a2..00000000 --- a/packages/cua-cli/src/skills.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { - formatSkillsForPrompt, - loadProjectContextFiles, - loadSkills, - type ResourceDiagnostic, - type Skill, -} from "@mariozechner/pi-coding-agent"; -import { existsSync, readFileSync } from "node:fs"; -import { homedir } from "node:os"; -import { join } from "node:path"; - -export interface DiscoverSkillsOptions { - cwd: string; - /** Extra explicit skill paths (files or directories) from --skill / --skill-dir flags. */ - extraPaths?: string[]; - /** Disable all skill discovery. */ - disabled?: boolean; -} - -export interface StartupContextFile { - path: string; - content: string; -} - -export interface StartupResources { - contextFiles: StartupContextFile[]; - skills: Skill[]; - skillDiagnostics: ResourceDiagnostic[]; -} - -/** - * Discover skills following the cross-agent `~/.agents/skills/` standard. - * - * Discovery order (first wins on name collision): - * 1. Explicit paths from `extraPaths` (`--skill ` flags). - * 2. `~/.agents/skills/` (user-global, shared across any agent that - * respects the standard). - * 3. `/.agents/skills/` (project-local). - * - * Skills' frontmatter `description` and `location` are added to the - * system prompt; the model uses the `read` tool to load a skill's body - * when the description matches the task. Use `/skill:` in a - * prompt to force-load a skill body inline. - */ -export function discoverCuaSkills(opts: DiscoverSkillsOptions): { - skills: Skill[]; - sources: string[]; - diagnostics: ResourceDiagnostic[]; -} { - if (opts.disabled) return { skills: [], sources: [], diagnostics: [] }; - - const userAgentsDir = join(homedir(), ".agents", "skills"); - const projectAgentsDir = join(opts.cwd, ".agents", "skills"); - const extras = opts.extraPaths?.filter((p) => p && p.trim().length > 0) ?? []; - - const skillPaths: string[] = []; - for (const p of [...extras, userAgentsDir, projectAgentsDir]) { - if (existsSync(p)) skillPaths.push(p); - } - - // `includeDefaults: false` because pi-coding-agent's defaults are - // `~/.pi/agent/skills/` etc., which are pi-specific. cua follows the - // emerging cross-agent `~/.agents/skills/` standard instead and - // passes those paths explicitly above. - const result = loadSkills({ - cwd: opts.cwd, - skillPaths, - includeDefaults: false, - }); - - return { skills: result.skills, sources: skillPaths, diagnostics: result.diagnostics }; -} - -export function discoverStartupResources(opts: DiscoverSkillsOptions): StartupResources { - const skills = discoverCuaSkills(opts); - return { - contextFiles: loadProjectContextFiles({ cwd: opts.cwd }), - skills: skills.skills, - skillDiagnostics: skills.diagnostics, - }; -} - -/** Append the rendered skills block to a system prompt, if any skills are present. */ -export function appendSkillsToSystemPrompt(systemPrompt: string, skills: Skill[]): string { - if (skills.length === 0) return systemPrompt; - const formatted = formatSkillsForPrompt(skills); - if (!formatted.trim()) return systemPrompt; - return systemPrompt.trim() + "\n\n" + formatted.trim() + "\n"; -} - -/** - * Resolve a `/skill:` invocation into the full skill body so the - * model gets the explicit instructions on the next turn. The text - * supplied by the user (the part after the skill name, if any) is - * appended as additional context. - */ -export function expandSkillInvocation(text: string, skills: Skill[]): { expanded: string; skill?: Skill } { - const trimmed = text.trim(); - const match = trimmed.match(/^\/skill:([A-Za-z0-9_\-.]+)\s*(.*)$/); - if (!match) return { expanded: text }; - const [, name, rest] = match; - const skill = skills.find((s) => s.name === name); - if (!skill) { - return { - expanded: `(no skill named "${name}" was found; pretending the user typed: ${rest || "(empty)"})\n\n${rest}`, - }; - } - const remainder = (rest ?? "").trim(); - const body = - `Use the following skill ("${skill.name}"): ${skill.description}\n\n` + - `${skill.filePath}:\n\n---\n${tryReadSkillBody(skill.filePath)}\n---\n` + - (remainder ? `\nUser request:\n${remainder}` : ""); - return { expanded: body, skill }; -} - -function tryReadSkillBody(path: string): string { - try { - return readFileSync(path, "utf8"); - } catch (err) { - return `(failed to read skill at ${path}: ${(err as Error).message})`; - } -} - -export type { Skill }; diff --git a/packages/cua-cli/tsconfig.json b/packages/cua-cli/tsconfig.json index 5f205254..95f9f63f 100644 --- a/packages/cua-cli/tsconfig.json +++ b/packages/cua-cli/tsconfig.json @@ -6,12 +6,6 @@ }, "include": ["src/**/*"], "references": [ - { "path": "../cua-translator" }, - { "path": "../cua-openai" }, - { "path": "../cua-anthropic" }, - { "path": "../cua-gemini" }, - { "path": "../cua-tzafon" }, - { "path": "../cua-yutori" }, { "path": "../ptywright" } ] } From d9aa32c634bc8ff1d5f88b6ee41640215f3f2018 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 12 Jun 2026 05:45:17 +0000 Subject: [PATCH 2/2] Drop `cua config init` from root README quickstart The subcommand was removed in this PR; the documented command would now fall through to interactive mode and start a TUI run with the literal prompt "config init". --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 20e2928b..578f3d89 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,7 @@ mkdir -p ~/.local/bin ln -s "$(pwd)/bin/cua" ~/.local/bin/cua # make sure ~/.local/bin is on $PATH (most distros already do) -# either set up a config file... -cua config init # interactive: profile name + API keys - -# ...or rely on env vars +# set API keys via env vars export OPENAI_API_KEY=sk-... # for gpt-5.5 export ANTHROPIC_API_KEY=sk-ant-... # for claude-opus-4-7 export GOOGLE_API_KEY=... # for gemini-3-flash-preview