Compare commits
1 Commits
feat/git-m
...
feat/multi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2a9d8a9e0 |
75
bun.lock
75
bun.lock
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"configVersion": 0,
|
||||
"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.2.16",
|
||||
"@opencode-ai/sdk": "^1.2.17",
|
||||
"@opencode-ai/plugin": "^1.1.19",
|
||||
"@opencode-ai/sdk": "^1.1.19",
|
||||
"commander": "^14.0.2",
|
||||
"detect-libc": "^2.0.0",
|
||||
"diff": "^8.0.3",
|
||||
@@ -48,45 +48,42 @@
|
||||
"@ast-grep/napi",
|
||||
"@code-yeongyu/comment-checker",
|
||||
],
|
||||
"overrides": {
|
||||
"@opencode-ai/sdk": "^1.2.17",
|
||||
},
|
||||
"packages": {
|
||||
"@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": ["@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-darwin-arm64": ["@ast-grep/cli-darwin-arm64@0.40.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-T9CzwJ1GqQhnANdsu6c7iT1akpvTVMK+AZrxnhIPv33Ze5hrXUUkqan+j4wUAukRJDqU7u94EhXLSLD+5tcJ8g=="],
|
||||
"@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-x64": ["@ast-grep/cli-darwin-x64@0.40.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-ez9b2zKvXU8f4ghhjlqYvbx6tWCKJTuVlNVqDDfjqwwhGeiTYfnzMlSVat4ElYRMd21gLtXZIMy055v2f21Ztg=="],
|
||||
"@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-linux-arm64-gnu": ["@ast-grep/cli-linux-arm64-gnu@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-VXa2L1IEYD66AMb0GuG7VlMMbPmEGoJUySWDcwSZo/D9neiry3MJ41LQR5oTG2HyhIPBsf9umrXnmuRq66BviA=="],
|
||||
"@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-x64-gnu": ["@ast-grep/cli-linux-x64-gnu@0.40.5", "", { "os": "linux", "cpu": "x64" }, "sha512-GQC5162eIOWXR2eQQ6Knzg7/8Trp5E1ODJkaErf0IubdQrZBGqj5AAcQPcWgPbbnmktjIp0H4NraPpOJ9eJ22A=="],
|
||||
"@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-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-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-ia32-msvc": ["@ast-grep/cli-win32-ia32-msvc@0.40.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-MHkCxCITVTr8sY9CcVqNKbfUzMa3Hc6IilGXad0Clnw2vNmPfWqSky+hU/UTerr5YHWwWfAVURH7ANZgirtx0Q=="],
|
||||
"@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-x64-msvc": ["@ast-grep/cli-win32-x64-msvc@0.40.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/MJ5un7yxlClaaxou9eYl+Kr2xr/yTtYtTq5aLBWjPWA6dmmJ1nAJgx5zKHVuplFXFBrFDQk3paEgAETMTGcrA=="],
|
||||
"@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/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": ["@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-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2F072fGN0WTq7KI3okuEnkGJVEHLbi56Bw1H6NAMf7j2mJJeQWsRyGOMcyNnUXZDeNdvoMH0OB2a5wwUegY/nQ=="],
|
||||
"@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZMjl5yLhKjxdwbqEEdMizgQdWH2NrWsM6Px+JuGErgCDe6Aedq9yurEPV7veybGdLVJQhOah6htlSflXxjHnYA=="],
|
||||
|
||||
"@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-darwin-x64": ["@ast-grep/napi-darwin-x64@0.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-f9Ol5oQKNRMBkvDtzBK1WiNn2/3eejF2Pn9xwTj7PhXuSFseedOspPYllxQo0gbwUlw/DJqGFTce/jarhR/rBw=="],
|
||||
|
||||
"@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-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-musl": ["@ast-grep/napi-linux-arm64-musl@0.40.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-/qKsmds5FMoaEj6FdNzepbmLMtlFuBLdrAn9GIWCqOIcVcYvM1Nka8+mncfeXB/MFZKOrzQsQdPTWqrrQzXLrA=="],
|
||||
"@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-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-gnu": ["@ast-grep/napi-linux-x64-gnu@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BeHZVMNXhM3WV3XE2yghO0fRxhMOt8BTN972p5piYEQUvKeSHmS8oeGcs6Ahgx5znBclqqqq37ZfioYANiTqJA=="],
|
||||
|
||||
"@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-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rG1YujF7O+lszX8fd5u6qkFTuv4FwHXjWvt1CCvCxXwQLSY96LaCW88oVKg7WoEYQh54y++Fk57F+Wh9Gv9nVQ=="],
|
||||
|
||||
"@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-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-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-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0JkdBZi5l9vZhGEO38A1way0LmLRDU5Vos6MXrLIOVkymmzDTDlCdY394J1LMmmsfwWcyJg6J7Yv2dw41MCxDQ=="],
|
||||
|
||||
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.5", "", { "os": "win32", "cpu": "x64" }, "sha512-dqm5zg/o4Nh4VOQPEpMS23ot8HVd22gG0eg01t4CFcZeuzyuSgBlOL3N7xLbz3iH2sVkk7keuBwAzOIpTqziNQ=="],
|
||||
"@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Hk2IwfPqMFGZt5SRxsoWmGLxBXxprow4LRp1eG6V8EEiJCNHxZ9ZiEaIc5bNvMDBjHVSnqZAXT22dROhrcSKQg=="],
|
||||
|
||||
"@clack/core": ["@clack/core@0.5.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow=="],
|
||||
|
||||
@@ -94,29 +91,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.10", "", { "peerDependencies": { "hono": "^4" } }, "sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw=="],
|
||||
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||
|
||||
"@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=="],
|
||||
"@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=="],
|
||||
|
||||
"@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/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/sdk": ["@opencode-ai/sdk@1.2.17", "", {}, "sha512-HdeLeyJ2/Yl/NBHqw9pGFBnkIXuf0Id1kX1GMXDcnZwbJROUJ6TtrW/wLngTYW478E4CCm1jwknjxxmDuxzVMQ=="],
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.19", "", {}, "sha512-XhZhFuvlLCqDpvNtUEjOsi/wvFj3YCXb1dySp+OONQRMuHlorNYnNa7P2A2ntKuhRdGT1Xt5na0nFzlUyNw+4A=="],
|
||||
|
||||
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||
|
||||
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
|
||||
|
||||
"@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.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": ["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-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.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=="],
|
||||
"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=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
||||
|
||||
@@ -126,7 +123,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.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||
"commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
@@ -136,7 +133,7 @@
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
@@ -194,11 +191,11 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.12.5", "", {}, "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg=="],
|
||||
"hono": ["hono@4.12.0", "", {}, "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA=="],
|
||||
|
||||
"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.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
"iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
@@ -278,7 +275,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.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
"qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
@@ -318,7 +315,7 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
@@ -330,10 +327,8 @@
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||
|
||||
"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.2.16",
|
||||
"@opencode-ai/sdk": "^1.2.17",
|
||||
"@opencode-ai/plugin": "^1.1.19",
|
||||
"@opencode-ai/sdk": "^1.1.19",
|
||||
"commander": "^14.0.2",
|
||||
"detect-libc": "^2.0.0",
|
||||
"diff": "^8.0.3",
|
||||
@@ -87,9 +87,6 @@
|
||||
"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",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1895,94 +1895,6 @@
|
||||
"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": "opencode/glm-4.7-free",
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"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": "opencode/glm-4.7-free",
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "anthropic/claude-opus-4-6",
|
||||
@@ -600,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/claude-sonnet-4-5",
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
@@ -675,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/claude-sonnet-4-5",
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
@@ -994,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": "anthropic/claude-sonnet-4-5",
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
},
|
||||
"explore": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
@@ -1271,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": "github-copilot/claude-sonnet-4.5",
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
@@ -1346,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": "anthropic/claude-sonnet-4-5",
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
},
|
||||
"explore": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
@@ -1421,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": "anthropic/claude-sonnet-4-5",
|
||||
"model": "opencode/kimi-k2.5-free",
|
||||
},
|
||||
"explore": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
|
||||
@@ -21,9 +21,19 @@ describe("runCliInstaller", () => {
|
||||
console.error = originalConsoleError
|
||||
})
|
||||
|
||||
it("completes installation without auth plugin or provider config steps", async () => {
|
||||
it("runs auth and provider setup steps when openai or copilot are enabled without gemini", 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,
|
||||
@@ -63,6 +73,8 @@ describe("runCliInstaller", () => {
|
||||
|
||||
//#then
|
||||
expect(result).toBe(0)
|
||||
expect(addAuthPluginsSpy).toHaveBeenCalledTimes(1)
|
||||
expect(addProviderConfigSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
for (const spy of restoreSpies) {
|
||||
spy.mockRestore()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import color from "picocolors"
|
||||
import type { InstallArgs } from "./types"
|
||||
import {
|
||||
addAuthPlugins,
|
||||
addPluginToOpenCodeConfig,
|
||||
addProviderConfig,
|
||||
detectCurrentConfig,
|
||||
getOpenCodeVersion,
|
||||
isOpenCodeInstalled,
|
||||
@@ -43,7 +45,7 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
|
||||
printHeader(isUpdate)
|
||||
|
||||
const totalSteps = 4
|
||||
const totalSteps = 6
|
||||
let step = 1
|
||||
|
||||
printStep(step++, totalSteps, "Checking OpenCode installation...")
|
||||
@@ -75,6 +77,28 @@ 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) {
|
||||
@@ -132,7 +156,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("→ Gemini")}\n` : "") +
|
||||
(config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\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 { getPluginNameWithVersion, fetchNpmDistTags, generateOmoConfig } from "./config-manager"
|
||||
import { ANTIGRAVITY_PROVIDER_CONFIG, getPluginNameWithVersion, fetchNpmDistTags, generateOmoConfig } from "./config-manager"
|
||||
import type { InstallConfig } from "./types"
|
||||
|
||||
describe("getPluginNameWithVersion", () => {
|
||||
@@ -169,6 +169,76 @@ 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)
|
||||
@@ -253,7 +323,7 @@ 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 (first fallback entry is gpt-5.3-codex)
|
||||
// #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.3-codex")
|
||||
})
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ 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"
|
||||
|
||||
205
src/cli/config-manager/add-provider-config.test.ts
Normal file
205
src/cli/config-manager/add-provider-config.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
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'])
|
||||
})
|
||||
})
|
||||
})
|
||||
82
src/cli/config-manager/add-provider-config.ts
Normal file
82
src/cli/config-manager/add-provider-config.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/cli/config-manager/antigravity-provider-configuration.ts
Normal file
64
src/cli/config-manager/antigravity-provider-configuration.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
230
src/cli/config-manager/auth-plugins.test.ts
Normal file
230
src/cli/config-manager/auth-plugins.test.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
140
src/cli/config-manager/auth-plugins.ts
Normal file
140
src/cli/config-manager/auth-plugins.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
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,8 +66,7 @@ export function detectCurrentConfig(): DetectedConfig {
|
||||
return result
|
||||
}
|
||||
|
||||
const providers = openCodeConfig.provider as Record<string, unknown> | undefined
|
||||
result.hasGemini = providers ? "google" in providers : false
|
||||
result.hasGemini = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
|
||||
|
||||
const { hasOpenAI, hasOpencodeZen, hasZaiCodingPlan, hasKimiForCoding } = detectProvidersFromOmoConfig()
|
||||
result.hasOpenAI = hasOpenAI
|
||||
|
||||
11
src/cli/config-manager/jsonc-provider-editor.ts
Normal file
11
src/cli/config-manager/jsonc-provider-editor.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
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,6 +9,7 @@ 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,9 +45,11 @@ export const CLI_AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
"multimodal-looker": {
|
||||
fallbackChain: [
|
||||
{ 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: ["zai-coding-plan"], model: "glm-4.6v" },
|
||||
{ providers: ["kimi-for-coding"], model: "k2p5" },
|
||||
{ providers: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-haiku-4-5" },
|
||||
{ providers: ["opencode"], model: "gpt-5-nano" },
|
||||
],
|
||||
},
|
||||
@@ -54,6 +57,7 @@ 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" },
|
||||
],
|
||||
@@ -62,6 +66,7 @@ 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" },
|
||||
],
|
||||
@@ -76,6 +81,7 @@ 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,7 +2,9 @@ import * as p from "@clack/prompts"
|
||||
import color from "picocolors"
|
||||
import type { InstallArgs } from "./types"
|
||||
import {
|
||||
addAuthPlugins,
|
||||
addPluginToOpenCodeConfig,
|
||||
addProviderConfig,
|
||||
detectCurrentConfig,
|
||||
getOpenCodeVersion,
|
||||
isOpenCodeInstalled,
|
||||
@@ -52,6 +54,26 @@ 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) {
|
||||
@@ -101,7 +123,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("→ Gemini")}`)
|
||||
if (config.hasGemini) providers.push(`Google ${color.gray("→ OAuth with Antigravity")}`)
|
||||
if (config.hasCopilot) providers.push(`GitHub ${color.gray("→ Copilot")}`)
|
||||
|
||||
console.log()
|
||||
|
||||
@@ -884,25 +884,6 @@ describe("GitMasterConfigSchema", () => {
|
||||
//#then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test("accepts shell-safe git_env_prefix", () => {
|
||||
const config = { git_env_prefix: "MY_HOOK=active" }
|
||||
|
||||
const result = GitMasterConfigSchema.safeParse(config)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.git_env_prefix).toBe("MY_HOOK=active")
|
||||
}
|
||||
})
|
||||
|
||||
test("rejects git_env_prefix with shell metacharacters", () => {
|
||||
const config = { git_env_prefix: "A=1; rm -rf /" }
|
||||
|
||||
const result = GitMasterConfigSchema.safeParse(config)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("skills schema", () => {
|
||||
|
||||
@@ -10,7 +10,6 @@ export * from "./schema/commands"
|
||||
export * from "./schema/dynamic-context-pruning"
|
||||
export * from "./schema/experimental"
|
||||
export * from "./schema/fallback-models"
|
||||
export * from "./schema/git-env-prefix"
|
||||
export * from "./schema/git-master"
|
||||
export * from "./schema/hooks"
|
||||
export * from "./schema/notification"
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const GIT_ENV_ASSIGNMENT_PATTERN =
|
||||
/^(?:[A-Za-z_][A-Za-z0-9_]*=[A-Za-z0-9_-]*)(?: [A-Za-z_][A-Za-z0-9_]*=[A-Za-z0-9_-]*)*$/
|
||||
|
||||
export const GIT_ENV_PREFIX_VALIDATION_MESSAGE =
|
||||
'git_env_prefix must be empty or use shell-safe env assignments like "GIT_MASTER=1"'
|
||||
|
||||
export function isValidGitEnvPrefix(value: string): boolean {
|
||||
if (value === "") {
|
||||
return true
|
||||
}
|
||||
|
||||
return GIT_ENV_ASSIGNMENT_PATTERN.test(value)
|
||||
}
|
||||
|
||||
export function assertValidGitEnvPrefix(value: string): string {
|
||||
if (!isValidGitEnvPrefix(value)) {
|
||||
throw new Error(GIT_ENV_PREFIX_VALIDATION_MESSAGE)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export const GitEnvPrefixSchema = z
|
||||
.string()
|
||||
.refine(isValidGitEnvPrefix, { message: GIT_ENV_PREFIX_VALIDATION_MESSAGE })
|
||||
.default("GIT_MASTER=1")
|
||||
@@ -1,14 +1,10 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { GitEnvPrefixSchema } from "./git-env-prefix"
|
||||
|
||||
export const GitMasterConfigSchema = z.object({
|
||||
/** Add "Ultraworked with Sisyphus" footer to commit messages (default: true). Can be boolean or custom string. */
|
||||
commit_footer: z.union([z.boolean(), z.string()]).default(true),
|
||||
/** Add "Co-authored-by: Sisyphus" trailer to commit messages (default: true) */
|
||||
include_co_authored_by: z.boolean().default(true),
|
||||
/** Environment variable prefix for all git commands (default: "GIT_MASTER=1"). Set to "" to disable. Allows custom git hooks to detect git-master skill usage. */
|
||||
git_env_prefix: GitEnvPrefixSchema,
|
||||
})
|
||||
|
||||
export type GitMasterConfig = z.infer<typeof GitMasterConfigSchema>
|
||||
|
||||
@@ -26,35 +26,29 @@ agent-browser close # Close browser
|
||||
|
||||
### Navigation
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
|
||||
agent-browser open <url> # Navigate to URL
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser (aliases: quit, exit)
|
||||
agent-browser close # Close browser
|
||||
```
|
||||
|
||||
### Snapshot (page analysis)
|
||||
```bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
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 -c # Compact output
|
||||
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 (--new-tab to open in new tab)
|
||||
agent-browser click @e1 # Click
|
||||
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
|
||||
@@ -63,8 +57,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 (--selector <sel> for container)
|
||||
agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto)
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scrollintoview @e1 # Scroll element into view
|
||||
agent-browser drag @e1 @e2 # Drag and drop
|
||||
agent-browser upload @e1 file.pdf # Upload files
|
||||
```
|
||||
@@ -79,7 +73,6 @@ 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
|
||||
@@ -91,20 +84,12 @@ agent-browser is checked @e1 # Check if checked
|
||||
|
||||
### Screenshots & PDF
|
||||
```bash
|
||||
agent-browser screenshot # Screenshot (saves to temp dir if no path)
|
||||
agent-browser screenshot # Screenshot to stdout
|
||||
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)
|
||||
@@ -124,12 +109,10 @@ 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 (left/right/middle)
|
||||
agent-browser mouse down left # Press button
|
||||
agent-browser mouse up left # Release button
|
||||
agent-browser mouse wheel 100 # Scroll wheel
|
||||
```
|
||||
@@ -139,18 +122,10 @@ 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
|
||||
@@ -167,13 +142,14 @@ 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 # Same for sessionStorage
|
||||
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
|
||||
```
|
||||
|
||||
### Network
|
||||
@@ -203,59 +179,13 @@ agent-browser frame main # Back to main frame
|
||||
|
||||
### Dialogs
|
||||
```bash
|
||||
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
|
||||
agent-browser dialog accept [text] # Accept dialog
|
||||
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
|
||||
@@ -263,60 +193,19 @@ agent-browser install --with-deps # Also install system deps (Linux)
|
||||
| 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 |
|
||||
| `--full, -f` | Full page screenshot |
|
||||
| `--annotate` | Annotated screenshot with numbered labels (`AGENT_BROWSER_ANNOTATE` env) |
|
||||
| `--headed` | Show browser window (`AGENT_BROWSER_HEADED` env) |
|
||||
| `--headed` | Show browser window (not headless) |
|
||||
| `--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
|
||||
@@ -358,13 +247,6 @@ 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)
|
||||
@@ -374,13 +256,6 @@ 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
|
||||
@@ -388,6 +263,9 @@ 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)
|
||||
|
||||
@@ -397,54 +275,62 @@ agent-browser snapshot -i --json
|
||||
agent-browser get text @e1 --json
|
||||
```
|
||||
|
||||
## Local files
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
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 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 --cdp "wss://browser-service.com/cdp?token=..." snapshot # Remote via WebSocket
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
Install: `bun add -g agent-browser && agent-browser install`. Run `agent-browser --help` for all commands. Repo: https://github.com/vercel-labs/agent-browser
|
||||
|
||||
## 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
|
||||
|
||||
@@ -40,35 +40,29 @@ agent-browser close # Close browser
|
||||
|
||||
### Navigation
|
||||
\`\`\`bash
|
||||
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
|
||||
agent-browser open <url> # Navigate to URL
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser (aliases: quit, exit)
|
||||
agent-browser close # Close browser
|
||||
\`\`\`
|
||||
|
||||
### Snapshot (page analysis)
|
||||
\`\`\`bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
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 -c # Compact output
|
||||
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 (--new-tab to open in new tab)
|
||||
agent-browser click @e1 # Click
|
||||
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
|
||||
@@ -77,8 +71,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 (--selector <sel> for container)
|
||||
agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto)
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scrollintoview @e1 # Scroll element into view
|
||||
agent-browser drag @e1 @e2 # Drag and drop
|
||||
agent-browser upload @e1 file.pdf # Upload files
|
||||
\`\`\`
|
||||
@@ -93,7 +87,6 @@ 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
|
||||
@@ -105,20 +98,12 @@ agent-browser is checked @e1 # Check if checked
|
||||
|
||||
### Screenshots & PDF
|
||||
\`\`\`bash
|
||||
agent-browser screenshot # Screenshot (saves to temp dir if no path)
|
||||
agent-browser screenshot # Screenshot to stdout
|
||||
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)
|
||||
@@ -138,12 +123,10 @@ 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 (left/right/middle)
|
||||
agent-browser mouse down left # Press button
|
||||
agent-browser mouse up left # Release button
|
||||
agent-browser mouse wheel 100 # Scroll wheel
|
||||
\`\`\`
|
||||
@@ -153,18 +136,10 @@ 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
|
||||
@@ -181,13 +156,14 @@ 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 # Same for sessionStorage
|
||||
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
|
||||
\`\`\`
|
||||
|
||||
### Network
|
||||
@@ -217,59 +193,13 @@ agent-browser frame main # Back to main frame
|
||||
|
||||
### Dialogs
|
||||
\`\`\`bash
|
||||
agent-browser dialog accept [text] # Accept dialog (with optional prompt text)
|
||||
agent-browser dialog accept [text] # Accept dialog
|
||||
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
|
||||
@@ -277,60 +207,19 @@ agent-browser install --with-deps # Also install system deps (Linux)
|
||||
| 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 |
|
||||
| \`--full, -f\` | Full page screenshot |
|
||||
| \`--annotate\` | Annotated screenshot with numbered labels (\`AGENT_BROWSER_ANNOTATE\` env) |
|
||||
| \`--headed\` | Show browser window (\`AGENT_BROWSER_HEADED\` env) |
|
||||
| \`--headed\` | Show browser window (not headless) |
|
||||
| \`--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
|
||||
@@ -372,13 +261,6 @@ 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)
|
||||
@@ -388,13 +270,6 @@ 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
|
||||
@@ -402,6 +277,9 @@ 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)
|
||||
|
||||
@@ -411,53 +289,21 @@ agent-browser snapshot -i --json
|
||||
agent-browser get text @e1 --json
|
||||
\`\`\`
|
||||
|
||||
## Local files
|
||||
## Debugging
|
||||
|
||||
\`\`\`bash
|
||||
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 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 --cdp "wss://browser-service.com/cdp?token=..." snapshot # Remote via WebSocket
|
||||
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
|
||||
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
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { injectGitMasterConfig } from "./git-master-template-injection"
|
||||
|
||||
const SAMPLE_TEMPLATE = [
|
||||
"# Git Master Agent",
|
||||
"",
|
||||
"## MODE DETECTION (FIRST STEP)",
|
||||
"",
|
||||
"Analyze the request.",
|
||||
"",
|
||||
"```bash",
|
||||
"git status",
|
||||
"git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null",
|
||||
"MERGE_BASE=$(git merge-base HEAD main)",
|
||||
"GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash $MERGE_BASE",
|
||||
"```",
|
||||
"",
|
||||
"```",
|
||||
"</execution>",
|
||||
].join("\n")
|
||||
|
||||
describe("#given git_env_prefix config", () => {
|
||||
describe("#when default config (GIT_MASTER=1)", () => {
|
||||
it("#then injects env prefix section before MODE DETECTION", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
})
|
||||
|
||||
expect(result).toContain("## GIT COMMAND PREFIX (MANDATORY)")
|
||||
expect(result).toContain("GIT_MASTER=1 git status")
|
||||
expect(result).toContain("GIT_MASTER=1 git commit")
|
||||
expect(result).toContain("GIT_MASTER=1 git push")
|
||||
expect(result).toContain("EVERY git command MUST be prefixed with `GIT_MASTER=1`")
|
||||
|
||||
const prefixIndex = result.indexOf("## GIT COMMAND PREFIX")
|
||||
const modeIndex = result.indexOf("## MODE DETECTION")
|
||||
expect(prefixIndex).toBeLessThan(modeIndex)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when git_env_prefix is empty string", () => {
|
||||
it("#then does NOT inject env prefix section", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "",
|
||||
})
|
||||
|
||||
expect(result).not.toContain("## GIT COMMAND PREFIX")
|
||||
expect(result).not.toContain("GIT_MASTER=1")
|
||||
expect(result).not.toContain("git_env_prefix")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when git_env_prefix is custom value", () => {
|
||||
it("#then injects custom prefix in section", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "MY_HOOK=active",
|
||||
})
|
||||
|
||||
expect(result).toContain("MY_HOOK=active git status")
|
||||
expect(result).toContain("MY_HOOK=active git commit")
|
||||
expect(result).not.toContain("GIT_MASTER=1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when git_env_prefix contains shell metacharacters", () => {
|
||||
it("#then rejects the malicious value", () => {
|
||||
expect(() =>
|
||||
injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "A=1; rm -rf /",
|
||||
})
|
||||
).toThrow('git_env_prefix must be empty or use shell-safe env assignments like "GIT_MASTER=1"')
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when no config provided", () => {
|
||||
it("#then uses default GIT_MASTER=1 prefix", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE)
|
||||
|
||||
expect(result).toContain("GIT_MASTER=1 git status")
|
||||
expect(result).toContain("## GIT COMMAND PREFIX (MANDATORY)")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given git_env_prefix with commit footer", () => {
|
||||
describe("#when both env prefix and footer are enabled", () => {
|
||||
it("#then commit examples include the env prefix", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
})
|
||||
|
||||
expect(result).toContain("GIT_MASTER=1 git commit")
|
||||
expect(result).toContain("Ultraworked with [Sisyphus]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when the template already contains bare git commands in bash blocks", () => {
|
||||
it("#then prefixes every git invocation in the final output", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
})
|
||||
|
||||
expect(result).toContain("GIT_MASTER=1 git status")
|
||||
expect(result).toContain(
|
||||
"GIT_MASTER=1 git merge-base HEAD main 2>/dev/null || GIT_MASTER=1 git merge-base HEAD master 2>/dev/null"
|
||||
)
|
||||
expect(result).toContain("MERGE_BASE=$(GIT_MASTER=1 git merge-base HEAD main)")
|
||||
expect(result).toContain(
|
||||
"GIT_SEQUENCE_EDITOR=: GIT_MASTER=1 git rebase -i --autosquash $MERGE_BASE"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when env prefix disabled but footer enabled", () => {
|
||||
it("#then commit examples have no env prefix", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "",
|
||||
})
|
||||
|
||||
expect(result).not.toContain("GIT_MASTER=1 git commit")
|
||||
expect(result).toContain("git commit -m")
|
||||
expect(result).toContain("Ultraworked with [Sisyphus]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when both env prefix and co-author are enabled", () => {
|
||||
it("#then commit example includes prefix, footer, and co-author", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: true,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
})
|
||||
|
||||
expect(result).toContain("GIT_MASTER=1 git commit")
|
||||
expect(result).toContain("Ultraworked with [Sisyphus]")
|
||||
expect(result).toContain("Co-authored-by: Sisyphus")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,88 +1,14 @@
|
||||
import { assertValidGitEnvPrefix, type GitMasterConfig } from "../../config/schema"
|
||||
|
||||
const BASH_CODE_BLOCK_PATTERN = /```bash\r?\n([\s\S]*?)```/g
|
||||
const LEADING_GIT_COMMAND_PATTERN = /^([ \t]*(?:[A-Za-z_][A-Za-z0-9_]*=[^ \t]+\s+)*)git(?=[ \t]|$)/gm
|
||||
const INLINE_GIT_COMMAND_PATTERN = /([;&|()][ \t]*)git(?=[ \t]|$)/g
|
||||
import type { GitMasterConfig } from "../../config/schema"
|
||||
|
||||
export function injectGitMasterConfig(template: string, config?: GitMasterConfig): string {
|
||||
const commitFooter = config?.commit_footer ?? true
|
||||
const includeCoAuthoredBy = config?.include_co_authored_by ?? true
|
||||
const gitEnvPrefix = assertValidGitEnvPrefix(config?.git_env_prefix ?? "GIT_MASTER=1")
|
||||
|
||||
let result = gitEnvPrefix ? injectGitEnvPrefix(template, gitEnvPrefix) : template
|
||||
|
||||
if (commitFooter || includeCoAuthoredBy) {
|
||||
const injection = buildCommitFooterInjection(commitFooter, includeCoAuthoredBy, gitEnvPrefix)
|
||||
const insertionPoint = result.indexOf("```\n</execution>")
|
||||
|
||||
result =
|
||||
insertionPoint !== -1
|
||||
? result.slice(0, insertionPoint) +
|
||||
"```\n\n" +
|
||||
injection +
|
||||
"\n</execution>" +
|
||||
result.slice(insertionPoint + "```\n</execution>".length)
|
||||
: result + "\n\n" + injection
|
||||
if (!commitFooter && !includeCoAuthoredBy) {
|
||||
return template
|
||||
}
|
||||
|
||||
return gitEnvPrefix ? prefixGitCommandsInBashCodeBlocks(result, gitEnvPrefix) : result
|
||||
}
|
||||
|
||||
function injectGitEnvPrefix(template: string, prefix: string): string {
|
||||
const envPrefixSection = [
|
||||
"## GIT COMMAND PREFIX (MANDATORY)",
|
||||
"",
|
||||
`<git_env_prefix>`,
|
||||
`**EVERY git command MUST be prefixed with \`${prefix}\`.**`,
|
||||
"",
|
||||
"This allows custom git hooks to detect when git-master skill is active.",
|
||||
"",
|
||||
"```bash",
|
||||
`${prefix} git status`,
|
||||
`${prefix} git add <files>`,
|
||||
`${prefix} git commit -m "message"`,
|
||||
`${prefix} git push`,
|
||||
`${prefix} git rebase ...`,
|
||||
`${prefix} git log ...`,
|
||||
"```",
|
||||
"",
|
||||
"**NO EXCEPTIONS. Every `git` invocation must include this prefix.**",
|
||||
`</git_env_prefix>`,
|
||||
].join("\n")
|
||||
|
||||
const modeDetectionMarker = "## MODE DETECTION (FIRST STEP)"
|
||||
const markerIndex = template.indexOf(modeDetectionMarker)
|
||||
if (markerIndex !== -1) {
|
||||
return (
|
||||
template.slice(0, markerIndex) +
|
||||
envPrefixSection +
|
||||
"\n\n---\n\n" +
|
||||
template.slice(markerIndex)
|
||||
)
|
||||
}
|
||||
|
||||
return envPrefixSection + "\n\n---\n\n" + template
|
||||
}
|
||||
|
||||
function prefixGitCommandsInBashCodeBlocks(template: string, prefix: string): string {
|
||||
return template.replace(BASH_CODE_BLOCK_PATTERN, (block, codeBlock: string) => {
|
||||
return block.replace(codeBlock, prefixGitCommandsInCodeBlock(codeBlock, prefix))
|
||||
})
|
||||
}
|
||||
|
||||
function prefixGitCommandsInCodeBlock(codeBlock: string, prefix: string): string {
|
||||
return codeBlock
|
||||
.replace(LEADING_GIT_COMMAND_PATTERN, `$1${prefix} git`)
|
||||
.replace(INLINE_GIT_COMMAND_PATTERN, `$1${prefix} git`)
|
||||
}
|
||||
|
||||
function buildCommitFooterInjection(
|
||||
commitFooter: boolean | string,
|
||||
includeCoAuthoredBy: boolean,
|
||||
gitEnvPrefix: string,
|
||||
): string {
|
||||
const sections: string[] = []
|
||||
const cmdPrefix = gitEnvPrefix ? `${gitEnvPrefix} ` : ""
|
||||
|
||||
sections.push("### 5.5 Commit Footer & Co-Author")
|
||||
sections.push("")
|
||||
@@ -117,7 +43,7 @@ function buildCommitFooterInjection(
|
||||
sections.push("**Example (both enabled):**")
|
||||
sections.push("```bash")
|
||||
sections.push(
|
||||
`${cmdPrefix}git commit -m "{Commit Message}" -m "${footerText}" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"`
|
||||
`git commit -m "{Commit Message}" -m "${footerText}" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"`
|
||||
)
|
||||
sections.push("```")
|
||||
} else if (commitFooter) {
|
||||
@@ -127,16 +53,29 @@ function buildCommitFooterInjection(
|
||||
: "Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)"
|
||||
sections.push("**Example:**")
|
||||
sections.push("```bash")
|
||||
sections.push(`${cmdPrefix}git commit -m "{Commit Message}" -m "${footerText}"`)
|
||||
sections.push(`git commit -m "{Commit Message}" -m "${footerText}"`)
|
||||
sections.push("```")
|
||||
} else if (includeCoAuthoredBy) {
|
||||
sections.push("**Example:**")
|
||||
sections.push("```bash")
|
||||
sections.push(
|
||||
`${cmdPrefix}git commit -m "{Commit Message}" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"`
|
||||
"git commit -m \"{Commit Message}\" -m \"Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>\""
|
||||
)
|
||||
sections.push("```")
|
||||
}
|
||||
|
||||
return sections.join("\n")
|
||||
const injection = sections.join("\n")
|
||||
|
||||
const insertionPoint = template.indexOf("```\n</execution>")
|
||||
if (insertionPoint !== -1) {
|
||||
return (
|
||||
template.slice(0, insertionPoint) +
|
||||
"```\n\n" +
|
||||
injection +
|
||||
"\n</execution>" +
|
||||
template.slice(insertionPoint + "```\n</execution>".length)
|
||||
)
|
||||
}
|
||||
|
||||
return template + "\n\n" + injection
|
||||
}
|
||||
|
||||
@@ -228,7 +228,6 @@ describe("resolveMultipleSkillsAsync", () => {
|
||||
gitMasterConfig: {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -250,7 +249,6 @@ describe("resolveMultipleSkillsAsync", () => {
|
||||
gitMasterConfig: {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: true,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -271,7 +269,6 @@ describe("resolveMultipleSkillsAsync", () => {
|
||||
gitMasterConfig: {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -305,7 +302,6 @@ describe("resolveMultipleSkillsAsync", () => {
|
||||
gitMasterConfig: {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: true,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -326,7 +322,6 @@ describe("resolveMultipleSkillsAsync", () => {
|
||||
gitMasterConfig: {
|
||||
commit_footer: customFooter,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -346,7 +341,6 @@ describe("resolveMultipleSkillsAsync", () => {
|
||||
gitMasterConfig: {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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: "zai-coding-plan",
|
||||
modelID: "glm-5",
|
||||
providerID: "opencode",
|
||||
modelID: "kimi-k2.5-free",
|
||||
})
|
||||
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: "zai-coding-plan",
|
||||
modelID: "glm-5",
|
||||
providerID: "opencode",
|
||||
modelID: "kimi-k2.5-free",
|
||||
})
|
||||
expect(second.message["variant"]).toBeUndefined()
|
||||
expect(abortCalls).toEqual([sessionID, sessionID])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export function createSystemTransformHandler(): (
|
||||
input: { sessionID?: string; model: { id: string; providerID: string; [key: string]: unknown } },
|
||||
input: { sessionID: string },
|
||||
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(3)
|
||||
expect(sisyphus.fallbackChain).toHaveLength(4)
|
||||
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[2]
|
||||
const last = sisyphus.fallbackChain[3]
|
||||
expect(last.providers[0]).toBe("opencode")
|
||||
expect(last.model).toBe("big-pickle")
|
||||
})
|
||||
@@ -91,7 +91,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
const multimodalLooker = AGENT_MODEL_REQUIREMENTS["multimodal-looker"]
|
||||
|
||||
// when - accessing multimodal-looker requirement
|
||||
// then - fallbackChain: gpt-5.3-codex -> k2p5 -> gemini-3-flash -> glm-4.6v -> gpt-5-nano
|
||||
// then - fallbackChain exists with gpt-5.3-codex first, gemini second, gpt-5-nano last
|
||||
expect(multimodalLooker).toBeDefined()
|
||||
expect(multimodalLooker.fallbackChain).toBeArray()
|
||||
expect(multimodalLooker.fallbackChain).toHaveLength(5)
|
||||
@@ -102,11 +102,7 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
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")
|
||||
expect(secondary.model).toBe("gemini-3-flash")
|
||||
|
||||
const last = multimodalLooker.fallbackChain[4]
|
||||
expect(last.providers).toEqual(["openai", "github-copilot", "opencode"])
|
||||
@@ -161,19 +157,19 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
|
||||
expect(primary.providers[0]).toBe("openai")
|
||||
})
|
||||
|
||||
test("atlas has valid fallbackChain with claude-sonnet-4-6 as primary", () => {
|
||||
test("atlas has valid fallbackChain with kimi-k2.5-free as primary", () => {
|
||||
// given - atlas agent requirement
|
||||
const atlas = AGENT_MODEL_REQUIREMENTS["atlas"]
|
||||
|
||||
// when - accessing Atlas requirement
|
||||
// then - fallbackChain exists with claude-sonnet-4-6 as first entry
|
||||
// then - fallbackChain exists with kimi-k2.5-free 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("claude-sonnet-4-6")
|
||||
expect(primary.providers[0]).toBe("anthropic")
|
||||
expect(primary.model).toBe("kimi-k2.5-free")
|
||||
expect(primary.providers[0]).toBe("opencode")
|
||||
})
|
||||
|
||||
test("hephaestus supports openai, github-copilot, venice, and opencode providers", () => {
|
||||
@@ -343,23 +339,27 @@ describe("CATEGORY_MODEL_REQUIREMENTS", () => {
|
||||
expect(primary.providers[0]).toBe("google")
|
||||
})
|
||||
|
||||
test("writing has valid fallbackChain with gemini-3-flash as primary", () => {
|
||||
test("writing has valid fallbackChain with kimi-k2.5-free as primary", () => {
|
||||
// given - writing category requirement
|
||||
const writing = CATEGORY_MODEL_REQUIREMENTS["writing"]
|
||||
|
||||
// when - accessing writing requirement
|
||||
// then - fallbackChain: gemini-3-flash -> claude-sonnet-4-6
|
||||
// then - fallbackChain: kimi-k2.5-free -> gemini-3-flash -> claude-sonnet-4-6
|
||||
expect(writing).toBeDefined()
|
||||
expect(writing.fallbackChain).toBeArray()
|
||||
expect(writing.fallbackChain).toHaveLength(2)
|
||||
expect(writing.fallbackChain).toHaveLength(3)
|
||||
|
||||
const primary = writing.fallbackChain[0]
|
||||
expect(primary.model).toBe("gemini-3-flash")
|
||||
expect(primary.providers[0]).toBe("google")
|
||||
expect(primary.model).toBe("kimi-k2.5-free")
|
||||
expect(primary.providers[0]).toBe("opencode")
|
||||
|
||||
const second = writing.fallbackChain[1]
|
||||
expect(second.model).toBe("claude-sonnet-4-6")
|
||||
expect(second.providers[0]).toBe("anthropic")
|
||||
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")
|
||||
})
|
||||
|
||||
test("all 8 categories have valid fallbackChain arrays", () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ 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,8 +54,8 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
|
||||
"multimodal-looker": {
|
||||
fallbackChain: [
|
||||
{ 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: ["opencode"], model: "kimi-k2.5-free" },
|
||||
{ providers: ["zai-coding-plan"], model: "glm-4.6v" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5-nano" },
|
||||
],
|
||||
@@ -63,12 +64,14 @@ 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" },
|
||||
],
|
||||
@@ -82,6 +85,7 @@ 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" },
|
||||
],
|
||||
@@ -142,6 +146,7 @@ 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. - Timeout values are in milliseconds (ms), NOT seconds.`
|
||||
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_CANCEL_DESCRIPTION = `Cancel running background task(s). Use all=true to cancel ALL before final answer.`
|
||||
|
||||
@@ -33,7 +33,6 @@ 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)
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
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,39 +56,36 @@ 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,
|
||||
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,
|
||||
},
|
||||
}
|
||||
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.
|
||||
|
||||
Background Task ID: ${task.id}
|
||||
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.${taskMetadataBlock}`
|
||||
System notifies on completion. Use \`background_output\` with task_id="${task.id}" to check.
|
||||
|
||||
<task_metadata>
|
||||
session_id: ${sessionId}
|
||||
</task_metadata>`
|
||||
} catch (error) {
|
||||
return formatDetailedError(error, {
|
||||
operation: "Launch background task",
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
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,7 +32,22 @@ export async function executeSyncContinuation(
|
||||
})
|
||||
}
|
||||
|
||||
let syncContMeta: { title: string; metadata: Record<string, unknown> } | undefined
|
||||
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 resumeAgent: string | undefined
|
||||
let resumeModel: { providerID: string; modelID: string } | undefined
|
||||
@@ -63,24 +78,6 @@ 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,7 +91,6 @@ 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,7 +66,6 @@ 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,96 +456,6 @@ 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,10 +217,6 @@ 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