Compare commits
32 Commits
feat/multi
...
feat/claud
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6ea3f4aff | ||
|
|
96b5811dc1 | ||
|
|
567f5075c3 | ||
|
|
5e25f55bc7 | ||
|
|
77a2ab7bdf | ||
|
|
ee3d88af9d | ||
|
|
89dc302403 | ||
|
|
5137df72d8 | ||
|
|
dd70ce37f0 | ||
|
|
7e0a1a133c | ||
|
|
be606cdfbe | ||
|
|
6a29a373f4 | ||
|
|
389625cb20 | ||
|
|
e916d564a9 | ||
|
|
3d8f390b9e | ||
|
|
a61f8bb853 | ||
|
|
c8c99445ea | ||
|
|
fc41a389c5 | ||
|
|
39d94a4af6 | ||
|
|
acf4c46439 | ||
|
|
5cbf7828f0 | ||
|
|
0efd1b65bb | ||
|
|
f8d2bd55b9 | ||
|
|
1ef8d73ce5 | ||
|
|
2b7524b1cb | ||
|
|
d6b0e564bf | ||
|
|
6897761b21 | ||
|
|
fe66b68baa | ||
|
|
a7f794c7a3 | ||
|
|
85690b69a8 | ||
|
|
8c2dcb75cb | ||
|
|
1ef5c17c35 |
75
bun.lock
75
bun.lock
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "oh-my-opencode",
|
||||
@@ -10,8 +10,8 @@
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@code-yeongyu/comment-checker": "^0.7.0",
|
||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||
"@opencode-ai/plugin": "^1.1.19",
|
||||
"@opencode-ai/sdk": "^1.1.19",
|
||||
"@opencode-ai/plugin": "^1.2.16",
|
||||
"@opencode-ai/sdk": "^1.2.17",
|
||||
"commander": "^14.0.2",
|
||||
"detect-libc": "^2.0.0",
|
||||
"diff": "^8.0.3",
|
||||
@@ -48,42 +48,45 @@
|
||||
"@ast-grep/napi",
|
||||
"@code-yeongyu/comment-checker",
|
||||
],
|
||||
"overrides": {
|
||||
"@opencode-ai/sdk": "^1.2.17",
|
||||
},
|
||||
"packages": {
|
||||
"@ast-grep/cli": ["@ast-grep/cli@0.40.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.40.0", "@ast-grep/cli-darwin-x64": "0.40.0", "@ast-grep/cli-linux-arm64-gnu": "0.40.0", "@ast-grep/cli-linux-x64-gnu": "0.40.0", "@ast-grep/cli-win32-arm64-msvc": "0.40.0", "@ast-grep/cli-win32-ia32-msvc": "0.40.0", "@ast-grep/cli-win32-x64-msvc": "0.40.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-L8AkflsfI2ZP70yIdrwqvjR02ScCuRmM/qNGnJWUkOFck+e6gafNVJ4e4jjGQlEul+dNdBpx36+O2Op629t47A=="],
|
||||
"@ast-grep/cli": ["@ast-grep/cli@0.40.5", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.40.5", "@ast-grep/cli-darwin-x64": "0.40.5", "@ast-grep/cli-linux-arm64-gnu": "0.40.5", "@ast-grep/cli-linux-x64-gnu": "0.40.5", "@ast-grep/cli-win32-arm64-msvc": "0.40.5", "@ast-grep/cli-win32-ia32-msvc": "0.40.5", "@ast-grep/cli-win32-x64-msvc": "0.40.5" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-yVXL7Gz0WIHerQLf+MVaVSkhIhidtWReG5akNVr/JS9OVCVkSdz7gWm7H8jVv2M9OO1tauuG76K3UaRGBPu5lQ=="],
|
||||
|
||||
"@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UehY2MMUkdJbsriP7NKc6+uojrqPn7d1Cl0em+WAkee7Eij81VdyIjRsRxtZSLh440ZWQBHI3PALZ9RkOO8pKQ=="],
|
||||
"@ast-grep/cli-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.40.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-T9CzwJ1GqQhnANdsu6c7iT1akpvTVMK+AZrxnhIPv33Ze5hrXUUkqan+j4wUAukRJDqU7u94EhXLSLD+5tcJ8g=="],
|
||||
|
||||
"@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-RFDJ2ZxUbT0+grntNlOLJx7wa9/ciVCeaVtQpQy8WJJTvXvkY0etl8Qlh2TmO2x2yr+i0Z6aMJi4IG/Yx5ghTQ=="],
|
||||
"@ast-grep/cli-darwin-x64": ["@ast-grep/cli-darwin-x64@0.40.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-ez9b2zKvXU8f4ghhjlqYvbx6tWCKJTuVlNVqDDfjqwwhGeiTYfnzMlSVat4ElYRMd21gLtXZIMy055v2f21Ztg=="],
|
||||
|
||||
"@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4p55gnTQ1mMFCyqjtM7bH9SB9r16mkwXtUcJQGX1YgFG4WD+QG8rC4GwSuNNZcdlYaOQuTWrgUEQ9z5K06UXfg=="],
|
||||
"@ast-grep/cli-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-VXa2L1IEYD66AMb0GuG7VlMMbPmEGoJUySWDcwSZo/D9neiry3MJ41LQR5oTG2HyhIPBsf9umrXnmuRq66BviA=="],
|
||||
|
||||
"@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-u2MXFceuwvrO+OQ6zFGoJ6wbATXn46HWwW79j4UPrXYJzVl97jRyjJOIQTJOzTflsk02fjP98DQkfvbXt2dl3Q=="],
|
||||
"@ast-grep/cli-linux-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-GQC5162eIOWXR2eQQ6Knzg7/8Trp5E1ODJkaErf0IubdQrZBGqj5AAcQPcWgPbbnmktjIp0H4NraPpOJ9eJ22A=="],
|
||||
|
||||
"@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-E/I1xpF/RQL2fo1CQsQfTxyDLnChsbZ+ERrQHKuF1FI4WrkaPOBibpqda60QgVmUcgOGZyZ/GRb3iKEVWPsQNQ=="],
|
||||
"@ast-grep/cli-win32-arm64-msvc": ["@ast-grep/cli-win32-arm64-msvc@0.40.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-YiZdnQZsSlXQTMsZJop/Ux9MmUGfuRvC2x/UbFgrt5OBSYxND+yoiMc0WcA3WG+wU+tt4ZkB5HUea3r/IkOLYA=="],
|
||||
|
||||
"@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-9h12OQu1BR0GxHEtT+Z4QkJk3LLWLiKwjBkjXUGlASHYDPTyLcs85KwDLeFHs4BwarF8TDdF+KySvB9WPGl/nQ=="],
|
||||
"@ast-grep/cli-win32-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.40.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-MHkCxCITVTr8sY9CcVqNKbfUzMa3Hc6IilGXad0Clnw2vNmPfWqSky+hU/UTerr5YHWwWfAVURH7ANZgirtx0Q=="],
|
||||
|
||||
"@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-n2+3WynEWFHhXg6KDgjwWQ0UEtIvqUITFbKEk5cDkUYrzYhg/A6kj0qauPwRbVMoJms49vtsNpLkzzqyunio5g=="],
|
||||
"@ast-grep/cli-win32-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.40.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/MJ5un7yxlClaaxou9eYl+Kr2xr/yTtYtTq5aLBWjPWA6dmmJ1nAJgx5zKHVuplFXFBrFDQk3paEgAETMTGcrA=="],
|
||||
|
||||
"@ast-grep/napi": ["@ast-grep/napi@0.40.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.0", "@ast-grep/napi-darwin-x64": "0.40.0", "@ast-grep/napi-linux-arm64-gnu": "0.40.0", "@ast-grep/napi-linux-arm64-musl": "0.40.0", "@ast-grep/napi-linux-x64-gnu": "0.40.0", "@ast-grep/napi-linux-x64-musl": "0.40.0", "@ast-grep/napi-win32-arm64-msvc": "0.40.0", "@ast-grep/napi-win32-ia32-msvc": "0.40.0", "@ast-grep/napi-win32-x64-msvc": "0.40.0" } }, "sha512-tq6nO/8KwUF/mHuk1ECaAOSOlz2OB/PmygnvprJzyAHGRVzdcffblaOOWe90M9sGz5MAasXoF+PTcayQj9TKKA=="],
|
||||
"@ast-grep/napi": ["@ast-grep/napi@0.40.5", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.40.5", "@ast-grep/napi-darwin-x64": "0.40.5", "@ast-grep/napi-linux-arm64-gnu": "0.40.5", "@ast-grep/napi-linux-arm64-musl": "0.40.5", "@ast-grep/napi-linux-x64-gnu": "0.40.5", "@ast-grep/napi-linux-x64-musl": "0.40.5", "@ast-grep/napi-win32-arm64-msvc": "0.40.5", "@ast-grep/napi-win32-ia32-msvc": "0.40.5", "@ast-grep/napi-win32-x64-msvc": "0.40.5" } }, "sha512-hJA62OeBKUQT68DD2gDyhOqJxZxycqg8wLxbqjgqSzYttCMSDL9tiAQ9abgekBYNHudbJosm9sWOEbmCDfpX2A=="],
|
||||
|
||||
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZMjl5yLhKjxdwbqEEdMizgQdWH2NrWsM6Px+JuGErgCDe6Aedq9yurEPV7veybGdLVJQhOah6htlSflXxjHnYA=="],
|
||||
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2F072fGN0WTq7KI3okuEnkGJVEHLbi56Bw1H6NAMf7j2mJJeQWsRyGOMcyNnUXZDeNdvoMH0OB2a5wwUegY/nQ=="],
|
||||
|
||||
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-f9Ol5oQKNRMBkvDtzBK1WiNn2/3eejF2Pn9xwTj7PhXuSFseedOspPYllxQo0gbwUlw/DJqGFTce/jarhR/rBw=="],
|
||||
"@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.40.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-dJMidHZhhxuLBYNi6/FKI812jQ7wcFPSKkVPwviez2D+KvYagapUMAV/4dJ7FCORfguVk8Y0jpPAlYmWRT5nvA=="],
|
||||
|
||||
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tO+VW5GDhT9jGkKOK+3b8+ohKjC98WTzn7wSskd/myyhK3oYL1WTKqCm07WSYBZOJvb3z+WaX+wOUrc4bvtyQ=="],
|
||||
"@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-nBRCbyoS87uqkaw4Oyfe5VO+SRm2B+0g0T8ME69Qry9ShMf41a2bTdpcQx9e8scZPogq+CTwDHo3THyBV71l9w=="],
|
||||
|
||||
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-MS9qalLRjUnF2PCzuTKTvCMVSORYHxxe3Qa0+SSaVULsXRBmuy5C/b1FeWwMFnwNnC0uie3VDet31Zujwi8q6A=="],
|
||||
"@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-/qKsmds5FMoaEj6FdNzepbmLMtlFuBLdrAn9GIWCqOIcVcYvM1Nka8+mncfeXB/MFZKOrzQsQdPTWqrrQzXLrA=="],
|
||||
|
||||
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BeHZVMNXhM3WV3XE2yghO0fRxhMOt8BTN972p5piYEQUvKeSHmS8oeGcs6Ahgx5znBclqqqq37ZfioYANiTqJA=="],
|
||||
"@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-DP4oDbq7f/1A2hRTFLhJfDFR6aI5mRWdEfKfHzRItmlKsR9WlcEl1qDJs/zX9R2EEtIDsSKRzuJNfJllY3/W8Q=="],
|
||||
|
||||
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rG1YujF7O+lszX8fd5u6qkFTuv4FwHXjWvt1CCvCxXwQLSY96LaCW88oVKg7WoEYQh54y++Fk57F+Wh9Gv9nVQ=="],
|
||||
"@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-BRZUvVBPUNpWPo6Ns8chXVzxHPY+k9gpsubGTHy92Q26ecZULd/dTkWWdnvfhRqttsSQ9Pe/XQdi5+hDQ6RYcg=="],
|
||||
|
||||
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-9SqmnQqd4zTEUk6yx0TuW2ycZZs2+e569O/R0QnhSiQNpgwiJCYOe/yPS0BC9HkiaozQm6jjAcasWpFtz/dp+w=="],
|
||||
"@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.40.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-y95zSEwc7vhxmcrcH0GnK4ZHEBQrmrszRBNQovzaciF9GUqEcCACNLoBesn4V47IaOp4fYgD2/EhGRTIBFb2Ug=="],
|
||||
|
||||
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0JkdBZi5l9vZhGEO38A1way0LmLRDU5Vos6MXrLIOVkymmzDTDlCdY394J1LMmmsfwWcyJg6J7Yv2dw41MCxDQ=="],
|
||||
"@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.40.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-K/u8De62iUnFCzVUs7FBdTZ2Jrgc5/DLHqjpup66KxZ7GIM9/HGME/O8aSoPkpcAeCD4TiTZ11C1i5p5H98hTg=="],
|
||||
|
||||
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Hk2IwfPqMFGZt5SRxsoWmGLxBXxprow4LRp1eG6V8EEiJCNHxZ9ZiEaIc5bNvMDBjHVSnqZAXT22dROhrcSKQg=="],
|
||||
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.5", "", { "os": "win32", "cpu": "x64" }, "sha512-dqm5zg/o4Nh4VOQPEpMS23ot8HVd22gG0eg01t4CFcZeuzyuSgBlOL3N7xLbz3iH2sVkk7keuBwAzOIpTqziNQ=="],
|
||||
|
||||
"@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
|
||||
|
||||
@@ -91,29 +94,29 @@
|
||||
|
||||
"@code-yeongyu/comment-checker": ["@code-yeongyu/comment-checker@0.7.0", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "comment-checker": "bin/comment-checker" } }, "sha512-AOic1jPHY3CpNraOuO87YZHO3uRzm9eLd0wyYYN89/76Ugk2TfdUYJ6El/Oe8fzOnHKiOF0IfBeWRo0IUjrHHg=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||
"@hono/node-server": ["@hono/node-server@1.19.10", "", { "peerDependencies": { "hono": "^4" } }, "sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="],
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.1.19", "", { "dependencies": { "@opencode-ai/sdk": "1.1.19", "zod": "4.1.8" } }, "sha512-Q6qBEjHb/dJMEw4BUqQxEswTMxCCHUpFMMb6jR8HTTs8X/28XRkKt5pHNPA82GU65IlSoPRph+zd8LReBDN53Q=="],
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.2.16", "", { "dependencies": { "@opencode-ai/sdk": "1.2.16", "zod": "4.1.8" } }, "sha512-9Kb7BQIC2P3oKCvI8K3thP5YP0vE7yLvcmBmgyACUIqc3e5UL6U+4umLpTvgQa2eQdjxtOXznuGTNwgcGMHUHg=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.19", "", {}, "sha512-XhZhFuvlLCqDpvNtUEjOsi/wvFj3YCXb1dySp+OONQRMuHlorNYnNa7P2A2ntKuhRdGT1Xt5na0nFzlUyNw+4A=="],
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.17", "", {}, "sha512-HdeLeyJ2/Yl/NBHqw9pGFBnkIXuf0Id1kX1GMXDcnZwbJROUJ6TtrW/wLngTYW478E4CCm1jwknjxxmDuxzVMQ=="],
|
||||
|
||||
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||
|
||||
"@types/picomatch": ["@types/picomatch@3.0.2", "", {}, "sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="],
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||
|
||||
@@ -123,7 +126,7 @@
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
|
||||
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
@@ -133,7 +136,7 @@
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
@@ -191,11 +194,11 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.12.0", "", {}, "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA=="],
|
||||
"hono": ["hono@4.12.5", "", {}, "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
@@ -275,7 +278,7 @@
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
|
||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
@@ -315,7 +318,7 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
@@ -327,8 +330,10 @@
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
|
||||
"@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@code-yeongyu/comment-checker": "^0.7.0",
|
||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||
"@opencode-ai/plugin": "^1.1.19",
|
||||
"@opencode-ai/sdk": "^1.1.19",
|
||||
"@opencode-ai/plugin": "^1.2.16",
|
||||
"@opencode-ai/sdk": "^1.2.17",
|
||||
"commander": "^14.0.2",
|
||||
"detect-libc": "^2.0.0",
|
||||
"diff": "^8.0.3",
|
||||
@@ -87,6 +87,9 @@
|
||||
"oh-my-opencode-windows-x64": "3.10.0",
|
||||
"oh-my-opencode-windows-x64-baseline": "3.10.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@opencode-ai/sdk": "^1.2.17"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@ast-grep/cli",
|
||||
"@ast-grep/napi",
|
||||
|
||||
315
packages/darwin-arm64/bin/index.js.map
Normal file
315
packages/darwin-arm64/bin/index.js.map
Normal file
File diff suppressed because one or more lines are too long
315
packages/darwin-x64-baseline/bin/index.js.map
Normal file
315
packages/darwin-x64-baseline/bin/index.js.map
Normal file
File diff suppressed because one or more lines are too long
315
packages/darwin-x64/bin/index.js.map
Normal file
315
packages/darwin-x64/bin/index.js.map
Normal file
File diff suppressed because one or more lines are too long
315
packages/linux-arm64-musl/bin/index.js.map
Normal file
315
packages/linux-arm64-musl/bin/index.js.map
Normal file
File diff suppressed because one or more lines are too long
315
packages/linux-arm64/bin/index.js.map
Normal file
315
packages/linux-arm64/bin/index.js.map
Normal file
File diff suppressed because one or more lines are too long
315
packages/linux-x64-baseline/bin/index.js.map
Normal file
315
packages/linux-x64-baseline/bin/index.js.map
Normal file
File diff suppressed because one or more lines are too long
315
packages/linux-x64-musl-baseline/bin/index.js.map
Normal file
315
packages/linux-x64-musl-baseline/bin/index.js.map
Normal file
File diff suppressed because one or more lines are too long
315
packages/linux-x64-musl/bin/index.js.map
Normal file
315
packages/linux-x64-musl/bin/index.js.map
Normal file
File diff suppressed because one or more lines are too long
315
packages/linux-x64/bin/index.js.map
Normal file
315
packages/linux-x64/bin/index.js.map
Normal file
File diff suppressed because one or more lines are too long
315
packages/windows-x64-baseline/bin/index.js.map
Normal file
315
packages/windows-x64-baseline/bin/index.js.map
Normal file
File diff suppressed because one or more lines are too long
315
packages/windows-x64/bin/index.js.map
Normal file
315
packages/windows-x64/bin/index.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -1895,6 +1895,94 @@
|
||||
"created_at": "2026-03-04T00:43:53Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 2277
|
||||
},
|
||||
{
|
||||
"name": "chan1103",
|
||||
"id": 241870013,
|
||||
"comment_id": 3996082243,
|
||||
"created_at": "2026-03-04T08:40:54Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 2288
|
||||
},
|
||||
{
|
||||
"name": "SeeYouCowboi",
|
||||
"id": 103308766,
|
||||
"comment_id": 3996126396,
|
||||
"created_at": "2026-03-04T08:50:32Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 2291
|
||||
},
|
||||
{
|
||||
"name": "guazi04",
|
||||
"id": 134621827,
|
||||
"comment_id": 3996644267,
|
||||
"created_at": "2026-03-04T10:31:44Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 2293
|
||||
},
|
||||
{
|
||||
"name": "brandonwebb-vista",
|
||||
"id": 237281185,
|
||||
"comment_id": 3998901238,
|
||||
"created_at": "2026-03-04T17:07:00Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 2299
|
||||
},
|
||||
{
|
||||
"name": "RaviTharuma",
|
||||
"id": 25951435,
|
||||
"comment_id": 4000536638,
|
||||
"created_at": "2026-03-04T21:53:38Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 2302
|
||||
},
|
||||
{
|
||||
"name": "Romanok2805",
|
||||
"id": 37216910,
|
||||
"comment_id": 4001032410,
|
||||
"created_at": "2026-03-04T23:51:02Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 2306
|
||||
},
|
||||
{
|
||||
"name": "Vacbo",
|
||||
"id": 53411412,
|
||||
"comment_id": 4002083771,
|
||||
"created_at": "2026-03-05T04:19:50Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 2310
|
||||
},
|
||||
{
|
||||
"name": "Wangmerlyn",
|
||||
"id": 29993182,
|
||||
"comment_id": 4004271570,
|
||||
"created_at": "2026-03-05T11:08:09Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 2318
|
||||
},
|
||||
{
|
||||
"name": "mInrOz",
|
||||
"id": 14320143,
|
||||
"comment_id": 4004791744,
|
||||
"created_at": "2026-03-05T12:42:30Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 2321
|
||||
},
|
||||
{
|
||||
"name": "hkc5",
|
||||
"id": 142545736,
|
||||
"comment_id": 4006670642,
|
||||
"created_at": "2026-03-05T17:49:07Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 2327
|
||||
},
|
||||
{
|
||||
"name": "mrosnerr",
|
||||
"id": 3758430,
|
||||
"comment_id": 4006707281,
|
||||
"created_at": "2026-03-05T17:55:33Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 2328
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -83,7 +83,7 @@ exports[`generateModelConfig single native provider uses Claude models when only
|
||||
"variant": "max",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
@@ -145,7 +145,7 @@ exports[`generateModelConfig single native provider uses Claude models with isMa
|
||||
"variant": "max",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
"model": "opencode/glm-4.7-free",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
@@ -212,7 +212,8 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -279,7 +280,8 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "openai/gpt-5.2",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -468,7 +470,8 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -542,7 +545,8 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -596,7 +600,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
"model": "opencode/claude-sonnet-4-5",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
@@ -617,7 +621,8 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "opencode/gemini-3-flash",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/gpt-5.2",
|
||||
@@ -670,7 +675,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
"model": "opencode/claude-sonnet-4-5",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
@@ -691,7 +696,8 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "opencode/gemini-3-flash",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/gpt-5.2",
|
||||
@@ -988,7 +994,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"explore": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
@@ -1009,7 +1015,8 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "opencode/gemini-3-flash",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/gpt-5.2",
|
||||
@@ -1083,7 +1090,8 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -1263,7 +1271,7 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
@@ -1284,7 +1292,8 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "opencode/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
@@ -1337,7 +1346,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"explore": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
@@ -1358,7 +1367,8 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -1411,7 +1421,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"explore": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
@@ -1432,7 +1442,8 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "openai/gpt-5.3-codex",
|
||||
"variant": "medium",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
|
||||
@@ -21,19 +21,9 @@ describe("runCliInstaller", () => {
|
||||
console.error = originalConsoleError
|
||||
})
|
||||
|
||||
it("runs auth and provider setup steps when openai or copilot are enabled without gemini", async () => {
|
||||
it("completes installation without auth plugin or provider config steps", async () => {
|
||||
//#given
|
||||
const addAuthPluginsSpy = spyOn(configManager, "addAuthPlugins").mockResolvedValue({
|
||||
success: true,
|
||||
configPath: "/tmp/opencode.jsonc",
|
||||
})
|
||||
const addProviderConfigSpy = spyOn(configManager, "addProviderConfig").mockReturnValue({
|
||||
success: true,
|
||||
configPath: "/tmp/opencode.jsonc",
|
||||
})
|
||||
const restoreSpies = [
|
||||
addAuthPluginsSpy,
|
||||
addProviderConfigSpy,
|
||||
spyOn(configManager, "detectCurrentConfig").mockReturnValue({
|
||||
isInstalled: false,
|
||||
hasClaude: false,
|
||||
@@ -73,8 +63,6 @@ describe("runCliInstaller", () => {
|
||||
|
||||
//#then
|
||||
expect(result).toBe(0)
|
||||
expect(addAuthPluginsSpy).toHaveBeenCalledTimes(1)
|
||||
expect(addProviderConfigSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
for (const spy of restoreSpies) {
|
||||
spy.mockRestore()
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import color from "picocolors"
|
||||
import type { InstallArgs } from "./types"
|
||||
import {
|
||||
addAuthPlugins,
|
||||
addPluginToOpenCodeConfig,
|
||||
addProviderConfig,
|
||||
detectCurrentConfig,
|
||||
getOpenCodeVersion,
|
||||
isOpenCodeInstalled,
|
||||
@@ -45,7 +43,7 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
|
||||
printHeader(isUpdate)
|
||||
|
||||
const totalSteps = 6
|
||||
const totalSteps = 4
|
||||
let step = 1
|
||||
|
||||
printStep(step++, totalSteps, "Checking OpenCode installation...")
|
||||
@@ -77,28 +75,6 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
`Plugin ${isUpdate ? "verified" : "added"} ${SYMBOLS.arrow} ${color.dim(pluginResult.configPath)}`,
|
||||
)
|
||||
|
||||
const needsProviderSetup = config.hasGemini || config.hasOpenAI || config.hasCopilot
|
||||
|
||||
if (needsProviderSetup) {
|
||||
printStep(step++, totalSteps, "Adding auth plugins...")
|
||||
const authResult = await addAuthPlugins(config)
|
||||
if (!authResult.success) {
|
||||
printError(`Failed: ${authResult.error}`)
|
||||
return 1
|
||||
}
|
||||
printSuccess(`Auth plugins configured ${SYMBOLS.arrow} ${color.dim(authResult.configPath)}`)
|
||||
|
||||
printStep(step++, totalSteps, "Adding provider configurations...")
|
||||
const providerResult = addProviderConfig(config)
|
||||
if (!providerResult.success) {
|
||||
printError(`Failed: ${providerResult.error}`)
|
||||
return 1
|
||||
}
|
||||
printSuccess(`Providers configured ${SYMBOLS.arrow} ${color.dim(providerResult.configPath)}`)
|
||||
} else {
|
||||
step += 2
|
||||
}
|
||||
|
||||
printStep(step++, totalSteps, "Writing oh-my-opencode configuration...")
|
||||
const omoResult = writeOmoConfig(config)
|
||||
if (!omoResult.success) {
|
||||
@@ -156,7 +132,7 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
printBox(
|
||||
`Run ${color.cyan("opencode auth login")} and select your provider:\n` +
|
||||
(config.hasClaude ? ` ${SYMBOLS.bullet} Anthropic ${color.gray("→ Claude Pro/Max")}\n` : "") +
|
||||
(config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\n` : "") +
|
||||
(config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ Gemini")}\n` : "") +
|
||||
(config.hasCopilot ? ` ${SYMBOLS.bullet} GitHub ${color.gray("→ Copilot")}` : ""),
|
||||
"Authenticate Your Providers",
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test, mock, afterEach } from "bun:test"
|
||||
|
||||
import { ANTIGRAVITY_PROVIDER_CONFIG, getPluginNameWithVersion, fetchNpmDistTags, generateOmoConfig } from "./config-manager"
|
||||
import { getPluginNameWithVersion, fetchNpmDistTags, generateOmoConfig } from "./config-manager"
|
||||
import type { InstallConfig } from "./types"
|
||||
|
||||
describe("getPluginNameWithVersion", () => {
|
||||
@@ -169,76 +169,6 @@ describe("fetchNpmDistTags", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
test("all models include full spec (limit + modalities + Antigravity label)", () => {
|
||||
const google = (ANTIGRAVITY_PROVIDER_CONFIG as any).google
|
||||
expect(google).toBeTruthy()
|
||||
|
||||
const models = google.models as Record<string, any>
|
||||
expect(models).toBeTruthy()
|
||||
|
||||
const required = [
|
||||
"antigravity-gemini-3.1-pro",
|
||||
"antigravity-gemini-3-flash",
|
||||
"antigravity-claude-sonnet-4-6",
|
||||
"antigravity-claude-sonnet-4-6-thinking",
|
||||
"antigravity-claude-opus-4-5-thinking",
|
||||
]
|
||||
|
||||
for (const key of required) {
|
||||
const model = models[key]
|
||||
expect(model).toBeTruthy()
|
||||
expect(typeof model.name).toBe("string")
|
||||
expect(model.name.includes("(Antigravity)")).toBe(true)
|
||||
|
||||
expect(model.limit).toBeTruthy()
|
||||
expect(typeof model.limit.context).toBe("number")
|
||||
expect(typeof model.limit.output).toBe("number")
|
||||
|
||||
expect(model.modalities).toBeTruthy()
|
||||
expect(Array.isArray(model.modalities.input)).toBe(true)
|
||||
expect(Array.isArray(model.modalities.output)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("Gemini models have variant definitions", () => {
|
||||
// #given the antigravity provider config
|
||||
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
|
||||
|
||||
// #when checking Gemini Pro variants
|
||||
const pro = models["antigravity-gemini-3.1-pro"]
|
||||
// #then should have low and high variants
|
||||
expect(pro.variants).toBeTruthy()
|
||||
expect(pro.variants.low).toBeTruthy()
|
||||
expect(pro.variants.high).toBeTruthy()
|
||||
|
||||
// #when checking Gemini Flash variants
|
||||
const flash = models["antigravity-gemini-3-flash"]
|
||||
// #then should have minimal, low, medium, high variants
|
||||
expect(flash.variants).toBeTruthy()
|
||||
expect(flash.variants.minimal).toBeTruthy()
|
||||
expect(flash.variants.low).toBeTruthy()
|
||||
expect(flash.variants.medium).toBeTruthy()
|
||||
expect(flash.variants.high).toBeTruthy()
|
||||
})
|
||||
|
||||
test("Claude thinking models have variant definitions", () => {
|
||||
// #given the antigravity provider config
|
||||
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
|
||||
|
||||
// #when checking Claude thinking variants
|
||||
const sonnetThinking = models["antigravity-claude-sonnet-4-6-thinking"]
|
||||
const opusThinking = models["antigravity-claude-opus-4-5-thinking"]
|
||||
|
||||
// #then both should have low and max variants
|
||||
for (const model of [sonnetThinking, opusThinking]) {
|
||||
expect(model.variants).toBeTruthy()
|
||||
expect(model.variants.low).toBeTruthy()
|
||||
expect(model.variants.max).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("generateOmoConfig - model fallback system", () => {
|
||||
test("uses github-copilot sonnet fallback when only copilot available", () => {
|
||||
// #given user has only copilot (no max plan)
|
||||
@@ -323,8 +253,8 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus).toBeUndefined()
|
||||
// #then Oracle should use native OpenAI (first fallback entry)
|
||||
expect((result.agents as Record<string, { model: string }>).oracle.model).toBe("openai/gpt-5.2")
|
||||
// #then multimodal-looker should use native OpenAI (fallback within native tier)
|
||||
expect((result.agents as Record<string, { model: string }>)["multimodal-looker"].model).toBe("openai/gpt-5.2")
|
||||
// #then multimodal-looker should use native OpenAI (first fallback entry is gpt-5.3-codex)
|
||||
expect((result.agents as Record<string, { model: string }>)["multimodal-looker"].model).toBe("openai/gpt-5.3-codex")
|
||||
})
|
||||
|
||||
test("uses haiku for explore when Claude max20", () => {
|
||||
|
||||
@@ -14,9 +14,6 @@ export { writeOmoConfig } from "./config-manager/write-omo-config"
|
||||
|
||||
export { isOpenCodeInstalled, getOpenCodeVersion } from "./config-manager/opencode-binary"
|
||||
|
||||
export { fetchLatestVersion, addAuthPlugins } from "./config-manager/auth-plugins"
|
||||
export { ANTIGRAVITY_PROVIDER_CONFIG } from "./config-manager/antigravity-provider-configuration"
|
||||
export { addProviderConfig } from "./config-manager/add-provider-config"
|
||||
export { detectCurrentConfig } from "./config-manager/detect-current-config"
|
||||
|
||||
export type { BunInstallResult } from "./config-manager/bun-install"
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { modifyProviderInJsonc } from "./jsonc-provider-editor"
|
||||
import { parseJsonc } from "../../shared/jsonc-parser"
|
||||
|
||||
describe("modifyProviderInJsonc", () => {
|
||||
describe("Test 1: Basic JSONC with existing provider", () => {
|
||||
it("replaces provider value, preserves comments and other keys", () => {
|
||||
// given
|
||||
const content = `{
|
||||
// my config
|
||||
"provider": { "openai": {} },
|
||||
"plugin": ["foo"]
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(result).toContain('"google"')
|
||||
expect(result).toContain('"plugin": ["foo"]')
|
||||
expect(result).toContain('// my config')
|
||||
|
||||
// Post-write validation
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('plugin')
|
||||
expect(parsed).toHaveProperty('provider')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 2: Comment containing '}' inside provider block", () => {
|
||||
it("must NOT corrupt file", () => {
|
||||
// given
|
||||
const content = `{
|
||||
"provider": {
|
||||
// } this brace should be ignored
|
||||
"openai": {}
|
||||
},
|
||||
"other": 1
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(result).toContain('"other"')
|
||||
|
||||
// Post-write validation
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('other')
|
||||
expect(parsed.other).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 3: Comment containing '\"provider\"' before real key", () => {
|
||||
it("must NOT match wrong location", () => {
|
||||
// given
|
||||
const content = `{
|
||||
// "provider": { "example": true }
|
||||
"provider": { "openai": {} },
|
||||
"other": 1
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(result).toContain('"other"')
|
||||
|
||||
// Post-write validation
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('other')
|
||||
expect(parsed.other).toBe(1)
|
||||
expect(parsed.provider).toHaveProperty('google')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 4: Comment containing '{' inside provider", () => {
|
||||
it("must NOT mess up depth", () => {
|
||||
// given
|
||||
const content = `{
|
||||
"provider": {
|
||||
// { unmatched brace in comment
|
||||
"openai": {}
|
||||
},
|
||||
"other": 1
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(result).toContain('"other"')
|
||||
|
||||
// Post-write validation
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('other')
|
||||
expect(parsed.other).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 5: No existing provider key", () => {
|
||||
it("inserts provider without corrupting", () => {
|
||||
// given
|
||||
const content = `{
|
||||
// config comment
|
||||
"plugin": ["foo"]
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(result).toContain('"provider"')
|
||||
expect(result).toContain('"plugin"')
|
||||
expect(result).toContain('foo')
|
||||
expect(result).toContain('// config comment')
|
||||
|
||||
// Post-write validation
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('provider')
|
||||
expect(parsed).toHaveProperty('plugin')
|
||||
expect(parsed.plugin).toEqual(['foo'])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 6: String value exactly 'provider' before real key", () => {
|
||||
it("must NOT match wrong location", () => {
|
||||
// given
|
||||
const content = `{
|
||||
"note": "provider",
|
||||
"provider": { "openai": {} },
|
||||
"other": 1
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(result).toContain('"other"')
|
||||
expect(result).toContain('"note": "provider"')
|
||||
|
||||
// Post-write validation
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('other')
|
||||
expect(parsed.other).toBe(1)
|
||||
expect(parsed.note).toBe('provider')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 7: Post-write validation", () => {
|
||||
it("result file must be valid JSONC for all cases", () => {
|
||||
// Test Case 1
|
||||
const content1 = `{
|
||||
"provider": { "openai": {} },
|
||||
"plugin": ["foo"]
|
||||
}`
|
||||
const result1 = modifyProviderInJsonc(content1, { google: {} })
|
||||
expect(() => parseJsonc(result1)).not.toThrow()
|
||||
|
||||
// Test Case 2
|
||||
const content2 = `{
|
||||
"provider": {
|
||||
// } comment
|
||||
"openai": {}
|
||||
}
|
||||
}`
|
||||
const result2 = modifyProviderInJsonc(content2, { google: {} })
|
||||
expect(() => parseJsonc(result2)).not.toThrow()
|
||||
|
||||
// Test Case 3
|
||||
const content3 = `{
|
||||
"plugin": ["foo"]
|
||||
}`
|
||||
const result3 = modifyProviderInJsonc(content3, { google: {} })
|
||||
expect(() => parseJsonc(result3)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 8: Trailing commas preserved", () => {
|
||||
it("file is valid JSONC with trailing commas", () => {
|
||||
// given
|
||||
const content = `{
|
||||
"provider": { "openai": {}, },
|
||||
"plugin": ["foo",],
|
||||
}`
|
||||
const newProviderValue = { google: { name: "Google" } }
|
||||
|
||||
// when
|
||||
const result = modifyProviderInJsonc(content, newProviderValue)
|
||||
|
||||
// then
|
||||
expect(() => parseJsonc(result)).not.toThrow()
|
||||
|
||||
const parsed = parseJsonc<Record<string, unknown>>(result)
|
||||
expect(parsed).toHaveProperty('plugin')
|
||||
expect(parsed.plugin).toEqual(['foo'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,82 +0,0 @@
|
||||
import { readFileSync, writeFileSync, copyFileSync } from "node:fs"
|
||||
import type { ConfigMergeResult, InstallConfig } from "../types"
|
||||
import { getConfigDir } from "./config-context"
|
||||
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
|
||||
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
|
||||
import { detectConfigFormat } from "./opencode-config-format"
|
||||
import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file"
|
||||
import { ANTIGRAVITY_PROVIDER_CONFIG } from "./antigravity-provider-configuration"
|
||||
import { modifyProviderInJsonc } from "./jsonc-provider-editor"
|
||||
import { parseJsonc } from "../../shared/jsonc-parser"
|
||||
|
||||
export function addProviderConfig(config: InstallConfig): ConfigMergeResult {
|
||||
try {
|
||||
ensureConfigDirectoryExists()
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: getConfigDir(),
|
||||
error: formatErrorWithSuggestion(err, "create config directory"),
|
||||
}
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
|
||||
try {
|
||||
let existingConfig: OpenCodeConfig | null = null
|
||||
if (format !== "none") {
|
||||
const parseResult = parseOpenCodeConfigFileWithError(path)
|
||||
if (parseResult.error && !parseResult.config) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: `Failed to parse config file: ${parseResult.error}`,
|
||||
}
|
||||
}
|
||||
existingConfig = parseResult.config
|
||||
}
|
||||
|
||||
const newConfig = { ...(existingConfig ?? {}) }
|
||||
const providers = (newConfig.provider ?? {}) as Record<string, unknown>
|
||||
|
||||
if (config.hasGemini) {
|
||||
providers.google = ANTIGRAVITY_PROVIDER_CONFIG.google
|
||||
}
|
||||
|
||||
if (Object.keys(providers).length > 0) {
|
||||
newConfig.provider = providers
|
||||
}
|
||||
|
||||
if (format === "jsonc") {
|
||||
const content = readFileSync(path, "utf-8")
|
||||
|
||||
// Backup original file
|
||||
copyFileSync(path, `${path}.bak`)
|
||||
|
||||
const providerValue = (newConfig.provider ?? {}) as Record<string, unknown>
|
||||
const newContent = modifyProviderInJsonc(content, providerValue)
|
||||
|
||||
// Post-write validation
|
||||
try {
|
||||
parseJsonc(newContent)
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: `Generated JSONC is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}
|
||||
}
|
||||
|
||||
writeFileSync(path, newContent)
|
||||
} else {
|
||||
writeFileSync(path, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
}
|
||||
return { success: true, configPath: path }
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: formatErrorWithSuggestion(err, "add provider config"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* Antigravity Provider Configuration
|
||||
*
|
||||
* IMPORTANT: Model names MUST use `antigravity-` prefix for stability.
|
||||
*
|
||||
* Since opencode-antigravity-auth v1.3.0, models use a variant system:
|
||||
* - `antigravity-gemini-3.1-pro` with variants: low, high
|
||||
* - `antigravity-gemini-3-flash` with variants: minimal, low, medium, high
|
||||
*
|
||||
* Legacy tier-suffixed names (e.g., `antigravity-gemini-3.1-pro-high`) still work
|
||||
* but variants are the recommended approach.
|
||||
*
|
||||
* @see https://github.com/NoeFabris/opencode-antigravity-auth#models
|
||||
*/
|
||||
export const ANTIGRAVITY_PROVIDER_CONFIG = {
|
||||
google: {
|
||||
name: "Google",
|
||||
models: {
|
||||
"antigravity-gemini-3.1-pro": {
|
||||
name: "Gemini 3 Pro (Antigravity)",
|
||||
limit: { context: 1048576, output: 65535 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
low: { thinkingLevel: "low" },
|
||||
high: { thinkingLevel: "high" },
|
||||
},
|
||||
},
|
||||
"antigravity-gemini-3-flash": {
|
||||
name: "Gemini 3 Flash (Antigravity)",
|
||||
limit: { context: 1048576, output: 65536 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
minimal: { thinkingLevel: "minimal" },
|
||||
low: { thinkingLevel: "low" },
|
||||
medium: { thinkingLevel: "medium" },
|
||||
high: { thinkingLevel: "high" },
|
||||
},
|
||||
},
|
||||
"antigravity-claude-sonnet-4-6": {
|
||||
name: "Claude Sonnet 4.6 (Antigravity)",
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
"antigravity-claude-sonnet-4-6-thinking": {
|
||||
name: "Claude Sonnet 4.6 Thinking (Antigravity)",
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
low: { thinkingConfig: { thinkingBudget: 8192 } },
|
||||
max: { thinkingConfig: { thinkingBudget: 32768 } },
|
||||
},
|
||||
},
|
||||
"antigravity-claude-opus-4-5-thinking": {
|
||||
name: "Claude Opus 4.5 Thinking (Antigravity)",
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
low: { thinkingConfig: { thinkingBudget: 8192 } },
|
||||
max: { thinkingConfig: { thinkingBudget: 32768 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
import { describe, expect, it, beforeEach, afterEach, spyOn } from "bun:test"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { writeFileSync, readFileSync, existsSync, rmSync, mkdirSync } from "node:fs"
|
||||
import { parseJsonc } from "../../shared/jsonc-parser"
|
||||
import type { InstallConfig } from "../types"
|
||||
import { resetConfigContext } from "./config-context"
|
||||
|
||||
let testConfigPath: string
|
||||
let testConfigDir: string
|
||||
let testCounter = 0
|
||||
let fetchVersionSpy: unknown
|
||||
|
||||
beforeEach(async () => {
|
||||
testCounter++
|
||||
testConfigDir = join(tmpdir(), `test-opencode-${Date.now()}-${testCounter}`)
|
||||
testConfigPath = join(testConfigDir, "opencode.jsonc")
|
||||
mkdirSync(testConfigDir, { recursive: true })
|
||||
|
||||
process.env.OPENCODE_CONFIG_DIR = testConfigDir
|
||||
resetConfigContext()
|
||||
|
||||
const module = await import("./auth-plugins")
|
||||
fetchVersionSpy = spyOn(module, "fetchLatestVersion").mockResolvedValue("1.2.3")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
rmSync(testConfigDir, { recursive: true, force: true })
|
||||
} catch {}
|
||||
})
|
||||
|
||||
const testConfig: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: true,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
}
|
||||
|
||||
describe("addAuthPlugins", () => {
|
||||
describe("Test 1: JSONC with commented plugin line", () => {
|
||||
it("preserves comment, does NOT add antigravity plugin", async () => {
|
||||
const content = `{
|
||||
// "plugin": ["old-plugin"]
|
||||
"plugin": ["existing-plugin"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(result.configPath, "utf-8")
|
||||
expect(newContent).toContain('// "plugin": ["old-plugin"]')
|
||||
expect(newContent).toContain('existing-plugin')
|
||||
// antigravity plugin should NOT be auto-added anymore
|
||||
expect(newContent).not.toContain('opencode-antigravity-auth')
|
||||
|
||||
const parsed = parseJsonc<Record<string, unknown>>(newContent)
|
||||
const plugins = parsed.plugin as string[]
|
||||
expect(plugins).toContain('existing-plugin')
|
||||
expect(plugins.some((p) => p.startsWith('opencode-antigravity-auth'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 2: Plugin array already contains antigravity", () => {
|
||||
it("preserves existing antigravity, does not add another", async () => {
|
||||
const content = `{
|
||||
"plugin": ["existing-plugin", "opencode-antigravity-auth"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(testConfigPath, "utf-8")
|
||||
const parsed = parseJsonc<Record<string, unknown>>(newContent)
|
||||
const plugins = parsed.plugin as string[]
|
||||
|
||||
const antigravityCount = plugins.filter((p) => p.startsWith('opencode-antigravity-auth')).length
|
||||
expect(antigravityCount).toBe(1)
|
||||
expect(plugins).toContain('existing-plugin')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 3: Backup created before write", () => {
|
||||
it("creates .bak file", async () => {
|
||||
const originalContent = `{
|
||||
"plugin": ["existing-plugin"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, originalContent, "utf-8")
|
||||
readFileSync(testConfigPath, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(existsSync(`${result.configPath}.bak`)).toBe(true)
|
||||
|
||||
const backupContent = readFileSync(`${result.configPath}.bak`, "utf-8")
|
||||
expect(backupContent).toBe(originalContent)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 4: Comment with } character", () => {
|
||||
it("preserves comments with special characters", async () => {
|
||||
const content = `{
|
||||
// This comment has } special characters
|
||||
"plugin": ["existing-plugin"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(testConfigPath, "utf-8")
|
||||
expect(newContent).toContain('// This comment has } special characters')
|
||||
|
||||
expect(() => parseJsonc(newContent)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 5: Comment containing 'plugin' string", () => {
|
||||
it("must NOT match comment location", async () => {
|
||||
const content = `{
|
||||
// "plugin": ["fake"]
|
||||
"plugin": ["existing-plugin"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(testConfigPath, "utf-8")
|
||||
expect(newContent).toContain('// "plugin": ["fake"]')
|
||||
|
||||
const parsed = parseJsonc<Record<string, unknown>>(newContent)
|
||||
const plugins = parsed.plugin as string[]
|
||||
expect(plugins).toContain('existing-plugin')
|
||||
expect(plugins).not.toContain('fake')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 6: No existing plugin array", () => {
|
||||
it("creates empty plugin array when none exists, does NOT add antigravity", async () => {
|
||||
const content = `{
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(result.configPath, "utf-8")
|
||||
|
||||
const parsed = parseJsonc<Record<string, unknown>>(newContent)
|
||||
expect(parsed).toHaveProperty('plugin')
|
||||
const plugins = parsed.plugin as string[]
|
||||
// antigravity plugin should NOT be auto-added anymore
|
||||
expect(plugins.some((p) => p.startsWith('opencode-antigravity-auth'))).toBe(false)
|
||||
expect(plugins.length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 7: Post-write validation ensures valid JSONC", () => {
|
||||
it("result file must be valid JSONC", async () => {
|
||||
const content = `{
|
||||
"plugin": ["existing-plugin"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(testConfigPath, "utf-8")
|
||||
expect(() => parseJsonc(newContent)).not.toThrow()
|
||||
|
||||
const parsed = parseJsonc<Record<string, unknown>>(newContent)
|
||||
expect(parsed).toHaveProperty('plugin')
|
||||
expect(parsed).toHaveProperty('provider')
|
||||
})
|
||||
})
|
||||
|
||||
describe("Test 8: Multiple plugins in array", () => {
|
||||
it("preserves existing plugins, does NOT add antigravity", async () => {
|
||||
const content = `{
|
||||
"plugin": ["plugin-1", "plugin-2", "plugin-3"],
|
||||
"provider": {}
|
||||
}`
|
||||
writeFileSync(testConfigPath, content, "utf-8")
|
||||
|
||||
const { addAuthPlugins } = await import("./auth-plugins")
|
||||
const result = await addAuthPlugins(testConfig)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
const newContent = readFileSync(result.configPath, "utf-8")
|
||||
const parsed = parseJsonc<Record<string, unknown>>(newContent)
|
||||
const plugins = parsed.plugin as string[]
|
||||
|
||||
expect(plugins).toContain('plugin-1')
|
||||
expect(plugins).toContain('plugin-2')
|
||||
expect(plugins).toContain('plugin-3')
|
||||
// antigravity plugin should NOT be auto-added anymore
|
||||
expect(plugins.some((p) => p.startsWith('opencode-antigravity-auth'))).toBe(false)
|
||||
expect(plugins.length).toBe(3)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,140 +0,0 @@
|
||||
import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs"
|
||||
import { modify, applyEdits } from "jsonc-parser"
|
||||
import type { ConfigMergeResult, InstallConfig } from "../types"
|
||||
import { getConfigDir } from "./config-context"
|
||||
import { ensureConfigDirectoryExists } from "./ensure-config-directory-exists"
|
||||
import { formatErrorWithSuggestion } from "./format-error-with-suggestion"
|
||||
import { detectConfigFormat } from "./opencode-config-format"
|
||||
import { parseOpenCodeConfigFileWithError, type OpenCodeConfig } from "./parse-opencode-config-file"
|
||||
import { parseJsonc } from "../../shared/jsonc-parser"
|
||||
|
||||
export async function fetchLatestVersion(packageName: string): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`)
|
||||
if (!res.ok) return null
|
||||
const data = (await res.json()) as { version: string }
|
||||
return data.version
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function addAuthPlugins(config: InstallConfig): Promise<ConfigMergeResult> {
|
||||
try {
|
||||
ensureConfigDirectoryExists()
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: getConfigDir(),
|
||||
error: formatErrorWithSuggestion(err, "create config directory"),
|
||||
}
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
const backupPath = `${path}.bak`
|
||||
|
||||
try {
|
||||
let existingConfig: OpenCodeConfig | null = null
|
||||
if (format !== "none") {
|
||||
const parseResult = parseOpenCodeConfigFileWithError(path)
|
||||
if (parseResult.error && !parseResult.config) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: `Failed to parse config file: ${parseResult.error}`,
|
||||
}
|
||||
}
|
||||
existingConfig = parseResult.config
|
||||
}
|
||||
|
||||
const rawPlugins = existingConfig?.plugin
|
||||
const plugins: string[] = Array.isArray(rawPlugins) ? rawPlugins : []
|
||||
|
||||
// Note: opencode-antigravity-auth plugin auto-installation has been removed
|
||||
// Users can manually add auth plugins if needed
|
||||
|
||||
const newConfig = { ...(existingConfig ?? {}), plugin: plugins }
|
||||
|
||||
if (format !== "none" && existsSync(path)) {
|
||||
copyFileSync(path, backupPath)
|
||||
}
|
||||
|
||||
if (format === "jsonc") {
|
||||
const content = readFileSync(path, "utf-8")
|
||||
|
||||
const newContent = applyEdits(
|
||||
content,
|
||||
modify(content, ["plugin"], plugins, {
|
||||
formattingOptions: { tabSize: 2, insertSpaces: true },
|
||||
})
|
||||
)
|
||||
|
||||
try {
|
||||
parseJsonc(newContent)
|
||||
} catch (error) {
|
||||
if (existsSync(backupPath)) {
|
||||
copyFileSync(backupPath, path)
|
||||
}
|
||||
throw new Error(`Generated JSONC is invalid: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
|
||||
try {
|
||||
writeFileSync(path, newContent)
|
||||
} catch (error) {
|
||||
const hasBackup = existsSync(backupPath)
|
||||
try {
|
||||
if (hasBackup) {
|
||||
copyFileSync(backupPath, path)
|
||||
}
|
||||
} catch (restoreError) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: `Failed to write config file, and restore from backup failed: ${String(error)}; restore error: ${String(restoreError)}`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: hasBackup
|
||||
? `Failed to write config file. Restored from backup: ${String(error)}`
|
||||
: `Failed to write config file. No backup was available: ${String(error)}`,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const nextContent = JSON.stringify(newConfig, null, 2) + "\n"
|
||||
try {
|
||||
writeFileSync(path, nextContent)
|
||||
} catch (error) {
|
||||
const hasBackup = existsSync(backupPath)
|
||||
try {
|
||||
if (hasBackup) {
|
||||
copyFileSync(backupPath, path)
|
||||
}
|
||||
} catch (restoreError) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: `Failed to write config file, and restore from backup failed: ${String(error)}; restore error: ${String(restoreError)}`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: hasBackup
|
||||
? `Failed to write config file. Restored from backup: ${String(error)}`
|
||||
: `Failed to write config file. No backup was available: ${String(error)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
return { success: true, configPath: path }
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
configPath: path,
|
||||
error: formatErrorWithSuggestion(err, "add auth plugins to config"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,8 @@ export function detectCurrentConfig(): DetectedConfig {
|
||||
return result
|
||||
}
|
||||
|
||||
result.hasGemini = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
|
||||
const providers = openCodeConfig.provider as Record<string, unknown> | undefined
|
||||
result.hasGemini = providers ? "google" in providers : false
|
||||
|
||||
const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding } = detectProvidersFromOmoConfig()
|
||||
result.hasOpenAI = hasOpenAI
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { modify, applyEdits } from "jsonc-parser"
|
||||
|
||||
export function modifyProviderInJsonc(
|
||||
content: string,
|
||||
newProviderValue: Record<string, unknown>
|
||||
): string {
|
||||
const edits = modify(content, ["provider"], newProviderValue, {
|
||||
formattingOptions: { tabSize: 2, insertSpaces: true },
|
||||
})
|
||||
return applyEdits(content, edits)
|
||||
}
|
||||
@@ -9,7 +9,6 @@ export const CLI_AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||
{ providers: ["kimi-for-coding"], model: "k2p5" },
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.7" },
|
||||
{ providers: ["opencode"], model: "glm-4.7-free" },
|
||||
],
|
||||
@@ -44,12 +43,10 @@ export const CLI_AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
},
|
||||
"multimodal-looker": {
|
||||
fallbackChain: [
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.6v" },
|
||||
{ providers: ["openai", "opencode"], model: "gpt-5.3-codex", variant: "medium" },
|
||||
{ providers: ["kimi-for-coding"], model: "k2p5" },
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.6v" },
|
||||
{ providers: ["opencode"], model: "gpt-5-nano" },
|
||||
],
|
||||
},
|
||||
@@ -57,7 +54,6 @@ export const CLI_AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||
{ providers: ["kimi-for-coding"], model: "k2p5" },
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro" },
|
||||
],
|
||||
@@ -66,7 +62,6 @@ export const CLI_AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||
{ providers: ["kimi-for-coding"], model: "k2p5" },
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro", variant: "high" },
|
||||
],
|
||||
@@ -81,7 +76,6 @@ export const CLI_AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
atlas: {
|
||||
fallbackChain: [
|
||||
{ providers: ["kimi-for-coding"], model: "k2p5" },
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro" },
|
||||
|
||||
@@ -2,9 +2,7 @@ import * as p from "@clack/prompts"
|
||||
import color from "picocolors"
|
||||
import type { InstallArgs } from "./types"
|
||||
import {
|
||||
addAuthPlugins,
|
||||
addPluginToOpenCodeConfig,
|
||||
addProviderConfig,
|
||||
detectCurrentConfig,
|
||||
getOpenCodeVersion,
|
||||
isOpenCodeInstalled,
|
||||
@@ -54,26 +52,6 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
|
||||
}
|
||||
spinner.stop(`Plugin added to ${color.cyan(pluginResult.configPath)}`)
|
||||
|
||||
if (config.hasGemini) {
|
||||
spinner.start("Adding auth plugins (fetching latest versions)")
|
||||
const authResult = await addAuthPlugins(config)
|
||||
if (!authResult.success) {
|
||||
spinner.stop(`Failed to add auth plugins: ${authResult.error}`)
|
||||
p.outro(color.red("Installation failed."))
|
||||
return 1
|
||||
}
|
||||
spinner.stop(`Auth plugins added to ${color.cyan(authResult.configPath)}`)
|
||||
|
||||
spinner.start("Adding provider configurations")
|
||||
const providerResult = addProviderConfig(config)
|
||||
if (!providerResult.success) {
|
||||
spinner.stop(`Failed to add provider config: ${providerResult.error}`)
|
||||
p.outro(color.red("Installation failed."))
|
||||
return 1
|
||||
}
|
||||
spinner.stop(`Provider config added to ${color.cyan(providerResult.configPath)}`)
|
||||
}
|
||||
|
||||
spinner.start("Writing oh-my-opencode configuration")
|
||||
const omoResult = writeOmoConfig(config)
|
||||
if (!omoResult.success) {
|
||||
@@ -123,7 +101,7 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
|
||||
if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||
const providers: string[] = []
|
||||
if (config.hasClaude) providers.push(`Anthropic ${color.gray("→ Claude Pro/Max")}`)
|
||||
if (config.hasGemini) providers.push(`Google ${color.gray("→ OAuth with Antigravity")}`)
|
||||
if (config.hasGemini) providers.push(`Google ${color.gray("→ Gemini")}`)
|
||||
if (config.hasCopilot) providers.push(`GitHub ${color.gray("→ Copilot")}`)
|
||||
|
||||
console.log()
|
||||
|
||||
@@ -26,29 +26,35 @@ agent-browser close # Close browser
|
||||
|
||||
### Navigation
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to URL
|
||||
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser
|
||||
agent-browser close # Close browser (aliases: quit, exit)
|
||||
```
|
||||
|
||||
### Snapshot (page analysis)
|
||||
```bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
agent-browser snapshot -c # Compact output
|
||||
agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, etc.)
|
||||
agent-browser snapshot -c # Compact (remove empty structural elements)
|
||||
agent-browser snapshot -d 3 # Limit depth to 3
|
||||
agent-browser snapshot -s "#main" # Scope to CSS selector
|
||||
agent-browser snapshot -i -c -d 5 # Combine options
|
||||
```
|
||||
|
||||
The `-C` flag is useful for modern web apps that use custom clickable elements (divs, spans) instead of standard buttons/links.
|
||||
|
||||
### Interactions (use @refs from snapshot)
|
||||
```bash
|
||||
agent-browser click @e1 # Click
|
||||
agent-browser click @e1 # Click (--new-tab to open in new tab)
|
||||
agent-browser dblclick @e1 # Double-click
|
||||
agent-browser focus @e1 # Focus element
|
||||
agent-browser fill @e2 "text" # Clear and type
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser keyboard type "text" # Type with real keystrokes (no selector, current focus)
|
||||
agent-browser keyboard inserttext "text" # Insert text without key events (no selector)
|
||||
agent-browser press Enter # Press key
|
||||
agent-browser press Control+a # Key combination
|
||||
agent-browser keydown Shift # Hold key down
|
||||
@@ -57,8 +63,8 @@ agent-browser hover @e1 # Hover
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser uncheck @e1 # Uncheck checkbox
|
||||
agent-browser select @e1 "value" # Select dropdown
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scrollintoview @e1 # Scroll element into view
|
||||
agent-browser scroll down 500 # Scroll page (--selector <sel> for container)
|
||||
agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto)
|
||||
agent-browser drag @e1 @e2 # Drag and drop
|
||||
agent-browser upload @e1 file.pdf # Upload files
|
||||
```
|
||||
@@ -73,6 +79,7 @@ agent-browser get title # Get page title
|
||||
agent-browser get url # Get current URL
|
||||
agent-browser get count ".item" # Count matching elements
|
||||
agent-browser get box @e1 # Get bounding box
|
||||
agent-browser get styles @e1 # Get computed styles
|
||||
```
|
||||
|
||||
### Check state
|
||||
@@ -84,12 +91,20 @@ agent-browser is checked @e1 # Check if checked
|
||||
|
||||
### Screenshots & PDF
|
||||
```bash
|
||||
agent-browser screenshot # Screenshot to stdout
|
||||
agent-browser screenshot # Screenshot (saves to temp dir if no path)
|
||||
agent-browser screenshot path.png # Save to file
|
||||
agent-browser screenshot --full # Full page
|
||||
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
||||
agent-browser pdf output.pdf # Save as PDF
|
||||
```
|
||||
|
||||
Annotated screenshots overlay numbered labels `[N]` on interactive elements. Each label corresponds to ref `@eN`, so refs work for both visual and text workflows:
|
||||
```bash
|
||||
agent-browser screenshot --annotate ./page.png
|
||||
# Output: [1] @e1 button "Submit", [2] @e2 link "Home", [3] @e3 textbox "Email"
|
||||
agent-browser click @e2 # Click the "Home" link labeled [2]
|
||||
```
|
||||
|
||||
### Video recording
|
||||
```bash
|
||||
agent-browser record start ./demo.webm # Start recording (uses current URL + state)
|
||||
@@ -109,10 +124,12 @@ agent-browser wait --load networkidle # Wait for network idle
|
||||
agent-browser wait --fn "window.ready" # Wait for JS condition
|
||||
```
|
||||
|
||||
Load states: `load`, `domcontentloaded`, `networkidle`
|
||||
|
||||
### Mouse control
|
||||
```bash
|
||||
agent-browser mouse move 100 200 # Move mouse
|
||||
agent-browser mouse down left # Press button
|
||||
agent-browser mouse down left # Press button (left/right/middle)
|
||||
agent-browser mouse up left # Release button
|
||||
agent-browser mouse wheel 100 # Scroll wheel
|
||||
```
|
||||
@@ -122,10 +139,18 @@ agent-browser mouse wheel 100 # Scroll wheel
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
agent-browser find placeholder "Search..." fill "query"
|
||||
agent-browser find alt "Logo" click
|
||||
agent-browser find title "Close" click
|
||||
agent-browser find testid "submit-btn" click
|
||||
agent-browser find first ".item" click
|
||||
agent-browser find last ".item" click
|
||||
agent-browser find nth 2 "a" text
|
||||
```
|
||||
|
||||
Actions: `click`, `fill`, `type`, `hover`, `focus`, `check`, `uncheck`, `text`
|
||||
Options: `--name <name>` (filter role by accessible name), `--exact` (require exact text match)
|
||||
|
||||
### Browser settings
|
||||
```bash
|
||||
agent-browser set viewport 1920 1080 # Set viewport size
|
||||
@@ -142,14 +167,13 @@ agent-browser set media dark # Emulate color scheme
|
||||
agent-browser cookies # Get all cookies
|
||||
agent-browser cookies set name value # Set cookie
|
||||
agent-browser cookies clear # Clear cookies
|
||||
|
||||
agent-browser storage local # Get all localStorage
|
||||
agent-browser storage local key # Get specific key
|
||||
agent-browser storage local set k v # Set value
|
||||
agent-browser storage local clear # Clear all
|
||||
agent-browser storage session # Get all sessionStorage
|
||||
agent-browser storage session key # Get specific key
|
||||
agent-browser storage session set k v # Set value
|
||||
agent-browser storage session clear # Clear all
|
||||
|
||||
agent-browser storage session # Same for sessionStorage
|
||||
```
|
||||
|
||||
### Network
|
||||
@@ -179,13 +203,59 @@ agent-browser frame main # Back to main frame
|
||||
|
||||
### Dialogs
|
||||
```bash
|
||||
agent-browser dialog accept [text] # Accept dialog
|
||||
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
|
||||
agent-browser dialog dismiss # Dismiss dialog
|
||||
```
|
||||
|
||||
### Diff (compare snapshots, screenshots, URLs)
|
||||
```bash
|
||||
agent-browser diff snapshot # Compare current vs last snapshot
|
||||
agent-browser diff snapshot --baseline before.txt # Compare current vs saved snapshot file
|
||||
agent-browser diff snapshot --selector "#main" --compact # Scoped snapshot diff
|
||||
agent-browser diff screenshot --baseline before.png # Visual pixel diff against baseline
|
||||
agent-browser diff screenshot --baseline b.png -o d.png # Save diff image to custom path
|
||||
agent-browser diff screenshot --baseline b.png -t 0.2 # Adjust color threshold (0-1)
|
||||
agent-browser diff url https://v1.com https://v2.com # Compare two URLs (snapshot diff)
|
||||
agent-browser diff url https://v1.com https://v2.com --screenshot # Also visual diff
|
||||
agent-browser diff url https://v1.com https://v2.com --selector "#main" # Scope to element
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
```bash
|
||||
agent-browser eval "document.title" # Run JavaScript
|
||||
agent-browser eval -b "base64code" # Run base64-encoded JS
|
||||
agent-browser eval --stdin # Read JS from stdin
|
||||
```
|
||||
|
||||
### Debug & Profiling
|
||||
```bash
|
||||
agent-browser console # View console messages
|
||||
agent-browser console --clear # Clear console
|
||||
agent-browser errors # View page errors
|
||||
agent-browser errors --clear # Clear errors
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser trace start # Start recording trace
|
||||
agent-browser trace stop trace.zip # Stop and save trace
|
||||
agent-browser profiler start # Start Chrome DevTools profiling
|
||||
agent-browser profiler stop profile.json # Stop and save profile
|
||||
```
|
||||
|
||||
### State management
|
||||
```bash
|
||||
agent-browser state save auth.json # Save auth state
|
||||
agent-browser state load auth.json # Load auth state
|
||||
agent-browser state list # List saved state files
|
||||
agent-browser state show <file> # Show state summary
|
||||
agent-browser state rename <old> <new> # Rename state file
|
||||
agent-browser state clear [name] # Clear states for session
|
||||
agent-browser state clear --all # Clear all saved states
|
||||
agent-browser state clean --older-than <days> # Delete old states
|
||||
```
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
agent-browser install # Download Chromium browser
|
||||
agent-browser install --with-deps # Also install system deps (Linux)
|
||||
```
|
||||
|
||||
## Global Options
|
||||
@@ -193,19 +263,60 @@ agent-browser eval "document.title" # Run JavaScript
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--session <name>` | Isolated browser session (`AGENT_BROWSER_SESSION` env) |
|
||||
| `--session-name <name>` | Auto-save/restore session state (`AGENT_BROWSER_SESSION_NAME` env) |
|
||||
| `--profile <path>` | Persistent browser profile (`AGENT_BROWSER_PROFILE` env) |
|
||||
| `--state <path>` | Load storage state from JSON file (`AGENT_BROWSER_STATE` env) |
|
||||
| `--headers <json>` | HTTP headers scoped to URL's origin |
|
||||
| `--executable-path <path>` | Custom browser binary (`AGENT_BROWSER_EXECUTABLE_PATH` env) |
|
||||
| `--extension <path>` | Load browser extension (repeatable; `AGENT_BROWSER_EXTENSIONS` env) |
|
||||
| `--args <args>` | Browser launch args (`AGENT_BROWSER_ARGS` env) |
|
||||
| `--user-agent <ua>` | Custom User-Agent (`AGENT_BROWSER_USER_AGENT` env) |
|
||||
| `--proxy <url>` | Proxy server (`AGENT_BROWSER_PROXY` env) |
|
||||
| `--proxy-bypass <hosts>` | Hosts to bypass proxy (`AGENT_BROWSER_PROXY_BYPASS` env) |
|
||||
| `--ignore-https-errors` | Ignore HTTPS certificate errors |
|
||||
| `--allow-file-access` | Allow file:// URLs to access local files |
|
||||
| `-p, --provider <name>` | Cloud browser provider (`AGENT_BROWSER_PROVIDER` env) |
|
||||
| `--device <name>` | iOS device name (`AGENT_BROWSER_IOS_DEVICE` env) |
|
||||
| `--json` | Machine-readable JSON output |
|
||||
| `--headed` | Show browser window (not headless) |
|
||||
| `--full, -f` | Full page screenshot |
|
||||
| `--annotate` | Annotated screenshot with numbered labels (`AGENT_BROWSER_ANNOTATE` env) |
|
||||
| `--headed` | Show browser window (`AGENT_BROWSER_HEADED` env) |
|
||||
| `--cdp <port\|wss://url>` | Connect via Chrome DevTools Protocol |
|
||||
| `--auto-connect` | Auto-discover running Chrome (`AGENT_BROWSER_AUTO_CONNECT` env) |
|
||||
| `--color-scheme <scheme>` | Color scheme: dark, light, no-preference (`AGENT_BROWSER_COLOR_SCHEME` env) |
|
||||
| `--download-path <path>` | Default download directory (`AGENT_BROWSER_DOWNLOAD_PATH` env) |
|
||||
| `--native` | [Experimental] Use native Rust daemon (`AGENT_BROWSER_NATIVE` env) |
|
||||
| `--config <path>` | Custom config file (`AGENT_BROWSER_CONFIG` env) |
|
||||
| `--debug` | Debug output |
|
||||
|
||||
### Security options
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--content-boundaries` | Wrap page output in boundary markers (`AGENT_BROWSER_CONTENT_BOUNDARIES` env) |
|
||||
| `--max-output <chars>` | Truncate page output to N characters (`AGENT_BROWSER_MAX_OUTPUT` env) |
|
||||
| `--allowed-domains <list>` | Comma-separated allowed domain patterns (`AGENT_BROWSER_ALLOWED_DOMAINS` env) |
|
||||
| `--action-policy <path>` | Path to action policy JSON file (`AGENT_BROWSER_ACTION_POLICY` env) |
|
||||
| `--confirm-actions <list>` | Action categories requiring confirmation (`AGENT_BROWSER_CONFIRM_ACTIONS` env) |
|
||||
|
||||
## Configuration file
|
||||
|
||||
Create `agent-browser.json` for persistent defaults (no need to repeat flags):
|
||||
|
||||
**Locations (lowest to highest priority):**
|
||||
1. `~/.agent-browser/config.json` — user-level defaults
|
||||
2. `./agent-browser.json` — project-level overrides
|
||||
3. `AGENT_BROWSER_*` environment variables
|
||||
4. CLI flags override everything
|
||||
|
||||
```json
|
||||
{
|
||||
"headed": true,
|
||||
"proxy": "http://localhost:8080",
|
||||
"profile": "./browser-data",
|
||||
"native": true
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Form submission
|
||||
|
||||
```bash
|
||||
@@ -247,6 +358,13 @@ agent-browser open other-site.com
|
||||
agent-browser set headers '{"X-Custom-Header": "value"}'
|
||||
```
|
||||
|
||||
### Authentication Vault
|
||||
```bash
|
||||
# Store credentials locally (encrypted). The LLM never sees passwords.
|
||||
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
|
||||
agent-browser auth login github
|
||||
```
|
||||
|
||||
## Sessions & Persistent Profiles
|
||||
|
||||
### Sessions (parallel browsers)
|
||||
@@ -256,6 +374,13 @@ agent-browser --session test2 open site-b.com
|
||||
agent-browser session list
|
||||
```
|
||||
|
||||
### Session persistence (auto-save/restore)
|
||||
```bash
|
||||
agent-browser --session-name twitter open twitter.com
|
||||
# Login once, state persists automatically across restarts
|
||||
# State files stored in ~/.agent-browser/sessions/
|
||||
```
|
||||
|
||||
### Persistent Profiles
|
||||
Persists cookies, localStorage, IndexedDB, service workers, cache, login sessions across browser restarts.
|
||||
```bash
|
||||
@@ -263,9 +388,6 @@ agent-browser --profile ~/.myapp-profile open myapp.com
|
||||
# Or via env var
|
||||
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
|
||||
```
|
||||
- Use different profile paths for different projects
|
||||
- Login once → restart browser → still logged in
|
||||
- Stores: cookies, localStorage, IndexedDB, service workers, browser cache
|
||||
|
||||
## JSON output (for parsing)
|
||||
|
||||
@@ -275,62 +397,54 @@ agent-browser snapshot -i --json
|
||||
agent-browser get text @e1 --json
|
||||
```
|
||||
|
||||
## Debugging
|
||||
## Local files
|
||||
|
||||
```bash
|
||||
agent-browser open example.com --headed # Show browser window
|
||||
agent-browser console # View console messages
|
||||
agent-browser errors # View page errors
|
||||
agent-browser record start ./debug.webm # Record from current page
|
||||
agent-browser record stop # Save recording
|
||||
agent-browser connect 9222 # Local CDP port
|
||||
agent-browser --allow-file-access open file:///path/to/document.pdf
|
||||
agent-browser --allow-file-access open file:///path/to/page.html
|
||||
```
|
||||
|
||||
## CDP Mode
|
||||
|
||||
```bash
|
||||
agent-browser connect 9222 # Local CDP port
|
||||
agent-browser --cdp 9222 snapshot # Direct CDP on each command
|
||||
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot # Remote via WebSocket
|
||||
agent-browser console --clear # Clear console
|
||||
agent-browser errors --clear # Clear errors
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser trace start # Start recording trace
|
||||
agent-browser trace stop trace.zip # Stop and save trace
|
||||
agent-browser --auto-connect snapshot # Auto-discover running Chrome
|
||||
```
|
||||
|
||||
## Cloud providers
|
||||
|
||||
```bash
|
||||
# Browserbase
|
||||
BROWSERBASE_API_KEY="key" BROWSERBASE_PROJECT_ID="id" agent-browser -p browserbase open example.com
|
||||
|
||||
# Browser Use
|
||||
BROWSER_USE_API_KEY="key" agent-browser -p browseruse open example.com
|
||||
|
||||
# Kernel
|
||||
KERNEL_API_KEY="key" agent-browser -p kernel open example.com
|
||||
```
|
||||
|
||||
## iOS Simulator
|
||||
|
||||
```bash
|
||||
agent-browser device list # List available simulators
|
||||
agent-browser -p ios --device "iPhone 16 Pro" open example.com # Launch Safari
|
||||
agent-browser -p ios snapshot -i # Same commands as desktop
|
||||
agent-browser -p ios tap @e1 # Tap
|
||||
agent-browser -p ios swipe up # Mobile-specific
|
||||
agent-browser -p ios close # Close session
|
||||
```
|
||||
|
||||
## Native Mode (Experimental)
|
||||
|
||||
Pure Rust daemon using direct CDP — no Node.js/Playwright required:
|
||||
```bash
|
||||
agent-browser --native open example.com
|
||||
# Or: export AGENT_BROWSER_NATIVE=1
|
||||
# Or: {"native": true} in agent-browser.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Step 1: Install agent-browser CLI
|
||||
|
||||
```bash
|
||||
bun add -g agent-browser
|
||||
```
|
||||
|
||||
### Step 2: Install Playwright browsers
|
||||
|
||||
**IMPORTANT**: `agent-browser install` may fail on some platforms (e.g., darwin-arm64) with "No binary found" error. In that case, install Playwright browsers directly:
|
||||
|
||||
```bash
|
||||
# Create a temp project and install playwright
|
||||
cd /tmp && bun init -y && bun add playwright
|
||||
|
||||
# Install Chromium browser
|
||||
bun playwright install chromium
|
||||
```
|
||||
|
||||
This downloads Chrome for Testing to `~/Library/Caches/ms-playwright/`.
|
||||
|
||||
### Verify installation
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com --headed
|
||||
```
|
||||
|
||||
If the browser opens successfully, installation is complete.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Error | Solution |
|
||||
|-------|----------|
|
||||
| `No binary found for darwin-arm64` | Run `bun playwright install chromium` in a project with playwright dependency |
|
||||
| `Executable doesn't exist at .../chromium-XXXX` | Re-run `bun playwright install chromium` |
|
||||
| Browser doesn't open | Ensure `--headed` flag is used for visible browser |
|
||||
|
||||
---
|
||||
Run `agent-browser --help` for all commands. Repo: https://github.com/vercel-labs/agent-browser
|
||||
Install: `bun add -g agent-browser && agent-browser install`. Run `agent-browser --help` for all commands. Repo: https://github.com/vercel-labs/agent-browser
|
||||
|
||||
@@ -40,29 +40,35 @@ agent-browser close # Close browser
|
||||
|
||||
### Navigation
|
||||
\`\`\`bash
|
||||
agent-browser open <url> # Navigate to URL
|
||||
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser
|
||||
agent-browser close # Close browser (aliases: quit, exit)
|
||||
\`\`\`
|
||||
|
||||
### Snapshot (page analysis)
|
||||
\`\`\`bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
agent-browser snapshot -c # Compact output
|
||||
agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, etc.)
|
||||
agent-browser snapshot -c # Compact (remove empty structural elements)
|
||||
agent-browser snapshot -d 3 # Limit depth to 3
|
||||
agent-browser snapshot -s "#main" # Scope to CSS selector
|
||||
agent-browser snapshot -i -c -d 5 # Combine options
|
||||
\`\`\`
|
||||
|
||||
The \`-C\` flag is useful for modern web apps that use custom clickable elements (divs, spans) instead of standard buttons/links.
|
||||
|
||||
### Interactions (use @refs from snapshot)
|
||||
\`\`\`bash
|
||||
agent-browser click @e1 # Click
|
||||
agent-browser click @e1 # Click (--new-tab to open in new tab)
|
||||
agent-browser dblclick @e1 # Double-click
|
||||
agent-browser focus @e1 # Focus element
|
||||
agent-browser fill @e2 "text" # Clear and type
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser keyboard type "text" # Type with real keystrokes (no selector, current focus)
|
||||
agent-browser keyboard inserttext "text" # Insert text without key events (no selector)
|
||||
agent-browser press Enter # Press key
|
||||
agent-browser press Control+a # Key combination
|
||||
agent-browser keydown Shift # Hold key down
|
||||
@@ -71,8 +77,8 @@ agent-browser hover @e1 # Hover
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser uncheck @e1 # Uncheck checkbox
|
||||
agent-browser select @e1 "value" # Select dropdown
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scrollintoview @e1 # Scroll element into view
|
||||
agent-browser scroll down 500 # Scroll page (--selector <sel> for container)
|
||||
agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto)
|
||||
agent-browser drag @e1 @e2 # Drag and drop
|
||||
agent-browser upload @e1 file.pdf # Upload files
|
||||
\`\`\`
|
||||
@@ -87,6 +93,7 @@ agent-browser get title # Get page title
|
||||
agent-browser get url # Get current URL
|
||||
agent-browser get count ".item" # Count matching elements
|
||||
agent-browser get box @e1 # Get bounding box
|
||||
agent-browser get styles @e1 # Get computed styles
|
||||
\`\`\`
|
||||
|
||||
### Check state
|
||||
@@ -98,12 +105,20 @@ agent-browser is checked @e1 # Check if checked
|
||||
|
||||
### Screenshots & PDF
|
||||
\`\`\`bash
|
||||
agent-browser screenshot # Screenshot to stdout
|
||||
agent-browser screenshot # Screenshot (saves to temp dir if no path)
|
||||
agent-browser screenshot path.png # Save to file
|
||||
agent-browser screenshot --full # Full page
|
||||
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
||||
agent-browser pdf output.pdf # Save as PDF
|
||||
\`\`\`
|
||||
|
||||
Annotated screenshots overlay numbered labels \`[N]\` on interactive elements. Each label corresponds to ref \`@eN\`, so refs work for both visual and text workflows:
|
||||
\`\`\`bash
|
||||
agent-browser screenshot --annotate ./page.png
|
||||
# Output: [1] @e1 button "Submit", [2] @e2 link "Home", [3] @e3 textbox "Email"
|
||||
agent-browser click @e2 # Click the "Home" link labeled [2]
|
||||
\`\`\`
|
||||
|
||||
### Video recording
|
||||
\`\`\`bash
|
||||
agent-browser record start ./demo.webm # Start recording (uses current URL + state)
|
||||
@@ -123,10 +138,12 @@ agent-browser wait --load networkidle # Wait for network idle
|
||||
agent-browser wait --fn "window.ready" # Wait for JS condition
|
||||
\`\`\`
|
||||
|
||||
Load states: \`load\`, \`domcontentloaded\`, \`networkidle\`
|
||||
|
||||
### Mouse control
|
||||
\`\`\`bash
|
||||
agent-browser mouse move 100 200 # Move mouse
|
||||
agent-browser mouse down left # Press button
|
||||
agent-browser mouse down left # Press button (left/right/middle)
|
||||
agent-browser mouse up left # Release button
|
||||
agent-browser mouse wheel 100 # Scroll wheel
|
||||
\`\`\`
|
||||
@@ -136,10 +153,18 @@ agent-browser mouse wheel 100 # Scroll wheel
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
agent-browser find placeholder "Search..." fill "query"
|
||||
agent-browser find alt "Logo" click
|
||||
agent-browser find title "Close" click
|
||||
agent-browser find testid "submit-btn" click
|
||||
agent-browser find first ".item" click
|
||||
agent-browser find last ".item" click
|
||||
agent-browser find nth 2 "a" text
|
||||
\`\`\`
|
||||
|
||||
Actions: \`click\`, \`fill\`, \`type\`, \`hover\`, \`focus\`, \`check\`, \`uncheck\`, \`text\`
|
||||
Options: \`--name <name>\` (filter role by accessible name), \`--exact\` (require exact text match)
|
||||
|
||||
### Browser settings
|
||||
\`\`\`bash
|
||||
agent-browser set viewport 1920 1080 # Set viewport size
|
||||
@@ -156,14 +181,13 @@ agent-browser set media dark # Emulate color scheme
|
||||
agent-browser cookies # Get all cookies
|
||||
agent-browser cookies set name value # Set cookie
|
||||
agent-browser cookies clear # Clear cookies
|
||||
|
||||
agent-browser storage local # Get all localStorage
|
||||
agent-browser storage local key # Get specific key
|
||||
agent-browser storage local set k v # Set value
|
||||
agent-browser storage local clear # Clear all
|
||||
agent-browser storage session # Get all sessionStorage
|
||||
agent-browser storage session key # Get specific key
|
||||
agent-browser storage session set k v # Set value
|
||||
agent-browser storage session clear # Clear all
|
||||
|
||||
agent-browser storage session # Same for sessionStorage
|
||||
\`\`\`
|
||||
|
||||
### Network
|
||||
@@ -193,13 +217,59 @@ agent-browser frame main # Back to main frame
|
||||
|
||||
### Dialogs
|
||||
\`\`\`bash
|
||||
agent-browser dialog accept [text] # Accept dialog
|
||||
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
|
||||
agent-browser dialog dismiss # Dismiss dialog
|
||||
\`\`\`
|
||||
|
||||
### Diff (compare snapshots, screenshots, URLs)
|
||||
\`\`\`bash
|
||||
agent-browser diff snapshot # Compare current vs last snapshot
|
||||
agent-browser diff snapshot --baseline before.txt # Compare current vs saved snapshot file
|
||||
agent-browser diff snapshot --selector "#main" --compact # Scoped snapshot diff
|
||||
agent-browser diff screenshot --baseline before.png # Visual pixel diff against baseline
|
||||
agent-browser diff screenshot --baseline b.png -o d.png # Save diff image to custom path
|
||||
agent-browser diff screenshot --baseline b.png -t 0.2 # Adjust color threshold (0-1)
|
||||
agent-browser diff url https://v1.com https://v2.com # Compare two URLs (snapshot diff)
|
||||
agent-browser diff url https://v1.com https://v2.com --screenshot # Also visual diff
|
||||
agent-browser diff url https://v1.com https://v2.com --selector "#main" # Scope to element
|
||||
\`\`\`
|
||||
|
||||
### JavaScript
|
||||
\`\`\`bash
|
||||
agent-browser eval "document.title" # Run JavaScript
|
||||
agent-browser eval -b "base64code" # Run base64-encoded JS
|
||||
agent-browser eval --stdin # Read JS from stdin
|
||||
\`\`\`
|
||||
|
||||
### Debug & Profiling
|
||||
\`\`\`bash
|
||||
agent-browser console # View console messages
|
||||
agent-browser console --clear # Clear console
|
||||
agent-browser errors # View page errors
|
||||
agent-browser errors --clear # Clear errors
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser trace start # Start recording trace
|
||||
agent-browser trace stop trace.zip # Stop and save trace
|
||||
agent-browser profiler start # Start Chrome DevTools profiling
|
||||
agent-browser profiler stop profile.json # Stop and save profile
|
||||
\`\`\`
|
||||
|
||||
### State management
|
||||
\`\`\`bash
|
||||
agent-browser state save auth.json # Save auth state
|
||||
agent-browser state load auth.json # Load auth state
|
||||
agent-browser state list # List saved state files
|
||||
agent-browser state show <file> # Show state summary
|
||||
agent-browser state rename <old> <new> # Rename state file
|
||||
agent-browser state clear [name] # Clear states for session
|
||||
agent-browser state clear --all # Clear all saved states
|
||||
agent-browser state clean --older-than <days> # Delete old states
|
||||
\`\`\`
|
||||
|
||||
### Setup
|
||||
\`\`\`bash
|
||||
agent-browser install # Download Chromium browser
|
||||
agent-browser install --with-deps # Also install system deps (Linux)
|
||||
\`\`\`
|
||||
|
||||
## Global Options
|
||||
@@ -207,19 +277,60 @@ agent-browser eval "document.title" # Run JavaScript
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| \`--session <name>\` | Isolated browser session (\`AGENT_BROWSER_SESSION\` env) |
|
||||
| \`--session-name <name>\` | Auto-save/restore session state (\`AGENT_BROWSER_SESSION_NAME\` env) |
|
||||
| \`--profile <path>\` | Persistent browser profile (\`AGENT_BROWSER_PROFILE\` env) |
|
||||
| \`--state <path>\` | Load storage state from JSON file (\`AGENT_BROWSER_STATE\` env) |
|
||||
| \`--headers <json>\` | HTTP headers scoped to URL's origin |
|
||||
| \`--executable-path <path>\` | Custom browser binary (\`AGENT_BROWSER_EXECUTABLE_PATH\` env) |
|
||||
| \`--extension <path>\` | Load browser extension (repeatable; \`AGENT_BROWSER_EXTENSIONS\` env) |
|
||||
| \`--args <args>\` | Browser launch args (\`AGENT_BROWSER_ARGS\` env) |
|
||||
| \`--user-agent <ua>\` | Custom User-Agent (\`AGENT_BROWSER_USER_AGENT\` env) |
|
||||
| \`--proxy <url>\` | Proxy server (\`AGENT_BROWSER_PROXY\` env) |
|
||||
| \`--proxy-bypass <hosts>\` | Hosts to bypass proxy (\`AGENT_BROWSER_PROXY_BYPASS\` env) |
|
||||
| \`--ignore-https-errors\` | Ignore HTTPS certificate errors |
|
||||
| \`--allow-file-access\` | Allow file:// URLs to access local files |
|
||||
| \`-p, --provider <name>\` | Cloud browser provider (\`AGENT_BROWSER_PROVIDER\` env) |
|
||||
| \`--device <name>\` | iOS device name (\`AGENT_BROWSER_IOS_DEVICE\` env) |
|
||||
| \`--json\` | Machine-readable JSON output |
|
||||
| \`--headed\` | Show browser window (not headless) |
|
||||
| \`--full, -f\` | Full page screenshot |
|
||||
| \`--annotate\` | Annotated screenshot with numbered labels (\`AGENT_BROWSER_ANNOTATE\` env) |
|
||||
| \`--headed\` | Show browser window (\`AGENT_BROWSER_HEADED\` env) |
|
||||
| \`--cdp <port\\|wss://url>\` | Connect via Chrome DevTools Protocol |
|
||||
| \`--auto-connect\` | Auto-discover running Chrome (\`AGENT_BROWSER_AUTO_CONNECT\` env) |
|
||||
| \`--color-scheme <scheme>\` | Color scheme: dark, light, no-preference (\`AGENT_BROWSER_COLOR_SCHEME\` env) |
|
||||
| \`--download-path <path>\` | Default download directory (\`AGENT_BROWSER_DOWNLOAD_PATH\` env) |
|
||||
| \`--native\` | [Experimental] Use native Rust daemon (\`AGENT_BROWSER_NATIVE\` env) |
|
||||
| \`--config <path>\` | Custom config file (\`AGENT_BROWSER_CONFIG\` env) |
|
||||
| \`--debug\` | Debug output |
|
||||
|
||||
### Security options
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| \`--content-boundaries\` | Wrap page output in boundary markers (\`AGENT_BROWSER_CONTENT_BOUNDARIES\` env) |
|
||||
| \`--max-output <chars>\` | Truncate page output to N characters (\`AGENT_BROWSER_MAX_OUTPUT\` env) |
|
||||
| \`--allowed-domains <list>\` | Comma-separated allowed domain patterns (\`AGENT_BROWSER_ALLOWED_DOMAINS\` env) |
|
||||
| \`--action-policy <path>\` | Path to action policy JSON file (\`AGENT_BROWSER_ACTION_POLICY\` env) |
|
||||
| \`--confirm-actions <list>\` | Action categories requiring confirmation (\`AGENT_BROWSER_CONFIRM_ACTIONS\` env) |
|
||||
|
||||
## Configuration file
|
||||
|
||||
Create \`agent-browser.json\` for persistent defaults (no need to repeat flags):
|
||||
|
||||
**Locations (lowest to highest priority):**
|
||||
1. \`~/.agent-browser/config.json\` — user-level defaults
|
||||
2. \`./agent-browser.json\` — project-level overrides
|
||||
3. \`AGENT_BROWSER_*\` environment variables
|
||||
4. CLI flags override everything
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"headed": true,
|
||||
"proxy": "http://localhost:8080",
|
||||
"profile": "./browser-data",
|
||||
"native": true
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Example: Form submission
|
||||
|
||||
\`\`\`bash
|
||||
@@ -261,6 +372,13 @@ agent-browser open other-site.com
|
||||
agent-browser set headers '{"X-Custom-Header": "value"}'
|
||||
\`\`\`
|
||||
|
||||
### Authentication Vault
|
||||
\`\`\`bash
|
||||
# Store credentials locally (encrypted). The LLM never sees passwords.
|
||||
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
|
||||
agent-browser auth login github
|
||||
\`\`\`
|
||||
|
||||
## Sessions & Persistent Profiles
|
||||
|
||||
### Sessions (parallel browsers)
|
||||
@@ -270,6 +388,13 @@ agent-browser --session test2 open site-b.com
|
||||
agent-browser session list
|
||||
\`\`\`
|
||||
|
||||
### Session persistence (auto-save/restore)
|
||||
\`\`\`bash
|
||||
agent-browser --session-name twitter open twitter.com
|
||||
# Login once, state persists automatically across restarts
|
||||
# State files stored in ~/.agent-browser/sessions/
|
||||
\`\`\`
|
||||
|
||||
### Persistent Profiles
|
||||
Persists cookies, localStorage, IndexedDB, service workers, cache, login sessions across browser restarts.
|
||||
\`\`\`bash
|
||||
@@ -277,9 +402,6 @@ agent-browser --profile ~/.myapp-profile open myapp.com
|
||||
# Or via env var
|
||||
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
|
||||
\`\`\`
|
||||
- Use different profile paths for different projects
|
||||
- Login once → restart browser → still logged in
|
||||
- Stores: cookies, localStorage, IndexedDB, service workers, browser cache
|
||||
|
||||
## JSON output (for parsing)
|
||||
|
||||
@@ -289,21 +411,53 @@ agent-browser snapshot -i --json
|
||||
agent-browser get text @e1 --json
|
||||
\`\`\`
|
||||
|
||||
## Debugging
|
||||
## Local files
|
||||
|
||||
\`\`\`bash
|
||||
agent-browser open example.com --headed # Show browser window
|
||||
agent-browser console # View console messages
|
||||
agent-browser errors # View page errors
|
||||
agent-browser record start ./debug.webm # Record from current page
|
||||
agent-browser record stop # Save recording
|
||||
agent-browser connect 9222 # Local CDP port
|
||||
agent-browser --allow-file-access open file:///path/to/document.pdf
|
||||
agent-browser --allow-file-access open file:///path/to/page.html
|
||||
\`\`\`
|
||||
|
||||
## CDP Mode
|
||||
|
||||
\`\`\`bash
|
||||
agent-browser connect 9222 # Local CDP port
|
||||
agent-browser --cdp 9222 snapshot # Direct CDP on each command
|
||||
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot # Remote via WebSocket
|
||||
agent-browser console --clear # Clear console
|
||||
agent-browser errors --clear # Clear errors
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser trace start # Start recording trace
|
||||
agent-browser trace stop trace.zip # Stop and save trace
|
||||
agent-browser --auto-connect snapshot # Auto-discover running Chrome
|
||||
\`\`\`
|
||||
|
||||
## Cloud providers
|
||||
|
||||
\`\`\`bash
|
||||
# Browserbase
|
||||
BROWSERBASE_API_KEY="key" BROWSERBASE_PROJECT_ID="id" agent-browser -p browserbase open example.com
|
||||
|
||||
# Browser Use
|
||||
BROWSER_USE_API_KEY="key" agent-browser -p browseruse open example.com
|
||||
|
||||
# Kernel
|
||||
KERNEL_API_KEY="key" agent-browser -p kernel open example.com
|
||||
\`\`\`
|
||||
|
||||
## iOS Simulator
|
||||
|
||||
\`\`\`bash
|
||||
agent-browser device list # List available simulators
|
||||
agent-browser -p ios --device "iPhone 16 Pro" open example.com # Launch Safari
|
||||
agent-browser -p ios snapshot -i # Same commands as desktop
|
||||
agent-browser -p ios tap @e1 # Tap
|
||||
agent-browser -p ios swipe up # Mobile-specific
|
||||
agent-browser -p ios close # Close session
|
||||
\`\`\`
|
||||
|
||||
## Native Mode (Experimental)
|
||||
|
||||
Pure Rust daemon using direct CDP — no Node.js/Playwright required:
|
||||
\`\`\`bash
|
||||
agent-browser --native open example.com
|
||||
# Or: export AGENT_BROWSER_NATIVE=1
|
||||
# Or: {"native": true} in agent-browser.json
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { mapClaudeModelToOpenCode } from "./claude-model-mapper"
|
||||
|
||||
describe("mapClaudeModelToOpenCode", () => {
|
||||
describe("#given undefined or empty input", () => {
|
||||
it("#when called with undefined #then returns undefined", () => {
|
||||
expect(mapClaudeModelToOpenCode(undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("#when called with empty string #then returns undefined", () => {
|
||||
expect(mapClaudeModelToOpenCode("")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("#when called with whitespace-only string #then returns undefined", () => {
|
||||
expect(mapClaudeModelToOpenCode(" ")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given Claude Code alias", () => {
|
||||
it("#when called with sonnet #then maps to anthropic claude-sonnet-4-6 object", () => {
|
||||
expect(mapClaudeModelToOpenCode("sonnet")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-6" })
|
||||
})
|
||||
|
||||
it("#when called with opus #then maps to anthropic claude-opus-4-6 object", () => {
|
||||
expect(mapClaudeModelToOpenCode("opus")).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" })
|
||||
})
|
||||
|
||||
it("#when called with haiku #then maps to anthropic claude-haiku-4-5 object", () => {
|
||||
expect(mapClaudeModelToOpenCode("haiku")).toEqual({ providerID: "anthropic", modelID: "claude-haiku-4-5" })
|
||||
})
|
||||
|
||||
it("#when called with Sonnet (capitalized) #then maps case-insensitively to object", () => {
|
||||
expect(mapClaudeModelToOpenCode("Sonnet")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-6" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given inherit", () => {
|
||||
it("#when called with inherit #then returns undefined", () => {
|
||||
expect(mapClaudeModelToOpenCode("inherit")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given bare Claude model name", () => {
|
||||
it("#when called with claude-sonnet-4-5-20250514 #then adds anthropic object format", () => {
|
||||
expect(mapClaudeModelToOpenCode("claude-sonnet-4-5-20250514")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-5-20250514" })
|
||||
})
|
||||
|
||||
it("#when called with claude-opus-4-6 #then adds anthropic object format", () => {
|
||||
expect(mapClaudeModelToOpenCode("claude-opus-4-6")).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" })
|
||||
})
|
||||
|
||||
it("#when called with claude-haiku-4-5-20251001 #then adds anthropic object format", () => {
|
||||
expect(mapClaudeModelToOpenCode("claude-haiku-4-5-20251001")).toEqual({ providerID: "anthropic", modelID: "claude-haiku-4-5-20251001" })
|
||||
})
|
||||
|
||||
it("#when called with claude-3-5-sonnet-20241022 #then adds anthropic object format", () => {
|
||||
expect(mapClaudeModelToOpenCode("claude-3-5-sonnet-20241022")).toEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet-20241022" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given model with dot version numbers", () => {
|
||||
it("#when called with claude-3.5-sonnet #then normalizes dots and returns object format", () => {
|
||||
expect(mapClaudeModelToOpenCode("claude-3.5-sonnet")).toEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet" })
|
||||
})
|
||||
|
||||
it("#when called with claude-3.5-sonnet-20241022 #then normalizes dots and returns object format", () => {
|
||||
expect(mapClaudeModelToOpenCode("claude-3.5-sonnet-20241022")).toEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet-20241022" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given model already in provider/model format", () => {
|
||||
it("#when called with anthropic/claude-sonnet-4-6 #then splits into object format", () => {
|
||||
expect(mapClaudeModelToOpenCode("anthropic/claude-sonnet-4-6")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-6" })
|
||||
})
|
||||
|
||||
it("#when called with openai/gpt-5.2 #then splits into object format", () => {
|
||||
expect(mapClaudeModelToOpenCode("openai/gpt-5.2")).toEqual({ providerID: "openai", modelID: "gpt-5.2" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given non-Claude bare model", () => {
|
||||
it("#when called with gpt-5.2 #then returns undefined", () => {
|
||||
expect(mapClaudeModelToOpenCode("gpt-5.2")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("#when called with gemini-3-flash #then returns undefined", () => {
|
||||
expect(mapClaudeModelToOpenCode("gemini-3-flash")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given prototype property name", () => {
|
||||
it("#when called with constructor #then returns undefined", () => {
|
||||
expect(mapClaudeModelToOpenCode("constructor")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("#when called with toString #then returns undefined", () => {
|
||||
expect(mapClaudeModelToOpenCode("toString")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given model with leading/trailing whitespace", () => {
|
||||
it("#when called with padded string #then trims before returning object format", () => {
|
||||
expect(mapClaudeModelToOpenCode(" claude-sonnet-4-6 ")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-6" })
|
||||
})
|
||||
})
|
||||
})
|
||||
39
src/features/claude-code-agent-loader/claude-model-mapper.ts
Normal file
39
src/features/claude-code-agent-loader/claude-model-mapper.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { normalizeModelFormat } from "../../shared/model-format-normalizer"
|
||||
import { normalizeModelID } from "../../shared/model-normalization"
|
||||
|
||||
const ANTHROPIC_PREFIX = "anthropic/"
|
||||
|
||||
const CLAUDE_CODE_ALIAS_MAP = new Map<string, string>([
|
||||
["sonnet", `${ANTHROPIC_PREFIX}claude-sonnet-4-6`],
|
||||
["opus", `${ANTHROPIC_PREFIX}claude-opus-4-6`],
|
||||
["haiku", `${ANTHROPIC_PREFIX}claude-haiku-4-5`],
|
||||
])
|
||||
|
||||
function mapClaudeModelString(model: string | undefined): string | undefined {
|
||||
if (!model) return undefined
|
||||
|
||||
const trimmed = model.trim()
|
||||
if (trimmed.length === 0) return undefined
|
||||
|
||||
if (trimmed === "inherit") return undefined
|
||||
|
||||
const aliasResult = CLAUDE_CODE_ALIAS_MAP.get(trimmed.toLowerCase())
|
||||
if (aliasResult) return aliasResult
|
||||
|
||||
if (trimmed.includes("/")) return trimmed
|
||||
|
||||
const normalized = normalizeModelID(trimmed)
|
||||
|
||||
if (normalized.startsWith("claude-")) {
|
||||
return `${ANTHROPIC_PREFIX}${normalized}`
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function mapClaudeModelToOpenCode(
|
||||
model: string | undefined
|
||||
): { providerID: string; modelID: string } | undefined {
|
||||
const mappedModel = mapClaudeModelString(model)
|
||||
return mappedModel ? normalizeModelFormat(mappedModel) : undefined
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||
import { join, basename } from "path"
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||
import { isMarkdownFile } from "../../shared/file-utils"
|
||||
import { getClaudeConfigDir } from "../../shared"
|
||||
import type { AgentScope, AgentFrontmatter, LoadedAgent } from "./types"
|
||||
import type { AgentScope, AgentFrontmatter, ClaudeCodeAgentConfig, LoadedAgent } from "./types"
|
||||
import { mapClaudeModelToOpenCode } from "./claude-model-mapper"
|
||||
|
||||
function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefined {
|
||||
if (!toolsStr) return undefined
|
||||
@@ -42,10 +42,13 @@ function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[]
|
||||
|
||||
const formattedDescription = `(${scope}) ${originalDescription}`
|
||||
|
||||
const config: AgentConfig = {
|
||||
const mappedModelOverride = mapClaudeModelToOpenCode(data.model)
|
||||
|
||||
const config: ClaudeCodeAgentConfig = {
|
||||
description: formattedDescription,
|
||||
mode: "subagent",
|
||||
prompt: body.trim(),
|
||||
...(mappedModelOverride ? { model: mappedModelOverride } : {}),
|
||||
}
|
||||
|
||||
const toolsConfig = parseToolsConfig(data.tools)
|
||||
@@ -67,22 +70,22 @@ function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[]
|
||||
return agents
|
||||
}
|
||||
|
||||
export function loadUserAgents(): Record<string, AgentConfig> {
|
||||
export function loadUserAgents(): Record<string, ClaudeCodeAgentConfig> {
|
||||
const userAgentsDir = join(getClaudeConfigDir(), "agents")
|
||||
const agents = loadAgentsFromDir(userAgentsDir, "user")
|
||||
|
||||
const result: Record<string, AgentConfig> = {}
|
||||
const result: Record<string, ClaudeCodeAgentConfig> = {}
|
||||
for (const agent of agents) {
|
||||
result[agent.name] = agent.config
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function loadProjectAgents(directory?: string): Record<string, AgentConfig> {
|
||||
export function loadProjectAgents(directory?: string): Record<string, ClaudeCodeAgentConfig> {
|
||||
const projectAgentsDir = join(directory ?? process.cwd(), ".claude", "agents")
|
||||
const agents = loadAgentsFromDir(projectAgentsDir, "project")
|
||||
|
||||
const result: Record<string, AgentConfig> = {}
|
||||
const result: Record<string, ClaudeCodeAgentConfig> = {}
|
||||
for (const agent of agents) {
|
||||
result[agent.name] = agent.config
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
|
||||
export type AgentScope = "user" | "project"
|
||||
|
||||
export type ClaudeCodeAgentConfig = Omit<AgentConfig, "model"> & {
|
||||
model?: string | { providerID: string; modelID: string }
|
||||
}
|
||||
|
||||
export interface AgentFrontmatter {
|
||||
name?: string
|
||||
description?: string
|
||||
@@ -12,6 +16,6 @@ export interface AgentFrontmatter {
|
||||
export interface LoadedAgent {
|
||||
name: string
|
||||
path: string
|
||||
config: AgentConfig
|
||||
config: ClaudeCodeAgentConfig
|
||||
scope: AgentScope
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||
import { basename, join } from "path"
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||
import { isMarkdownFile } from "../../shared/file-utils"
|
||||
import { log } from "../../shared/logger"
|
||||
import type { AgentFrontmatter } from "../claude-code-agent-loader/types"
|
||||
import type { AgentFrontmatter, ClaudeCodeAgentConfig } from "../claude-code-agent-loader/types"
|
||||
import { mapClaudeModelToOpenCode } from "../claude-code-agent-loader/claude-model-mapper"
|
||||
import type { LoadedPlugin } from "./types"
|
||||
|
||||
function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefined {
|
||||
@@ -24,8 +24,8 @@ function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefine
|
||||
return result
|
||||
}
|
||||
|
||||
export function loadPluginAgents(plugins: LoadedPlugin[]): Record<string, AgentConfig> {
|
||||
const agents: Record<string, AgentConfig> = {}
|
||||
export function loadPluginAgents(plugins: LoadedPlugin[]): Record<string, ClaudeCodeAgentConfig> {
|
||||
const agents: Record<string, ClaudeCodeAgentConfig> = {}
|
||||
|
||||
for (const plugin of plugins) {
|
||||
if (!plugin.agentsDir || !existsSync(plugin.agentsDir)) continue
|
||||
@@ -46,10 +46,13 @@ export function loadPluginAgents(plugins: LoadedPlugin[]): Record<string, AgentC
|
||||
const originalDescription = data.description || ""
|
||||
const formattedDescription = `(plugin: ${plugin.name}) ${originalDescription}`
|
||||
|
||||
const config: AgentConfig = {
|
||||
const mappedModelOverride = mapClaudeModelToOpenCode(data.model)
|
||||
|
||||
const config: ClaudeCodeAgentConfig = {
|
||||
description: formattedDescription,
|
||||
mode: "subagent",
|
||||
prompt: body.trim(),
|
||||
...(mappedModelOverride ? { model: mappedModelOverride } : {}),
|
||||
}
|
||||
|
||||
const toolsConfig = parseToolsConfig(data.tools)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { log } from "../../shared/logger"
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
||||
import type { McpServerConfig } from "../claude-code-mcp-loader/types"
|
||||
import type { ClaudeCodeAgentConfig } from "../claude-code-agent-loader/types"
|
||||
import type { HooksConfig, LoadedPlugin, PluginLoadError, PluginLoaderOptions } from "./types"
|
||||
import { discoverInstalledPlugins } from "./discovery"
|
||||
import { loadPluginCommands } from "./command-loader"
|
||||
@@ -20,7 +20,7 @@ export { loadPluginHooksConfigs } from "./hook-loader"
|
||||
export interface PluginComponentsResult {
|
||||
commands: Record<string, CommandDefinition>
|
||||
skills: Record<string, CommandDefinition>
|
||||
agents: Record<string, AgentConfig>
|
||||
agents: Record<string, ClaudeCodeAgentConfig>
|
||||
mcpServers: Record<string, McpServerConfig>
|
||||
hooksConfigs: HooksConfig[]
|
||||
plugins: LoadedPlugin[]
|
||||
|
||||
@@ -134,8 +134,8 @@ describe("model fallback hook", () => {
|
||||
|
||||
//#then - chain should progress to entry[1], not repeat entry[0]
|
||||
expect(secondOutput.message["model"]).toEqual({
|
||||
providerID: "opencode",
|
||||
modelID: "kimi-k2.5-free",
|
||||
providerID: "zai-coding-plan",
|
||||
modelID: "glm-5",
|
||||
})
|
||||
expect(secondOutput.message["variant"]).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -334,8 +334,8 @@ describe("createEventHandler - model fallback", () => {
|
||||
|
||||
//#then - second fallback entry applied (chain advanced)
|
||||
expect(second.message["model"]).toEqual({
|
||||
providerID: "opencode",
|
||||
modelID: "kimi-k2.5-free",
|
||||
providerID: "zai-coding-plan",
|
||||
modelID: "glm-5",
|
||||
})
|
||||
expect(second.message["variant"]).toBeUndefined()
|
||||
expect(abortCalls).toEqual([sessionID, sessionID])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export function createSystemTransformHandler(): (
|
||||
input: { sessionID: string },
|
||||
input: { sessionID?: string; model: { id: string; providerID: string; [key: string]: unknown } },
|
||||
output: { system: string[] },
|
||||
) => Promise<void> {
|
||||
return async (): Promise<void> => {}
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
// #then - fallbackChain has claude-opus-4-6 first, big-pickle last
|
||||
expect(sisyphus).toBeDefined()
|
||||
expect(sisyphus.fallbackChain).toBeArray()
|
||||
expect(sisyphus.fallbackChain).toHaveLength(4)
|
||||
expect(sisyphus.fallbackChain).toHaveLength(3)
|
||||
expect(sisyphus.requiresAnyModel).toBe(true)
|
||||
|
||||
const primary = sisyphus.fallbackChain[0]
|
||||
@@ -39,7 +39,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
expect(primary.model).toBe("claude-opus-4-6")
|
||||
expect(primary.variant).toBe("max")
|
||||
|
||||
const last = sisyphus.fallbackChain[3]
|
||||
const last = sisyphus.fallbackChain[2]
|
||||
expect(last.providers[0]).toBe("opencode")
|
||||
expect(last.model).toBe("big-pickle")
|
||||
})
|
||||
@@ -86,19 +86,27 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
expect(quaternary.model).toBe("gpt-5-nano")
|
||||
})
|
||||
|
||||
test("multimodal-looker has valid fallbackChain with kimi-k2.5-free as primary", () => {
|
||||
test("multimodal-looker has valid fallbackChain with gpt-5.3-codex as primary", () => {
|
||||
// given - multimodal-looker agent requirement
|
||||
const multimodalLooker = AGENT_MODEL_REQUIREMENTS["multimodal-looker"]
|
||||
|
||||
// when - accessing multimodal-looker requirement
|
||||
// then - fallbackChain exists with kimi-k2.5-free first, gpt-5-nano last
|
||||
// then - fallbackChain: gpt-5.3-codex -> k2p5 -> gemini-3-flash -> glm-4.6v -> gpt-5-nano
|
||||
expect(multimodalLooker).toBeDefined()
|
||||
expect(multimodalLooker.fallbackChain).toBeArray()
|
||||
expect(multimodalLooker.fallbackChain).toHaveLength(5)
|
||||
|
||||
const primary = multimodalLooker.fallbackChain[0]
|
||||
expect(primary.providers[0]).toBe("opencode")
|
||||
expect(primary.model).toBe("kimi-k2.5-free")
|
||||
expect(primary.providers).toEqual(["openai", "opencode"])
|
||||
expect(primary.model).toBe("gpt-5.3-codex")
|
||||
expect(primary.variant).toBe("medium")
|
||||
|
||||
const secondary = multimodalLooker.fallbackChain[1]
|
||||
expect(secondary.providers).toEqual(["kimi-for-coding"])
|
||||
expect(secondary.model).toBe("k2p5")
|
||||
|
||||
const tertiary = multimodalLooker.fallbackChain[2]
|
||||
expect(tertiary.model).toBe("gemini-3-flash")
|
||||
|
||||
const last = multimodalLooker.fallbackChain[4]
|
||||
expect(last.providers).toEqual(["openai", "github-copilot", "opencode"])
|
||||
@@ -153,19 +161,19 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
expect(primary.providers[0]).toBe("openai")
|
||||
})
|
||||
|
||||
test("atlas has valid fallbackChain with kimi-k2.5-free as primary", () => {
|
||||
test("atlas has valid fallbackChain with claude-sonnet-4-6 as primary", () => {
|
||||
// given - atlas agent requirement
|
||||
const atlas = AGENT_MODEL_REQUIREMENTS["atlas"]
|
||||
|
||||
// when - accessing Atlas requirement
|
||||
// then - fallbackChain exists with kimi-k2.5-free as first entry
|
||||
// then - fallbackChain exists with claude-sonnet-4-6 as first entry
|
||||
expect(atlas).toBeDefined()
|
||||
expect(atlas.fallbackChain).toBeArray()
|
||||
expect(atlas.fallbackChain.length).toBeGreaterThan(0)
|
||||
|
||||
const primary = atlas.fallbackChain[0]
|
||||
expect(primary.model).toBe("kimi-k2.5-free")
|
||||
expect(primary.providers[0]).toBe("opencode")
|
||||
expect(primary.model).toBe("claude-sonnet-4-6")
|
||||
expect(primary.providers[0]).toBe("anthropic")
|
||||
})
|
||||
|
||||
test("hephaestus supports openai, github-copilot, venice, and opencode providers", () => {
|
||||
@@ -335,27 +343,23 @@ describe("CATEGORY_MODEL_REQUIREMENTS", () => {
|
||||
expect(primary.providers[0]).toBe("google")
|
||||
})
|
||||
|
||||
test("writing has valid fallbackChain with kimi-k2.5-free as primary", () => {
|
||||
test("writing has valid fallbackChain with gemini-3-flash as primary", () => {
|
||||
// given - writing category requirement
|
||||
const writing = CATEGORY_MODEL_REQUIREMENTS["writing"]
|
||||
|
||||
// when - accessing writing requirement
|
||||
// then - fallbackChain: kimi-k2.5-free -> gemini-3-flash -> claude-sonnet-4-6
|
||||
// then - fallbackChain: gemini-3-flash -> claude-sonnet-4-6
|
||||
expect(writing).toBeDefined()
|
||||
expect(writing.fallbackChain).toBeArray()
|
||||
expect(writing.fallbackChain).toHaveLength(3)
|
||||
expect(writing.fallbackChain).toHaveLength(2)
|
||||
|
||||
const primary = writing.fallbackChain[0]
|
||||
expect(primary.model).toBe("kimi-k2.5-free")
|
||||
expect(primary.providers[0]).toBe("opencode")
|
||||
expect(primary.model).toBe("gemini-3-flash")
|
||||
expect(primary.providers[0]).toBe("google")
|
||||
|
||||
const second = writing.fallbackChain[1]
|
||||
expect(second.model).toBe("gemini-3-flash")
|
||||
expect(second.providers[0]).toBe("google")
|
||||
|
||||
const third = writing.fallbackChain[2]
|
||||
expect(third.model).toBe("claude-sonnet-4-6")
|
||||
expect(third.providers[0]).toBe("anthropic")
|
||||
expect(second.model).toBe("claude-sonnet-4-6")
|
||||
expect(second.providers[0]).toBe("anthropic")
|
||||
})
|
||||
|
||||
test("all 8 categories have valid fallbackChain arrays", () => {
|
||||
|
||||
@@ -16,7 +16,6 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
sisyphus: {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["zai-coding-plan", "opencode"], model: "glm-5" },
|
||||
{ providers: ["opencode"], model: "big-pickle" },
|
||||
],
|
||||
@@ -53,9 +52,9 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
},
|
||||
"multimodal-looker": {
|
||||
fallbackChain: [
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["openai", "opencode"], model: "gpt-5.3-codex", variant: "medium" },
|
||||
{ providers: ["kimi-for-coding"], model: "k2p5" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.6v" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5-nano" },
|
||||
],
|
||||
@@ -64,14 +63,12 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro" },
|
||||
],
|
||||
},
|
||||
metis: {
|
||||
fallbackChain: [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-opus-4-6", variant: "max" },
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3.1-pro", variant: "high" },
|
||||
],
|
||||
@@ -85,7 +82,6 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
},
|
||||
atlas: {
|
||||
fallbackChain: [
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2" },
|
||||
],
|
||||
@@ -146,7 +142,6 @@ export const CATEGORY_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
},
|
||||
writing: {
|
||||
fallbackChain: [
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-flash" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-6" },
|
||||
],
|
||||
|
||||
@@ -2,6 +2,6 @@ export const BACKGROUND_TASK_DESCRIPTION = `Run agent task in background. Return
|
||||
|
||||
Use \`background_output\` to get results. Prompts MUST be in English.`
|
||||
|
||||
export const BACKGROUND_OUTPUT_DESCRIPTION = `Get output from background task. Use full_session=true to fetch session messages with filters. System notifies on completion, so block=true rarely needed.`
|
||||
export const BACKGROUND_OUTPUT_DESCRIPTION = `Get output from background task. Use full_session=true to fetch session messages with filters. System notifies on completion, so block=true rarely needed. - Timeout values are in milliseconds (ms), NOT seconds.`
|
||||
|
||||
export const BACKGROUND_CANCEL_DESCRIPTION = `Cancel running background task(s). Use all=true to cancel ALL before final answer.`
|
||||
|
||||
@@ -33,6 +33,7 @@ export async function executeBackgroundContinuation(
|
||||
run_in_background: args.run_in_background,
|
||||
sessionId: task.sessionID,
|
||||
command: args.command,
|
||||
model: task.model ? { providerID: task.model.providerID, modelID: task.model.modelID } : undefined,
|
||||
},
|
||||
}
|
||||
await ctx.metadata?.(bgContMeta)
|
||||
|
||||
158
src/tools/delegate-task/background-task.test.ts
Normal file
158
src/tools/delegate-task/background-task.test.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
const bunTest = require("bun:test")
|
||||
const describeFn = bunTest.describe
|
||||
const testFn = bunTest.test
|
||||
const expectFn = bunTest.expect
|
||||
const beforeEachFn = bunTest.beforeEach
|
||||
const afterEachFn = bunTest.afterEach
|
||||
|
||||
const { executeBackgroundTask } = require("./background-task")
|
||||
const { __setTimingConfig, __resetTimingConfig } = require("./timing")
|
||||
|
||||
describeFn("executeBackgroundTask output/session metadata compatibility", () => {
|
||||
beforeEachFn(() => {
|
||||
//#given - reduce waiting to keep tests fast
|
||||
__setTimingConfig({
|
||||
WAIT_FOR_SESSION_INTERVAL_MS: 1,
|
||||
WAIT_FOR_SESSION_TIMEOUT_MS: 2,
|
||||
})
|
||||
})
|
||||
|
||||
afterEachFn(() => {
|
||||
__resetTimingConfig()
|
||||
})
|
||||
|
||||
testFn("does not emit synthetic pending session metadata when session id is unresolved", async () => {
|
||||
//#given - launched task without resolved subagent session id
|
||||
const metadataCalls: any[] = []
|
||||
const manager = {
|
||||
launch: async () => ({
|
||||
id: "bg_unresolved",
|
||||
sessionID: undefined,
|
||||
description: "Unresolved session",
|
||||
agent: "explore",
|
||||
status: "running",
|
||||
}),
|
||||
getTask: () => undefined,
|
||||
}
|
||||
|
||||
const result = await executeBackgroundTask(
|
||||
{
|
||||
description: "Unresolved session",
|
||||
prompt: "check",
|
||||
run_in_background: true,
|
||||
load_skills: [],
|
||||
},
|
||||
{
|
||||
sessionID: "ses_parent",
|
||||
callID: "call_1",
|
||||
metadata: async (value: any) => metadataCalls.push(value),
|
||||
abort: new AbortController().signal,
|
||||
},
|
||||
{ manager },
|
||||
{ sessionID: "ses_parent", messageID: "msg_1" },
|
||||
"explore",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
)
|
||||
|
||||
//#then - output and metadata should avoid fake session markers
|
||||
expectFn(result).not.toContain("<task_metadata>")
|
||||
expectFn(result).not.toContain("session_id: undefined")
|
||||
expectFn(result).not.toContain("session_id: pending")
|
||||
expectFn(metadataCalls).toHaveLength(1)
|
||||
expectFn("sessionId" in metadataCalls[0].metadata).toBe(false)
|
||||
})
|
||||
|
||||
testFn("emits task metadata session_id when real session id is available", async () => {
|
||||
//#given - launched task with resolved subagent session id
|
||||
const metadataCalls: any[] = []
|
||||
const manager = {
|
||||
launch: async () => ({
|
||||
id: "bg_resolved",
|
||||
sessionID: "ses_sub_123",
|
||||
description: "Resolved session",
|
||||
agent: "explore",
|
||||
status: "running",
|
||||
}),
|
||||
getTask: () => ({ sessionID: "ses_sub_123" }),
|
||||
}
|
||||
|
||||
const result = await executeBackgroundTask(
|
||||
{
|
||||
description: "Resolved session",
|
||||
prompt: "check",
|
||||
run_in_background: true,
|
||||
load_skills: [],
|
||||
},
|
||||
{
|
||||
sessionID: "ses_parent",
|
||||
callID: "call_2",
|
||||
metadata: async (value: any) => metadataCalls.push(value),
|
||||
abort: new AbortController().signal,
|
||||
},
|
||||
{ manager },
|
||||
{ sessionID: "ses_parent", messageID: "msg_2" },
|
||||
"explore",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
)
|
||||
|
||||
//#then - output and metadata should include canonical session linkage
|
||||
expectFn(result).toContain("<task_metadata>")
|
||||
expectFn(result).toContain("session_id: ses_sub_123")
|
||||
expectFn(result).toContain("task_id: ses_sub_123")
|
||||
expectFn(result).toContain("background_task_id: bg_resolved")
|
||||
expectFn(result).toContain("Background Task ID: bg_resolved")
|
||||
expectFn(metadataCalls).toHaveLength(1)
|
||||
expectFn(metadataCalls[0].metadata.sessionId).toBe("ses_sub_123")
|
||||
})
|
||||
|
||||
testFn("captures late-resolved session id and emits synced metadata", async () => {
|
||||
//#given - background task session id appears after launch via manager polling
|
||||
const metadataCalls: any[] = []
|
||||
let reads = 0
|
||||
const manager = {
|
||||
launch: async () => ({
|
||||
id: "bg_late",
|
||||
sessionID: undefined,
|
||||
description: "Late session",
|
||||
agent: "explore",
|
||||
status: "running",
|
||||
}),
|
||||
getTask: () => {
|
||||
reads += 1
|
||||
return reads >= 2 ? { sessionID: "ses_late_123" } : undefined
|
||||
},
|
||||
}
|
||||
|
||||
const result = await executeBackgroundTask(
|
||||
{
|
||||
description: "Late session",
|
||||
prompt: "check",
|
||||
run_in_background: true,
|
||||
load_skills: [],
|
||||
},
|
||||
{
|
||||
sessionID: "ses_parent",
|
||||
callID: "call_3",
|
||||
metadata: async (value: any) => metadataCalls.push(value),
|
||||
abort: new AbortController().signal,
|
||||
},
|
||||
{ manager },
|
||||
{ sessionID: "ses_parent", messageID: "msg_3" },
|
||||
"explore",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
)
|
||||
|
||||
//#then - late session id still propagates to task metadata contract
|
||||
expectFn(result).toContain("session_id: ses_late_123")
|
||||
expectFn(result).toContain("task_id: ses_late_123")
|
||||
expectFn(result).toContain("background_task_id: bg_late")
|
||||
expectFn(metadataCalls).toHaveLength(1)
|
||||
expectFn(metadataCalls[0].metadata.sessionId).toBe("ses_late_123")
|
||||
})
|
||||
})
|
||||
@@ -56,36 +56,39 @@ export async function executeBackgroundTask(
|
||||
SessionCategoryRegistry.register(sessionId, args.category)
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
prompt: args.prompt,
|
||||
agent: task.agent,
|
||||
category: args.category,
|
||||
load_skills: args.load_skills,
|
||||
description: args.description,
|
||||
run_in_background: args.run_in_background,
|
||||
command: args.command,
|
||||
...(sessionId ? { sessionId } : {}),
|
||||
...(categoryModel ? { model: { providerID: categoryModel.providerID, modelID: categoryModel.modelID } } : {}),
|
||||
}
|
||||
|
||||
const unstableMeta = {
|
||||
title: args.description,
|
||||
metadata: {
|
||||
prompt: args.prompt,
|
||||
agent: task.agent,
|
||||
category: args.category,
|
||||
load_skills: args.load_skills,
|
||||
description: args.description,
|
||||
run_in_background: args.run_in_background,
|
||||
sessionId: sessionId ?? "pending",
|
||||
command: args.command,
|
||||
},
|
||||
metadata,
|
||||
}
|
||||
await ctx.metadata?.(unstableMeta)
|
||||
if (ctx.callID) {
|
||||
storeToolMetadata(ctx.sessionID, ctx.callID, unstableMeta)
|
||||
}
|
||||
|
||||
const taskMetadataBlock = sessionId
|
||||
? `\n\n<task_metadata>\nsession_id: ${sessionId}\ntask_id: ${sessionId}\nbackground_task_id: ${task.id}\n</task_metadata>`
|
||||
: ""
|
||||
|
||||
return `Background task launched.
|
||||
|
||||
Task ID: ${task.id}
|
||||
Background Task ID: ${task.id}
|
||||
Description: ${task.description}
|
||||
Agent: ${task.agent}${args.category ? ` (category: ${args.category})` : ""}
|
||||
Status: ${task.status}
|
||||
|
||||
System notifies on completion. Use \`background_output\` with task_id="${task.id}" to check.
|
||||
|
||||
<task_metadata>
|
||||
session_id: ${sessionId}
|
||||
</task_metadata>`
|
||||
System notifies on completion. Use \`background_output\` with task_id="${task.id}" to check.${taskMetadataBlock}`
|
||||
} catch (error) {
|
||||
return formatDetailedError(error, {
|
||||
operation: "Launch background task",
|
||||
|
||||
172
src/tools/delegate-task/metadata-model-unification.test.ts
Normal file
172
src/tools/delegate-task/metadata-model-unification.test.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
const { describe, test, expect, mock } = require("bun:test")
|
||||
|
||||
import type { DelegateTaskArgs, ToolContextWithMetadata } from "./types"
|
||||
import type { ParentContext } from "./executor-types"
|
||||
|
||||
const MODEL = { providerID: "anthropic", modelID: "claude-sonnet-4-6" }
|
||||
|
||||
function makeMockCtx(): ToolContextWithMetadata & { captured: any[] } {
|
||||
const captured: any[] = []
|
||||
return {
|
||||
sessionID: "ses_parent",
|
||||
messageID: "msg_parent",
|
||||
agent: "sisyphus",
|
||||
abort: new AbortController().signal,
|
||||
callID: "call_001",
|
||||
metadata: async (input: any) => { captured.push(input) },
|
||||
captured,
|
||||
}
|
||||
}
|
||||
|
||||
const parentContext: ParentContext = {
|
||||
sessionID: "ses_parent",
|
||||
messageID: "msg_parent",
|
||||
agent: "sisyphus",
|
||||
model: MODEL,
|
||||
}
|
||||
|
||||
describe("metadata model unification", () => {
|
||||
describe("#given delegate-task executors", () => {
|
||||
describe("#when metadata is set during execution", () => {
|
||||
|
||||
test("#then sync-task metadata includes model", async () => {
|
||||
const { executeSyncTask } = require("./sync-task")
|
||||
const ctx = makeMockCtx()
|
||||
const deps = {
|
||||
createSyncSession: async () => ({ ok: true, sessionID: "ses_sync" }),
|
||||
sendSyncPrompt: async () => null,
|
||||
pollSyncSession: async () => null,
|
||||
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
||||
}
|
||||
const args: DelegateTaskArgs = {
|
||||
description: "test", prompt: "do it",
|
||||
category: "quick", load_skills: [], run_in_background: false,
|
||||
}
|
||||
|
||||
await executeSyncTask(args, ctx, {
|
||||
client: { session: { create: async () => ({ data: { id: "ses_sync" } }) } },
|
||||
directory: "/tmp",
|
||||
onSyncSessionCreated: null,
|
||||
}, parentContext, "explore", MODEL, undefined, undefined, undefined, deps)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
expect(meta.metadata.model).toEqual(MODEL)
|
||||
})
|
||||
|
||||
test("#then background-task metadata includes model", async () => {
|
||||
const { executeBackgroundTask } = require("./background-task")
|
||||
const ctx = makeMockCtx()
|
||||
const args: DelegateTaskArgs = {
|
||||
description: "test", prompt: "do it",
|
||||
load_skills: [], run_in_background: true, subagent_type: "explore",
|
||||
}
|
||||
|
||||
await executeBackgroundTask(args, ctx, {
|
||||
manager: {
|
||||
launch: async () => ({
|
||||
id: "bg_1", description: "test", agent: "explore",
|
||||
status: "pending", sessionID: "ses_bg", model: MODEL,
|
||||
}),
|
||||
getTask: () => undefined,
|
||||
},
|
||||
} as any, parentContext, "explore", MODEL, undefined)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
expect(meta.metadata.model).toEqual(MODEL)
|
||||
})
|
||||
|
||||
test("#then unstable-agent-task metadata includes model", async () => {
|
||||
const { executeUnstableAgentTask } = require("./unstable-agent-task")
|
||||
const ctx = makeMockCtx()
|
||||
const args: DelegateTaskArgs = {
|
||||
description: "test", prompt: "do it",
|
||||
category: "quick", load_skills: [], run_in_background: false,
|
||||
}
|
||||
|
||||
const launchedTask = {
|
||||
id: "bg_unstable", description: "test", agent: "explore",
|
||||
status: "completed", sessionID: "ses_unstable", model: MODEL,
|
||||
}
|
||||
const result = await executeUnstableAgentTask(
|
||||
args, ctx,
|
||||
{
|
||||
manager: {
|
||||
launch: async () => launchedTask,
|
||||
getTask: () => launchedTask,
|
||||
},
|
||||
client: {
|
||||
session: {
|
||||
status: async () => ({ data: { ses_unstable: { type: "idle" } } }),
|
||||
messages: async () => ({
|
||||
data: [{
|
||||
info: { role: "assistant", time: { created: 1 } },
|
||||
parts: [{ type: "text", text: "done" }],
|
||||
}],
|
||||
}),
|
||||
},
|
||||
},
|
||||
syncPollTimeoutMs: 100,
|
||||
} as any,
|
||||
parentContext, "explore", MODEL, undefined, "anthropic/claude-sonnet-4-6",
|
||||
)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
expect(meta.metadata.model).toEqual(MODEL)
|
||||
})
|
||||
|
||||
test("#then background-continuation metadata includes model from task", async () => {
|
||||
const { executeBackgroundContinuation } = require("./background-continuation")
|
||||
const ctx = makeMockCtx()
|
||||
const args: DelegateTaskArgs = {
|
||||
description: "continue", prompt: "keep going",
|
||||
load_skills: [], run_in_background: true, session_id: "ses_resumed",
|
||||
}
|
||||
|
||||
await executeBackgroundContinuation(args, ctx, {
|
||||
manager: {
|
||||
resume: async () => ({
|
||||
id: "bg_2", description: "continue", agent: "explore",
|
||||
status: "running", sessionID: "ses_resumed", model: MODEL,
|
||||
}),
|
||||
},
|
||||
} as any, parentContext)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
expect(meta.metadata.model).toEqual(MODEL)
|
||||
})
|
||||
|
||||
test("#then sync-continuation metadata includes model from resumed session", async () => {
|
||||
const { executeSyncContinuation } = require("./sync-continuation")
|
||||
const ctx = makeMockCtx()
|
||||
const args: DelegateTaskArgs = {
|
||||
description: "continue", prompt: "keep going",
|
||||
load_skills: [], run_in_background: false, session_id: "ses_cont",
|
||||
}
|
||||
|
||||
const deps = {
|
||||
pollSyncSession: async () => null,
|
||||
fetchSyncResult: async () => ({ ok: true as const, textContent: "done" }),
|
||||
}
|
||||
|
||||
await executeSyncContinuation(args, ctx, {
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({
|
||||
data: [{ info: { agent: "explore", model: MODEL, providerID: "anthropic", modelID: "claude-sonnet-4-6" } }],
|
||||
}),
|
||||
prompt: async () => ({}),
|
||||
},
|
||||
},
|
||||
} as any, deps)
|
||||
|
||||
const meta = ctx.captured.find((m: any) => m.metadata?.sessionId)
|
||||
expect(meta).toBeDefined()
|
||||
expect(meta.metadata.model).toEqual(MODEL)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -32,22 +32,7 @@ export async function executeSyncContinuation(
|
||||
})
|
||||
}
|
||||
|
||||
const syncContMeta = {
|
||||
title: `Continue: ${args.description}`,
|
||||
metadata: {
|
||||
prompt: args.prompt,
|
||||
load_skills: args.load_skills,
|
||||
description: args.description,
|
||||
run_in_background: args.run_in_background,
|
||||
sessionId: args.session_id,
|
||||
sync: true,
|
||||
command: args.command,
|
||||
},
|
||||
}
|
||||
await ctx.metadata?.(syncContMeta)
|
||||
if (ctx.callID) {
|
||||
storeToolMetadata(ctx.sessionID, ctx.callID, syncContMeta)
|
||||
}
|
||||
let syncContMeta: { title: string; metadata: Record<string, unknown> } | undefined
|
||||
|
||||
let resumeAgent: string | undefined
|
||||
let resumeModel: { providerID: string; modelID: string } | undefined
|
||||
@@ -78,6 +63,24 @@ export async function executeSyncContinuation(
|
||||
resumeVariant = resumeMessage?.model?.variant
|
||||
}
|
||||
|
||||
syncContMeta = {
|
||||
title: `Continue: ${args.description}`,
|
||||
metadata: {
|
||||
prompt: args.prompt,
|
||||
load_skills: args.load_skills,
|
||||
description: args.description,
|
||||
run_in_background: args.run_in_background,
|
||||
sessionId: args.session_id,
|
||||
sync: true,
|
||||
command: args.command,
|
||||
model: resumeModel,
|
||||
},
|
||||
}
|
||||
await ctx.metadata?.(syncContMeta)
|
||||
if (ctx.callID) {
|
||||
storeToolMetadata(ctx.sessionID, ctx.callID, syncContMeta)
|
||||
}
|
||||
|
||||
const allowTask = isPlanFamily(resumeAgent)
|
||||
const tools = {
|
||||
...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}),
|
||||
|
||||
@@ -91,6 +91,7 @@ export async function executeSyncTask(
|
||||
sessionId: sessionID,
|
||||
sync: true,
|
||||
command: args.command,
|
||||
model: categoryModel ? { providerID: categoryModel.providerID, modelID: categoryModel.modelID } : undefined,
|
||||
},
|
||||
}
|
||||
await ctx.metadata?.(syncTaskMeta)
|
||||
|
||||
@@ -66,6 +66,7 @@ export async function executeUnstableAgentTask(
|
||||
run_in_background: args.run_in_background,
|
||||
sessionId: sessionID,
|
||||
command: args.command,
|
||||
model: categoryModel ? { providerID: categoryModel.providerID, modelID: categoryModel.modelID } : undefined,
|
||||
},
|
||||
}
|
||||
await ctx.metadata?.(bgTaskMeta)
|
||||
|
||||
@@ -456,6 +456,96 @@ describe("look-at tool", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("createLookAt unhandled error resilience", () => {
|
||||
const createToolContext = (): ToolContext => ({
|
||||
sessionID: "parent-session",
|
||||
messageID: "parent-message",
|
||||
agent: "sisyphus",
|
||||
directory: "/project",
|
||||
worktree: "/project",
|
||||
abort: new AbortController().signal,
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
})
|
||||
|
||||
// given session.create throws (network error, not error response)
|
||||
// when LookAt tool executed
|
||||
// then returns error string instead of crashing
|
||||
test("catches session.create throw and returns error string", async () => {
|
||||
const mockClient = {
|
||||
session: {
|
||||
get: async () => ({ data: { directory: "/project" } }),
|
||||
create: async () => { throw new Error("ECONNREFUSED: connection refused") },
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
|
||||
const result = await tool.execute(
|
||||
{ file_path: "/test/file.png", goal: "analyze" },
|
||||
createToolContext(),
|
||||
)
|
||||
expect(result).toContain("Error")
|
||||
expect(result).toContain("ECONNREFUSED")
|
||||
})
|
||||
|
||||
// given session.messages throws unexpectedly
|
||||
// when LookAt tool executed
|
||||
// then returns error string instead of crashing
|
||||
test("catches session.messages throw and returns error string", async () => {
|
||||
const mockClient = {
|
||||
app: {
|
||||
agents: async () => ({ data: [] }),
|
||||
},
|
||||
session: {
|
||||
get: async () => ({ data: { directory: "/project" } }),
|
||||
create: async () => ({ data: { id: "ses_msg_throw" } }),
|
||||
prompt: async () => ({}),
|
||||
messages: async () => { throw new Error("Unexpected server error") },
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
|
||||
const result = await tool.execute(
|
||||
{ file_path: "/test/file.png", goal: "analyze" },
|
||||
createToolContext(),
|
||||
)
|
||||
expect(result).toContain("Error")
|
||||
expect(result).toContain("Unexpected server error")
|
||||
})
|
||||
|
||||
// given a non-Error object is thrown
|
||||
// when LookAt tool executed
|
||||
// then still returns error string
|
||||
test("handles non-Error thrown objects gracefully", async () => {
|
||||
const mockClient = {
|
||||
session: {
|
||||
get: async () => ({ data: { directory: "/project" } }),
|
||||
create: async () => { throw "string error thrown" },
|
||||
},
|
||||
}
|
||||
|
||||
const tool = createLookAt({
|
||||
client: mockClient,
|
||||
directory: "/project",
|
||||
} as any)
|
||||
|
||||
const result = await tool.execute(
|
||||
{ file_path: "/test/file.png", goal: "analyze" },
|
||||
createToolContext(),
|
||||
)
|
||||
expect(result).toContain("Error")
|
||||
expect(result).toContain("string error thrown")
|
||||
})
|
||||
})
|
||||
|
||||
describe("createLookAt with image_data", () => {
|
||||
// given base64 image data is provided
|
||||
// when LookAt tool executed
|
||||
|
||||
@@ -217,6 +217,10 @@ Original error: ${createResult.error}`
|
||||
|
||||
log(`[look_at] Got response, length: ${responseText.length}`)
|
||||
return responseText
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
log(`[look_at] Unexpected error analyzing ${sourceDescription}:`, error)
|
||||
return `Error: Failed to analyze ${sourceDescription}: ${errorMessage}`
|
||||
} finally {
|
||||
if (tempConversionPath) {
|
||||
cleanupConvertedImage(tempConversionPath)
|
||||
|
||||
Reference in New Issue
Block a user