mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -74,9 +74,9 @@ jobs:
|
|||||||
- runtime: node
|
- runtime: node
|
||||||
task: protocol
|
task: protocol
|
||||||
command: pnpm protocol:check
|
command: pnpm protocol:check
|
||||||
- runtime: bun
|
- runtime: node
|
||||||
task: lint
|
task: format
|
||||||
command: bunx biome check src
|
command: pnpm format
|
||||||
- runtime: bun
|
- runtime: bun
|
||||||
task: test
|
task: test
|
||||||
command: bunx vitest run
|
command: bunx vitest run
|
||||||
|
|||||||
5
.oxfmtrc.jsonc
Normal file
5
.oxfmtrc.jsonc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
4
.oxlintrc.jsonc
Normal file
4
.oxlintrc.jsonc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/oxlintrc",
|
||||||
|
"extends": ["recommended"]
|
||||||
|
}
|
||||||
@@ -25,12 +25,12 @@
|
|||||||
- Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`.
|
- Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`.
|
||||||
- Node remains supported for running built output (`dist/*`) and production installs.
|
- Node remains supported for running built output (`dist/*`) and production installs.
|
||||||
- Type-check/build: `pnpm build` (tsc)
|
- Type-check/build: `pnpm build` (tsc)
|
||||||
- Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format)
|
- Lint/format: `pnpm lint` (oxlint), `pnpm format` (oxfmt)
|
||||||
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
|
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
## Coding Style & Naming Conventions
|
||||||
- Language: TypeScript (ESM). Prefer strict typing; avoid `any`.
|
- Language: TypeScript (ESM). Prefer strict typing; avoid `any`.
|
||||||
- Formatting/linting via Biome; run `pnpm lint` before commits.
|
- Formatting/linting via Oxlint and Oxfmt; run `pnpm lint` before commits.
|
||||||
- Add brief code comments for tricky or non-obvious logic.
|
- Add brief code comments for tricky or non-obvious logic.
|
||||||
- Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`.
|
- Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`.
|
||||||
- Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability.
|
- Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability.
|
||||||
|
|||||||
17
biome.json
17
biome.json
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/biome.json",
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"indentWidth": 2,
|
|
||||||
"indentStyle": "space"
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"includes": ["src/**/*.ts", "test/**/*.ts"]
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
12
package.json
12
package.json
@@ -87,14 +87,14 @@
|
|||||||
"mac:restart": "bash scripts/restart-mac.sh",
|
"mac:restart": "bash scripts/restart-mac.sh",
|
||||||
"mac:package": "bash scripts/package-mac-app.sh",
|
"mac:package": "bash scripts/package-mac-app.sh",
|
||||||
"mac:open": "open dist/Clawdbot.app",
|
"mac:open": "open dist/Clawdbot.app",
|
||||||
"lint": "biome check src test && oxlint --type-aware src test --ignore-pattern src/canvas-host/a2ui/a2ui.bundle.js",
|
"lint": "oxlint --type-aware src test --ignore-pattern src/canvas-host/a2ui/a2ui.bundle.js",
|
||||||
"lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
|
"lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
|
||||||
"lint:all": "pnpm lint && pnpm lint:swift",
|
"lint:all": "pnpm lint && pnpm lint:swift",
|
||||||
"lint:fix": "biome check --write --unsafe src && biome format --write src",
|
"lint:fix": "pnpm format:fix && oxlint --type-aware --fix src test --ignore-pattern src/canvas-host/a2ui/a2ui.bundle.js",
|
||||||
"format": "biome format src",
|
"format": "oxfmt --check src test",
|
||||||
"format:swift": "swiftformat --lint --config .swiftformat apps/macos/Sources apps/ios/Sources apps/shared/ClawdbotKit/Sources",
|
"format:swift": "swiftformat --lint --config .swiftformat apps/macos/Sources apps/ios/Sources apps/shared/ClawdbotKit/Sources",
|
||||||
"format:all": "pnpm format && pnpm format:swift",
|
"format:all": "pnpm format && pnpm format:swift",
|
||||||
"format:fix": "biome format src --write",
|
"format:fix": "oxfmt --write src test",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:ui": "pnpm --dir ui test",
|
"test:ui": "pnpm --dir ui test",
|
||||||
@@ -176,7 +176,6 @@
|
|||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.3.11",
|
|
||||||
"@grammyjs/types": "^3.23.0",
|
"@grammyjs/types": "^3.23.0",
|
||||||
"@lit-labs/signals": "^0.2.0",
|
"@lit-labs/signals": "^0.2.0",
|
||||||
"@lit/context": "^1.1.6",
|
"@lit/context": "^1.1.6",
|
||||||
@@ -194,7 +193,8 @@
|
|||||||
"lit": "^3.3.2",
|
"lit": "^3.3.2",
|
||||||
"lucide": "^0.562.0",
|
"lucide": "^0.562.0",
|
||||||
"ollama": "^0.6.3",
|
"ollama": "^0.6.3",
|
||||||
"oxlint": "^1.38.0",
|
"oxfmt": "0.24.0",
|
||||||
|
"oxlint": "^1.39.0",
|
||||||
"oxlint-tsgolint": "^0.11.0",
|
"oxlint-tsgolint": "^0.11.0",
|
||||||
"quicktype-core": "^23.2.6",
|
"quicktype-core": "^23.2.6",
|
||||||
"rolldown": "1.0.0-beta.59",
|
"rolldown": "1.0.0-beta.59",
|
||||||
|
|||||||
184
pnpm-lock.yaml
generated
184
pnpm-lock.yaml
generated
@@ -149,9 +149,6 @@ importers:
|
|||||||
specifier: ^4.3.5
|
specifier: ^4.3.5
|
||||||
version: 4.3.5
|
version: 4.3.5
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@biomejs/biome':
|
|
||||||
specifier: ^2.3.11
|
|
||||||
version: 2.3.11
|
|
||||||
'@grammyjs/types':
|
'@grammyjs/types':
|
||||||
specifier: ^3.23.0
|
specifier: ^3.23.0
|
||||||
version: 3.23.0
|
version: 3.23.0
|
||||||
@@ -203,8 +200,11 @@ importers:
|
|||||||
ollama:
|
ollama:
|
||||||
specifier: ^0.6.3
|
specifier: ^0.6.3
|
||||||
version: 0.6.3
|
version: 0.6.3
|
||||||
|
oxfmt:
|
||||||
|
specifier: 0.24.0
|
||||||
|
version: 0.24.0
|
||||||
oxlint:
|
oxlint:
|
||||||
specifier: ^1.38.0
|
specifier: ^1.39.0
|
||||||
version: 1.39.0(oxlint-tsgolint@0.11.0)
|
version: 1.39.0(oxlint-tsgolint@0.11.0)
|
||||||
oxlint-tsgolint:
|
oxlint-tsgolint:
|
||||||
specifier: ^0.11.0
|
specifier: ^0.11.0
|
||||||
@@ -452,59 +452,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@biomejs/biome@2.3.11':
|
|
||||||
resolution: {integrity: sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==}
|
|
||||||
engines: {node: '>=14.21.3'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
'@biomejs/cli-darwin-arm64@2.3.11':
|
|
||||||
resolution: {integrity: sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==}
|
|
||||||
engines: {node: '>=14.21.3'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@biomejs/cli-darwin-x64@2.3.11':
|
|
||||||
resolution: {integrity: sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==}
|
|
||||||
engines: {node: '>=14.21.3'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@biomejs/cli-linux-arm64-musl@2.3.11':
|
|
||||||
resolution: {integrity: sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==}
|
|
||||||
engines: {node: '>=14.21.3'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@biomejs/cli-linux-arm64@2.3.11':
|
|
||||||
resolution: {integrity: sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==}
|
|
||||||
engines: {node: '>=14.21.3'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@biomejs/cli-linux-x64-musl@2.3.11':
|
|
||||||
resolution: {integrity: sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==}
|
|
||||||
engines: {node: '>=14.21.3'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@biomejs/cli-linux-x64@2.3.11':
|
|
||||||
resolution: {integrity: sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==}
|
|
||||||
engines: {node: '>=14.21.3'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@biomejs/cli-win32-arm64@2.3.11':
|
|
||||||
resolution: {integrity: sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==}
|
|
||||||
engines: {node: '>=14.21.3'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@biomejs/cli-win32-x64@2.3.11':
|
|
||||||
resolution: {integrity: sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==}
|
|
||||||
engines: {node: '>=14.21.3'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@borewit/text-codec@0.2.1':
|
'@borewit/text-codec@0.2.1':
|
||||||
resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==}
|
resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==}
|
||||||
|
|
||||||
@@ -1245,6 +1192,46 @@ packages:
|
|||||||
'@oxc-project/types@0.107.0':
|
'@oxc-project/types@0.107.0':
|
||||||
resolution: {integrity: sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==}
|
resolution: {integrity: sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==}
|
||||||
|
|
||||||
|
'@oxfmt/darwin-arm64@0.24.0':
|
||||||
|
resolution: {integrity: sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@oxfmt/darwin-x64@0.24.0':
|
||||||
|
resolution: {integrity: sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@oxfmt/linux-arm64-gnu@0.24.0':
|
||||||
|
resolution: {integrity: sha512-ItPDOPoQ0wLj/s8osc5ch57uUcA1Wk8r0YdO8vLRpXA3UNg7KPOm1vdbkIZRRiSUphZcuX5ioOEetEK8H7RlTw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@oxfmt/linux-arm64-musl@0.24.0':
|
||||||
|
resolution: {integrity: sha512-JkQO3WnQjQTJONx8nxdgVBfl6BBFfpp9bKhChYhWeakwJdr7QPOAWJ/v3FGZfr0TbqINwnNR74aVZayDDRyXEA==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@oxfmt/linux-x64-gnu@0.24.0':
|
||||||
|
resolution: {integrity: sha512-N/SXlFO+2kak5gMt0oxApi0WXQDhwA0PShR0UbkY0PwtHjfSiDqJSOumyNqgQVoroKr1GNnoRmUqjZIz6DKIcw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@oxfmt/linux-x64-musl@0.24.0':
|
||||||
|
resolution: {integrity: sha512-WM0pek5YDCQf50XQ7GLCE9sMBCMPW/NPAEPH/Hx6Qyir37lEsP4rUmSECo/QFNTU6KBc9NnsviAyJruWPpCMXw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@oxfmt/win32-arm64@0.24.0':
|
||||||
|
resolution: {integrity: sha512-vFCseli1KWtwdHrVlT/jWfZ8jP8oYpnPPEjI23mPLW8K/6GEJmmvy0PZP5NpWUFNTzX0lqie58XnrATJYAe9Xw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@oxfmt/win32-x64@0.24.0':
|
||||||
|
resolution: {integrity: sha512-0tmlNzcyewAnauNeBCq0xmAkmiKzl+H09p0IdHy+QKrTQdtixtf+AOjDAADbRfihkS+heF15Pjc4IyJMdAAJjw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@oxlint-tsgolint/darwin-arm64@0.11.0':
|
'@oxlint-tsgolint/darwin-arm64@0.11.0':
|
||||||
resolution: {integrity: sha512-F67T8dXgYIrgv6wpd52fKQFdmieSOHaxBkscgso64YdtEHrV3s52ASiZGNzw62TKihn9Ox9ek3PYx9XsxIJDUw==}
|
resolution: {integrity: sha512-F67T8dXgYIrgv6wpd52fKQFdmieSOHaxBkscgso64YdtEHrV3s52ASiZGNzw62TKihn9Ox9ek3PYx9XsxIJDUw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
@@ -3324,6 +3311,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-GJR9XnS8dQ+sAdbhX90RA4WbmEyrso7X9aHMws4MaQ2GRpfEjnOUSZIdOXJQfnIfBoy9oCc7US/MNFCyuJQzjg==}
|
resolution: {integrity: sha512-GJR9XnS8dQ+sAdbhX90RA4WbmEyrso7X9aHMws4MaQ2GRpfEjnOUSZIdOXJQfnIfBoy9oCc7US/MNFCyuJQzjg==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
oxfmt@0.24.0:
|
||||||
|
resolution: {integrity: sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
oxlint-tsgolint@0.11.0:
|
oxlint-tsgolint@0.11.0:
|
||||||
resolution: {integrity: sha512-fGYb7z/cljC0Rjtbxh7mIe8vtF/M9TShLvniwc2rdcqNG3Z9g3nM01cr2kWRb1DZdbY4/kItvIsrV4uhaMifyQ==}
|
resolution: {integrity: sha512-fGYb7z/cljC0Rjtbxh7mIe8vtF/M9TShLvniwc2rdcqNG3Z9g3nM01cr2kWRb1DZdbY4/kItvIsrV4uhaMifyQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -3858,6 +3850,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tinypool@2.0.0:
|
||||||
|
resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==}
|
||||||
|
engines: {node: ^20.0.0 || >=22.0.0}
|
||||||
|
|
||||||
tinyrainbow@3.0.3:
|
tinyrainbow@3.0.3:
|
||||||
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
|
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -4641,41 +4637,6 @@ snapshots:
|
|||||||
|
|
||||||
'@bcoe/v8-coverage@1.0.2': {}
|
'@bcoe/v8-coverage@1.0.2': {}
|
||||||
|
|
||||||
'@biomejs/biome@2.3.11':
|
|
||||||
optionalDependencies:
|
|
||||||
'@biomejs/cli-darwin-arm64': 2.3.11
|
|
||||||
'@biomejs/cli-darwin-x64': 2.3.11
|
|
||||||
'@biomejs/cli-linux-arm64': 2.3.11
|
|
||||||
'@biomejs/cli-linux-arm64-musl': 2.3.11
|
|
||||||
'@biomejs/cli-linux-x64': 2.3.11
|
|
||||||
'@biomejs/cli-linux-x64-musl': 2.3.11
|
|
||||||
'@biomejs/cli-win32-arm64': 2.3.11
|
|
||||||
'@biomejs/cli-win32-x64': 2.3.11
|
|
||||||
|
|
||||||
'@biomejs/cli-darwin-arm64@2.3.11':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@biomejs/cli-darwin-x64@2.3.11':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@biomejs/cli-linux-arm64-musl@2.3.11':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@biomejs/cli-linux-arm64@2.3.11':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@biomejs/cli-linux-x64-musl@2.3.11':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@biomejs/cli-linux-x64@2.3.11':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@biomejs/cli-win32-arm64@2.3.11':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@biomejs/cli-win32-x64@2.3.11':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@borewit/text-codec@0.2.1': {}
|
'@borewit/text-codec@0.2.1': {}
|
||||||
|
|
||||||
'@buape/carbon@0.0.0-beta-20260110172854(hono@4.11.3)':
|
'@buape/carbon@0.0.0-beta-20260110172854(hono@4.11.3)':
|
||||||
@@ -5420,6 +5381,30 @@ snapshots:
|
|||||||
|
|
||||||
'@oxc-project/types@0.107.0': {}
|
'@oxc-project/types@0.107.0': {}
|
||||||
|
|
||||||
|
'@oxfmt/darwin-arm64@0.24.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxfmt/darwin-x64@0.24.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxfmt/linux-arm64-gnu@0.24.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxfmt/linux-arm64-musl@0.24.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxfmt/linux-x64-gnu@0.24.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxfmt/linux-x64-musl@0.24.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxfmt/win32-arm64@0.24.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxfmt/win32-x64@0.24.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@oxlint-tsgolint/darwin-arm64@0.11.0':
|
'@oxlint-tsgolint/darwin-arm64@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -7653,6 +7638,19 @@ snapshots:
|
|||||||
|
|
||||||
osc-progress@0.2.0: {}
|
osc-progress@0.2.0: {}
|
||||||
|
|
||||||
|
oxfmt@0.24.0:
|
||||||
|
dependencies:
|
||||||
|
tinypool: 2.0.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@oxfmt/darwin-arm64': 0.24.0
|
||||||
|
'@oxfmt/darwin-x64': 0.24.0
|
||||||
|
'@oxfmt/linux-arm64-gnu': 0.24.0
|
||||||
|
'@oxfmt/linux-arm64-musl': 0.24.0
|
||||||
|
'@oxfmt/linux-x64-gnu': 0.24.0
|
||||||
|
'@oxfmt/linux-x64-musl': 0.24.0
|
||||||
|
'@oxfmt/win32-arm64': 0.24.0
|
||||||
|
'@oxfmt/win32-x64': 0.24.0
|
||||||
|
|
||||||
oxlint-tsgolint@0.11.0:
|
oxlint-tsgolint@0.11.0:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@oxlint-tsgolint/darwin-arm64': 0.11.0
|
'@oxlint-tsgolint/darwin-arm64': 0.11.0
|
||||||
@@ -8291,6 +8289,8 @@ snapshots:
|
|||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
tinypool@2.0.0: {}
|
||||||
|
|
||||||
tinyrainbow@3.0.3: {}
|
tinyrainbow@3.0.3: {}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
|
|||||||
@@ -6,15 +6,9 @@ import { resolveUserPath } from "../utils.js";
|
|||||||
|
|
||||||
export function resolveClawdbotAgentDir(): string {
|
export function resolveClawdbotAgentDir(): string {
|
||||||
const override =
|
const override =
|
||||||
process.env.CLAWDBOT_AGENT_DIR?.trim() ||
|
process.env.CLAWDBOT_AGENT_DIR?.trim() || process.env.PI_CODING_AGENT_DIR?.trim();
|
||||||
process.env.PI_CODING_AGENT_DIR?.trim();
|
|
||||||
if (override) return resolveUserPath(override);
|
if (override) return resolveUserPath(override);
|
||||||
const defaultAgentDir = path.join(
|
const defaultAgentDir = path.join(resolveStateDir(), "agents", DEFAULT_AGENT_ID, "agent");
|
||||||
resolveStateDir(),
|
|
||||||
"agents",
|
|
||||||
DEFAULT_AGENT_ID,
|
|
||||||
"agent",
|
|
||||||
);
|
|
||||||
return resolveUserPath(defaultAgentDir);
|
return resolveUserPath(defaultAgentDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,12 +72,8 @@ describe("resolveAgentConfig", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(resolveAgentModelPrimary(cfg, "linus")).toBe(
|
expect(resolveAgentModelPrimary(cfg, "linus")).toBe("anthropic/claude-opus-4");
|
||||||
"anthropic/claude-opus-4",
|
expect(resolveAgentModelFallbacksOverride(cfg, "linus")).toEqual(["openai/gpt-5.2"]);
|
||||||
);
|
|
||||||
expect(resolveAgentModelFallbacksOverride(cfg, "linus")).toEqual([
|
|
||||||
"openai/gpt-5.2",
|
|
||||||
]);
|
|
||||||
|
|
||||||
// If fallbacks isn't present, we don't override the global fallbacks.
|
// If fallbacks isn't present, we don't override the global fallbacks.
|
||||||
const cfgNoOverride: ClawdbotConfig = {
|
const cfgNoOverride: ClawdbotConfig = {
|
||||||
@@ -92,9 +88,7 @@ describe("resolveAgentConfig", () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(resolveAgentModelFallbacksOverride(cfgNoOverride, "linus")).toBe(
|
expect(resolveAgentModelFallbacksOverride(cfgNoOverride, "linus")).toBe(undefined);
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Explicit empty list disables global fallbacks for that agent.
|
// Explicit empty list disables global fallbacks for that agent.
|
||||||
const cfgDisable: ClawdbotConfig = {
|
const cfgDisable: ClawdbotConfig = {
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ import { DEFAULT_AGENT_WORKSPACE_DIR } from "./workspace.js";
|
|||||||
|
|
||||||
export { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
export { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
||||||
|
|
||||||
type AgentEntry = NonNullable<
|
type AgentEntry = NonNullable<NonNullable<ClawdbotConfig["agents"]>["list"]>[number];
|
||||||
NonNullable<ClawdbotConfig["agents"]>["list"]
|
|
||||||
>[number];
|
|
||||||
|
|
||||||
type ResolvedAgentConfig = {
|
type ResolvedAgentConfig = {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -36,9 +34,7 @@ let defaultAgentWarned = false;
|
|||||||
function listAgents(cfg: ClawdbotConfig): AgentEntry[] {
|
function listAgents(cfg: ClawdbotConfig): AgentEntry[] {
|
||||||
const list = cfg.agents?.list;
|
const list = cfg.agents?.list;
|
||||||
if (!Array.isArray(list)) return [];
|
if (!Array.isArray(list)) return [];
|
||||||
return list.filter((entry): entry is AgentEntry =>
|
return list.filter((entry): entry is AgentEntry => Boolean(entry && typeof entry === "object"));
|
||||||
Boolean(entry && typeof entry === "object"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDefaultAgentId(cfg: ClawdbotConfig): string {
|
export function resolveDefaultAgentId(cfg: ClawdbotConfig): string {
|
||||||
@@ -47,24 +43,20 @@ export function resolveDefaultAgentId(cfg: ClawdbotConfig): string {
|
|||||||
const defaults = agents.filter((agent) => agent?.default);
|
const defaults = agents.filter((agent) => agent?.default);
|
||||||
if (defaults.length > 1 && !defaultAgentWarned) {
|
if (defaults.length > 1 && !defaultAgentWarned) {
|
||||||
defaultAgentWarned = true;
|
defaultAgentWarned = true;
|
||||||
console.warn(
|
console.warn("Multiple agents marked default=true; using the first entry as default.");
|
||||||
"Multiple agents marked default=true; using the first entry as default.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const chosen = (defaults[0] ?? agents[0])?.id?.trim();
|
const chosen = (defaults[0] ?? agents[0])?.id?.trim();
|
||||||
return normalizeAgentId(chosen || DEFAULT_AGENT_ID);
|
return normalizeAgentId(chosen || DEFAULT_AGENT_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveSessionAgentIds(params: {
|
export function resolveSessionAgentIds(params: { sessionKey?: string; config?: ClawdbotConfig }): {
|
||||||
sessionKey?: string;
|
defaultAgentId: string;
|
||||||
config?: ClawdbotConfig;
|
sessionAgentId: string;
|
||||||
}): { defaultAgentId: string; sessionAgentId: string } {
|
} {
|
||||||
const defaultAgentId = resolveDefaultAgentId(params.config ?? {});
|
const defaultAgentId = resolveDefaultAgentId(params.config ?? {});
|
||||||
const sessionKey = params.sessionKey?.trim();
|
const sessionKey = params.sessionKey?.trim();
|
||||||
const parsed = sessionKey ? parseAgentSessionKey(sessionKey) : null;
|
const parsed = sessionKey ? parseAgentSessionKey(sessionKey) : null;
|
||||||
const sessionAgentId = parsed?.agentId
|
const sessionAgentId = parsed?.agentId ? normalizeAgentId(parsed.agentId) : defaultAgentId;
|
||||||
? normalizeAgentId(parsed.agentId)
|
|
||||||
: defaultAgentId;
|
|
||||||
return { defaultAgentId, sessionAgentId };
|
return { defaultAgentId, sessionAgentId };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,10 +67,7 @@ export function resolveSessionAgentId(params: {
|
|||||||
return resolveSessionAgentIds(params).sessionAgentId;
|
return resolveSessionAgentIds(params).sessionAgentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAgentEntry(
|
function resolveAgentEntry(cfg: ClawdbotConfig, agentId: string): AgentEntry | undefined {
|
||||||
cfg: ClawdbotConfig,
|
|
||||||
agentId: string,
|
|
||||||
): AgentEntry | undefined {
|
|
||||||
const id = normalizeAgentId(agentId);
|
const id = normalizeAgentId(agentId);
|
||||||
return listAgents(cfg).find((entry) => normalizeAgentId(entry.id) === id);
|
return listAgents(cfg).find((entry) => normalizeAgentId(entry.id) === id);
|
||||||
}
|
}
|
||||||
@@ -92,31 +81,23 @@ export function resolveAgentConfig(
|
|||||||
if (!entry) return undefined;
|
if (!entry) return undefined;
|
||||||
return {
|
return {
|
||||||
name: typeof entry.name === "string" ? entry.name : undefined,
|
name: typeof entry.name === "string" ? entry.name : undefined,
|
||||||
workspace:
|
workspace: typeof entry.workspace === "string" ? entry.workspace : undefined,
|
||||||
typeof entry.workspace === "string" ? entry.workspace : undefined,
|
|
||||||
agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined,
|
agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined,
|
||||||
model:
|
model:
|
||||||
typeof entry.model === "string" ||
|
typeof entry.model === "string" || (entry.model && typeof entry.model === "object")
|
||||||
(entry.model && typeof entry.model === "object")
|
|
||||||
? entry.model
|
? entry.model
|
||||||
: undefined,
|
: undefined,
|
||||||
memorySearch: entry.memorySearch,
|
memorySearch: entry.memorySearch,
|
||||||
humanDelay: entry.humanDelay,
|
humanDelay: entry.humanDelay,
|
||||||
identity: entry.identity,
|
identity: entry.identity,
|
||||||
groupChat: entry.groupChat,
|
groupChat: entry.groupChat,
|
||||||
subagents:
|
subagents: typeof entry.subagents === "object" && entry.subagents ? entry.subagents : undefined,
|
||||||
typeof entry.subagents === "object" && entry.subagents
|
|
||||||
? entry.subagents
|
|
||||||
: undefined,
|
|
||||||
sandbox: entry.sandbox,
|
sandbox: entry.sandbox,
|
||||||
tools: entry.tools,
|
tools: entry.tools,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveAgentModelPrimary(
|
export function resolveAgentModelPrimary(cfg: ClawdbotConfig, agentId: string): string | undefined {
|
||||||
cfg: ClawdbotConfig,
|
|
||||||
agentId: string,
|
|
||||||
): string | undefined {
|
|
||||||
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
||||||
if (!raw) return undefined;
|
if (!raw) return undefined;
|
||||||
if (typeof raw === "string") return raw.trim() || undefined;
|
if (typeof raw === "string") return raw.trim() || undefined;
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai";
|
import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai";
|
||||||
import {
|
import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
|
||||||
discoverAuthStorage,
|
|
||||||
discoverModels,
|
|
||||||
} from "@mariozechner/pi-coding-agent";
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
ANTHROPIC_SETUP_TOKEN_PREFIX,
|
ANTHROPIC_SETUP_TOKEN_PREFIX,
|
||||||
@@ -26,15 +23,11 @@ import { ensureClawdbotModelsJson } from "./models-config.js";
|
|||||||
|
|
||||||
const LIVE = process.env.LIVE === "1" || process.env.CLAWDBOT_LIVE_TEST === "1";
|
const LIVE = process.env.LIVE === "1" || process.env.CLAWDBOT_LIVE_TEST === "1";
|
||||||
const SETUP_TOKEN_RAW = process.env.CLAWDBOT_LIVE_SETUP_TOKEN?.trim() ?? "";
|
const SETUP_TOKEN_RAW = process.env.CLAWDBOT_LIVE_SETUP_TOKEN?.trim() ?? "";
|
||||||
const SETUP_TOKEN_VALUE =
|
const SETUP_TOKEN_VALUE = process.env.CLAWDBOT_LIVE_SETUP_TOKEN_VALUE?.trim() ?? "";
|
||||||
process.env.CLAWDBOT_LIVE_SETUP_TOKEN_VALUE?.trim() ?? "";
|
const SETUP_TOKEN_PROFILE = process.env.CLAWDBOT_LIVE_SETUP_TOKEN_PROFILE?.trim() ?? "";
|
||||||
const SETUP_TOKEN_PROFILE =
|
const SETUP_TOKEN_MODEL = process.env.CLAWDBOT_LIVE_SETUP_TOKEN_MODEL?.trim() ?? "";
|
||||||
process.env.CLAWDBOT_LIVE_SETUP_TOKEN_PROFILE?.trim() ?? "";
|
|
||||||
const SETUP_TOKEN_MODEL =
|
|
||||||
process.env.CLAWDBOT_LIVE_SETUP_TOKEN_MODEL?.trim() ?? "";
|
|
||||||
|
|
||||||
const ENABLED =
|
const ENABLED = LIVE && Boolean(SETUP_TOKEN_RAW || SETUP_TOKEN_VALUE || SETUP_TOKEN_PROFILE);
|
||||||
LIVE && Boolean(SETUP_TOKEN_RAW || SETUP_TOKEN_VALUE || SETUP_TOKEN_PROFILE);
|
|
||||||
const describeLive = ENABLED ? describe : describe.skip;
|
const describeLive = ENABLED ? describe : describe.skip;
|
||||||
|
|
||||||
type TokenSource = {
|
type TokenSource = {
|
||||||
@@ -60,11 +53,7 @@ function listSetupTokenProfiles(store: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pickSetupTokenProfile(candidates: string[]): string {
|
function pickSetupTokenProfile(candidates: string[]): string {
|
||||||
const preferred = [
|
const preferred = ["anthropic:setup-token-test", "anthropic:setup-token", "anthropic:default"];
|
||||||
"anthropic:setup-token-test",
|
|
||||||
"anthropic:setup-token",
|
|
||||||
"anthropic:default",
|
|
||||||
];
|
|
||||||
for (const id of preferred) {
|
for (const id of preferred) {
|
||||||
if (candidates.includes(id)) return id;
|
if (candidates.includes(id)) return id;
|
||||||
}
|
}
|
||||||
@@ -73,17 +62,14 @@ function pickSetupTokenProfile(candidates: string[]): string {
|
|||||||
|
|
||||||
async function resolveTokenSource(): Promise<TokenSource> {
|
async function resolveTokenSource(): Promise<TokenSource> {
|
||||||
const explicitToken =
|
const explicitToken =
|
||||||
(SETUP_TOKEN_RAW && isSetupToken(SETUP_TOKEN_RAW) ? SETUP_TOKEN_RAW : "") ||
|
(SETUP_TOKEN_RAW && isSetupToken(SETUP_TOKEN_RAW) ? SETUP_TOKEN_RAW : "") || SETUP_TOKEN_VALUE;
|
||||||
SETUP_TOKEN_VALUE;
|
|
||||||
|
|
||||||
if (explicitToken) {
|
if (explicitToken) {
|
||||||
const error = validateAnthropicSetupToken(explicitToken);
|
const error = validateAnthropicSetupToken(explicitToken);
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new Error(`Invalid setup-token: ${error}`);
|
throw new Error(`Invalid setup-token: ${error}`);
|
||||||
}
|
}
|
||||||
const tempDir = await fs.mkdtemp(
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-setup-token-"));
|
||||||
path.join(os.tmpdir(), "clawdbot-setup-token-"),
|
|
||||||
);
|
|
||||||
const profileId = `anthropic:setup-token-live-${randomUUID()}`;
|
const profileId = `anthropic:setup-token-live-${randomUUID()}`;
|
||||||
const store = ensureAuthProfileStore(tempDir, {
|
const store = ensureAuthProfileStore(tempDir, {
|
||||||
allowKeychainPrompt: false,
|
allowKeychainPrompt: false,
|
||||||
@@ -111,8 +97,7 @@ async function resolveTokenSource(): Promise<TokenSource> {
|
|||||||
const candidates = listSetupTokenProfiles(store);
|
const candidates = listSetupTokenProfiles(store);
|
||||||
if (SETUP_TOKEN_PROFILE) {
|
if (SETUP_TOKEN_PROFILE) {
|
||||||
if (!candidates.includes(SETUP_TOKEN_PROFILE)) {
|
if (!candidates.includes(SETUP_TOKEN_PROFILE)) {
|
||||||
const available =
|
const available = candidates.length > 0 ? candidates.join(", ") : "(none)";
|
||||||
candidates.length > 0 ? candidates.join(", ") : "(none)";
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Setup-token profile "${SETUP_TOKEN_PROFILE}" not found. Available: ${available}.`,
|
`Setup-token profile "${SETUP_TOKEN_PROFILE}" not found. Available: ${available}.`,
|
||||||
);
|
);
|
||||||
@@ -120,11 +105,7 @@ async function resolveTokenSource(): Promise<TokenSource> {
|
|||||||
return { agentDir, profileId: SETUP_TOKEN_PROFILE };
|
return { agentDir, profileId: SETUP_TOKEN_PROFILE };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (SETUP_TOKEN_RAW && SETUP_TOKEN_RAW !== "1" && SETUP_TOKEN_RAW !== "auto") {
|
||||||
SETUP_TOKEN_RAW &&
|
|
||||||
SETUP_TOKEN_RAW !== "1" &&
|
|
||||||
SETUP_TOKEN_RAW !== "auto"
|
|
||||||
) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"CLAWDBOT_LIVE_SETUP_TOKEN did not look like a setup-token. Use CLAWDBOT_LIVE_SETUP_TOKEN_VALUE for raw tokens.",
|
"CLAWDBOT_LIVE_SETUP_TOKEN did not look like a setup-token. Use CLAWDBOT_LIVE_SETUP_TOKEN_VALUE for raw tokens.",
|
||||||
);
|
);
|
||||||
@@ -146,8 +127,7 @@ function pickModel(models: Array<Model<Api>>, raw?: string): Model<Api> | null {
|
|||||||
return (
|
return (
|
||||||
models.find(
|
models.find(
|
||||||
(model) =>
|
(model) =>
|
||||||
normalizeProviderId(model.provider) === parsed.provider &&
|
normalizeProviderId(model.provider) === parsed.provider && model.id === parsed.model,
|
||||||
model.id === parsed.model,
|
|
||||||
) ?? null
|
) ?? null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -176,9 +156,7 @@ describeLive("live anthropic setup-token", () => {
|
|||||||
|
|
||||||
const authStorage = discoverAuthStorage(tokenSource.agentDir);
|
const authStorage = discoverAuthStorage(tokenSource.agentDir);
|
||||||
const modelRegistry = discoverModels(authStorage, tokenSource.agentDir);
|
const modelRegistry = discoverModels(authStorage, tokenSource.agentDir);
|
||||||
const all = Array.isArray(modelRegistry)
|
const all = Array.isArray(modelRegistry) ? modelRegistry : modelRegistry.getAll();
|
||||||
? modelRegistry
|
|
||||||
: modelRegistry.getAll();
|
|
||||||
const candidates = all.filter(
|
const candidates = all.filter(
|
||||||
(model) => normalizeProviderId(model.provider) === "anthropic",
|
(model) => normalizeProviderId(model.provider) === "anthropic",
|
||||||
) as Array<Model<Api>>;
|
) as Array<Model<Api>>;
|
||||||
@@ -201,9 +179,7 @@ describeLive("live anthropic setup-token", () => {
|
|||||||
});
|
});
|
||||||
const tokenError = validateAnthropicSetupToken(apiKeyInfo.apiKey);
|
const tokenError = validateAnthropicSetupToken(apiKeyInfo.apiKey);
|
||||||
if (tokenError) {
|
if (tokenError) {
|
||||||
throw new Error(
|
throw new Error(`Resolved profile is not a setup-token: ${tokenError}`);
|
||||||
`Resolved profile is not a setup-token: ${tokenError}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await completeSimple(
|
const res = await completeSimple(
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ export async function applyUpdateHunk(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const originalLines = originalContents.split("\n");
|
const originalLines = originalContents.split("\n");
|
||||||
if (
|
if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
|
||||||
originalLines.length > 0 &&
|
|
||||||
originalLines[originalLines.length - 1] === ""
|
|
||||||
) {
|
|
||||||
originalLines.pop();
|
originalLines.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,24 +38,16 @@ function computeReplacements(
|
|||||||
|
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
if (chunk.changeContext) {
|
if (chunk.changeContext) {
|
||||||
const ctxIndex = seekSequence(
|
const ctxIndex = seekSequence(originalLines, [chunk.changeContext], lineIndex, false);
|
||||||
originalLines,
|
|
||||||
[chunk.changeContext],
|
|
||||||
lineIndex,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
if (ctxIndex === null) {
|
if (ctxIndex === null) {
|
||||||
throw new Error(
|
throw new Error(`Failed to find context '${chunk.changeContext}' in ${filePath}`);
|
||||||
`Failed to find context '${chunk.changeContext}' in ${filePath}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
lineIndex = ctxIndex + 1;
|
lineIndex = ctxIndex + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chunk.oldLines.length === 0) {
|
if (chunk.oldLines.length === 0) {
|
||||||
const insertionIndex =
|
const insertionIndex =
|
||||||
originalLines.length > 0 &&
|
originalLines.length > 0 && originalLines[originalLines.length - 1] === ""
|
||||||
originalLines[originalLines.length - 1] === ""
|
|
||||||
? originalLines.length - 1
|
? originalLines.length - 1
|
||||||
: originalLines.length;
|
: originalLines.length;
|
||||||
replacements.push([insertionIndex, 0, chunk.newLines]);
|
replacements.push([insertionIndex, 0, chunk.newLines]);
|
||||||
@@ -67,24 +56,14 @@ function computeReplacements(
|
|||||||
|
|
||||||
let pattern = chunk.oldLines;
|
let pattern = chunk.oldLines;
|
||||||
let newSlice = chunk.newLines;
|
let newSlice = chunk.newLines;
|
||||||
let found = seekSequence(
|
let found = seekSequence(originalLines, pattern, lineIndex, chunk.isEndOfFile);
|
||||||
originalLines,
|
|
||||||
pattern,
|
|
||||||
lineIndex,
|
|
||||||
chunk.isEndOfFile,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (found === null && pattern[pattern.length - 1] === "") {
|
if (found === null && pattern[pattern.length - 1] === "") {
|
||||||
pattern = pattern.slice(0, -1);
|
pattern = pattern.slice(0, -1);
|
||||||
if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") {
|
if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") {
|
||||||
newSlice = newSlice.slice(0, -1);
|
newSlice = newSlice.slice(0, -1);
|
||||||
}
|
}
|
||||||
found = seekSequence(
|
found = seekSequence(originalLines, pattern, lineIndex, chunk.isEndOfFile);
|
||||||
originalLines,
|
|
||||||
pattern,
|
|
||||||
lineIndex,
|
|
||||||
chunk.isEndOfFile,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found === null) {
|
if (found === null) {
|
||||||
@@ -142,11 +121,7 @@ function seekSequence(
|
|||||||
if (linesMatch(lines, pattern, i, (value) => value.trim())) return i;
|
if (linesMatch(lines, pattern, i, (value) => value.trim())) return i;
|
||||||
}
|
}
|
||||||
for (let i = searchStart; i <= maxStart; i += 1) {
|
for (let i = searchStart; i <= maxStart; i += 1) {
|
||||||
if (
|
if (linesMatch(lines, pattern, i, (value) => normalizePunctuation(value.trim()))) {
|
||||||
linesMatch(lines, pattern, i, (value) =>
|
|
||||||
normalizePunctuation(value.trim()),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,10 +282,7 @@ function checkPatchBoundariesLenient(lines: string[]): string[] {
|
|||||||
}
|
}
|
||||||
const first = lines[0];
|
const first = lines[0];
|
||||||
const last = lines[lines.length - 1];
|
const last = lines[lines.length - 1];
|
||||||
if (
|
if ((first === "<<EOF" || first === "<<'EOF'" || first === '<<"EOF"') && last.endsWith("EOF")) {
|
||||||
(first === "<<EOF" || first === "<<'EOF'" || first === '<<"EOF"') &&
|
|
||||||
last.endsWith("EOF")
|
|
||||||
) {
|
|
||||||
const inner = lines.slice(1, lines.length - 1);
|
const inner = lines.slice(1, lines.length - 1);
|
||||||
const innerError = checkPatchBoundariesStrict(inner);
|
const innerError = checkPatchBoundariesStrict(inner);
|
||||||
if (!innerError) return inner;
|
if (!innerError) return inner;
|
||||||
@@ -308,10 +305,7 @@ function checkPatchBoundariesStrict(lines: string[]): string | null {
|
|||||||
return "The last line of the patch must be '*** End Patch'";
|
return "The last line of the patch must be '*** End Patch'";
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseOneHunk(
|
function parseOneHunk(lines: string[], lineNumber: number): { hunk: Hunk; consumed: number } {
|
||||||
lines: string[],
|
|
||||||
lineNumber: number,
|
|
||||||
): { hunk: Hunk; consumed: number } {
|
|
||||||
if (lines.length === 0) {
|
if (lines.length === 0) {
|
||||||
throw new Error(`Invalid patch hunk at line ${lineNumber}: empty hunk`);
|
throw new Error(`Invalid patch hunk at line ${lineNumber}: empty hunk`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import {
|
import { buildAuthHealthSummary, DEFAULT_OAUTH_WARN_MS } from "./auth-health.js";
|
||||||
buildAuthHealthSummary,
|
|
||||||
DEFAULT_OAUTH_WARN_MS,
|
|
||||||
} from "./auth-health.js";
|
|
||||||
|
|
||||||
describe("buildAuthHealthSummary", () => {
|
describe("buildAuthHealthSummary", () => {
|
||||||
const now = 1_700_000_000_000;
|
const now = 1_700_000_000_000;
|
||||||
@@ -59,9 +56,7 @@ describe("buildAuthHealthSummary", () => {
|
|||||||
expect(statuses["anthropic:expired"]).toBe("expired");
|
expect(statuses["anthropic:expired"]).toBe("expired");
|
||||||
expect(statuses["anthropic:api"]).toBe("static");
|
expect(statuses["anthropic:api"]).toBe("static");
|
||||||
|
|
||||||
const provider = summary.providers.find(
|
const provider = summary.providers.find((entry) => entry.provider === "anthropic");
|
||||||
(entry) => entry.provider === "anthropic",
|
|
||||||
);
|
|
||||||
expect(provider?.status).toBe("expired");
|
expect(provider?.status).toBe("expired");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,12 +9,7 @@ import {
|
|||||||
|
|
||||||
export type AuthProfileSource = "claude-cli" | "codex-cli" | "store";
|
export type AuthProfileSource = "claude-cli" | "codex-cli" | "store";
|
||||||
|
|
||||||
export type AuthProfileHealthStatus =
|
export type AuthProfileHealthStatus = "ok" | "expiring" | "expired" | "missing" | "static";
|
||||||
| "ok"
|
|
||||||
| "expiring"
|
|
||||||
| "expired"
|
|
||||||
| "missing"
|
|
||||||
| "static";
|
|
||||||
|
|
||||||
export type AuthProfileHealth = {
|
export type AuthProfileHealth = {
|
||||||
profileId: string;
|
profileId: string;
|
||||||
@@ -27,12 +22,7 @@ export type AuthProfileHealth = {
|
|||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthProviderHealthStatus =
|
export type AuthProviderHealthStatus = "ok" | "expiring" | "expired" | "missing" | "static";
|
||||||
| "ok"
|
|
||||||
| "expiring"
|
|
||||||
| "expired"
|
|
||||||
| "missing"
|
|
||||||
| "static";
|
|
||||||
|
|
||||||
export type AuthProviderHealth = {
|
export type AuthProviderHealth = {
|
||||||
provider: string;
|
provider: string;
|
||||||
@@ -111,8 +101,7 @@ function buildProfileHealth(params: {
|
|||||||
|
|
||||||
if (credential.type === "token") {
|
if (credential.type === "token") {
|
||||||
const expiresAt =
|
const expiresAt =
|
||||||
typeof credential.expires === "number" &&
|
typeof credential.expires === "number" && Number.isFinite(credential.expires)
|
||||||
Number.isFinite(credential.expires)
|
|
||||||
? credential.expires
|
? credential.expires
|
||||||
: undefined;
|
: undefined;
|
||||||
if (!expiresAt || expiresAt <= 0) {
|
if (!expiresAt || expiresAt <= 0) {
|
||||||
@@ -125,11 +114,7 @@ function buildProfileHealth(params: {
|
|||||||
label,
|
label,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { status, remainingMs } = resolveOAuthStatus(
|
const { status, remainingMs } = resolveOAuthStatus(expiresAt, now, warnAfterMs);
|
||||||
expiresAt,
|
|
||||||
now,
|
|
||||||
warnAfterMs,
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
profileId,
|
profileId,
|
||||||
provider: credential.provider,
|
provider: credential.provider,
|
||||||
@@ -142,11 +127,7 @@ function buildProfileHealth(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { status, remainingMs } = resolveOAuthStatus(
|
const { status, remainingMs } = resolveOAuthStatus(credential.expires, now, warnAfterMs);
|
||||||
credential.expires,
|
|
||||||
now,
|
|
||||||
warnAfterMs,
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
profileId,
|
profileId,
|
||||||
provider: credential.provider,
|
provider: credential.provider,
|
||||||
@@ -172,9 +153,7 @@ export function buildAuthHealthSummary(params: {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
const profiles = Object.entries(params.store.profiles)
|
const profiles = Object.entries(params.store.profiles)
|
||||||
.filter(([_, cred]) =>
|
.filter(([_, cred]) => (providerFilter ? providerFilter.has(cred.provider) : true))
|
||||||
providerFilter ? providerFilter.has(cred.provider) : true,
|
|
||||||
)
|
|
||||||
.map(([profileId, credential]) =>
|
.map(([profileId, credential]) =>
|
||||||
buildProfileHealth({
|
buildProfileHealth({
|
||||||
profileId,
|
profileId,
|
||||||
@@ -226,9 +205,7 @@ export function buildAuthHealthSummary(params: {
|
|||||||
|
|
||||||
const oauthProfiles = provider.profiles.filter((p) => p.type === "oauth");
|
const oauthProfiles = provider.profiles.filter((p) => p.type === "oauth");
|
||||||
const tokenProfiles = provider.profiles.filter((p) => p.type === "token");
|
const tokenProfiles = provider.profiles.filter((p) => p.type === "token");
|
||||||
const apiKeyProfiles = provider.profiles.filter(
|
const apiKeyProfiles = provider.profiles.filter((p) => p.type === "api_key");
|
||||||
(p) => p.type === "api_key",
|
|
||||||
);
|
|
||||||
|
|
||||||
const expirable = [...oauthProfiles, ...tokenProfiles];
|
const expirable = [...oauthProfiles, ...tokenProfiles];
|
||||||
if (expirable.length === 0) {
|
if (expirable.length === 0) {
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ import {
|
|||||||
ensureAuthProfileStore,
|
ensureAuthProfileStore,
|
||||||
resolveApiKeyForProfile,
|
resolveApiKeyForProfile,
|
||||||
} from "./auth-profiles.js";
|
} from "./auth-profiles.js";
|
||||||
import {
|
import { CHUTES_TOKEN_ENDPOINT, type ChutesStoredOAuth } from "./chutes-oauth.js";
|
||||||
CHUTES_TOKEN_ENDPOINT,
|
|
||||||
type ChutesStoredOAuth,
|
|
||||||
} from "./chutes-oauth.js";
|
|
||||||
|
|
||||||
describe("auth-profiles (chutes)", () => {
|
describe("auth-profiles (chutes)", () => {
|
||||||
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
|
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||||
@@ -30,32 +27,19 @@ describe("auth-profiles (chutes)", () => {
|
|||||||
else process.env.CLAWDBOT_STATE_DIR = previousStateDir;
|
else process.env.CLAWDBOT_STATE_DIR = previousStateDir;
|
||||||
if (previousAgentDir === undefined) delete process.env.CLAWDBOT_AGENT_DIR;
|
if (previousAgentDir === undefined) delete process.env.CLAWDBOT_AGENT_DIR;
|
||||||
else process.env.CLAWDBOT_AGENT_DIR = previousAgentDir;
|
else process.env.CLAWDBOT_AGENT_DIR = previousAgentDir;
|
||||||
if (previousPiAgentDir === undefined)
|
if (previousPiAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
||||||
delete process.env.PI_CODING_AGENT_DIR;
|
|
||||||
else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
||||||
if (previousChutesClientId === undefined)
|
if (previousChutesClientId === undefined) delete process.env.CHUTES_CLIENT_ID;
|
||||||
delete process.env.CHUTES_CLIENT_ID;
|
|
||||||
else process.env.CHUTES_CLIENT_ID = previousChutesClientId;
|
else process.env.CHUTES_CLIENT_ID = previousChutesClientId;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("refreshes expired Chutes OAuth credentials", async () => {
|
it("refreshes expired Chutes OAuth credentials", async () => {
|
||||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-chutes-"));
|
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-chutes-"));
|
||||||
process.env.CLAWDBOT_STATE_DIR = tempDir;
|
process.env.CLAWDBOT_STATE_DIR = tempDir;
|
||||||
process.env.CLAWDBOT_AGENT_DIR = path.join(
|
process.env.CLAWDBOT_AGENT_DIR = path.join(tempDir, "agents", "main", "agent");
|
||||||
tempDir,
|
|
||||||
"agents",
|
|
||||||
"main",
|
|
||||||
"agent",
|
|
||||||
);
|
|
||||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||||
|
|
||||||
const authProfilePath = path.join(
|
const authProfilePath = path.join(tempDir, "agents", "main", "agent", "auth-profiles.json");
|
||||||
tempDir,
|
|
||||||
"agents",
|
|
||||||
"main",
|
|
||||||
"agent",
|
|
||||||
"auth-profiles.json",
|
|
||||||
);
|
|
||||||
await fs.mkdir(path.dirname(authProfilePath), { recursive: true });
|
await fs.mkdir(path.dirname(authProfilePath), { recursive: true });
|
||||||
|
|
||||||
const store: AuthProfileStore = {
|
const store: AuthProfileStore = {
|
||||||
@@ -75,8 +59,7 @@ describe("auth-profiles (chutes)", () => {
|
|||||||
|
|
||||||
const fetchSpy = vi.fn(async (input: string | URL) => {
|
const fetchSpy = vi.fn(async (input: string | URL) => {
|
||||||
const url = typeof input === "string" ? input : input.toString();
|
const url = typeof input === "string" ? input : input.toString();
|
||||||
if (url !== CHUTES_TOKEN_ENDPOINT)
|
if (url !== CHUTES_TOKEN_ENDPOINT) return new Response("not found", { status: 404 });
|
||||||
return new Response("not found", { status: 404 });
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
access_token: "at_new",
|
access_token: "at_new",
|
||||||
@@ -96,9 +79,7 @@ describe("auth-profiles (chutes)", () => {
|
|||||||
expect(resolved?.apiKey).toBe("at_new");
|
expect(resolved?.apiKey).toBe("at_new");
|
||||||
expect(fetchSpy).toHaveBeenCalled();
|
expect(fetchSpy).toHaveBeenCalled();
|
||||||
|
|
||||||
const persisted = JSON.parse(
|
const persisted = JSON.parse(await fs.readFile(authProfilePath, "utf8")) as {
|
||||||
await fs.readFile(authProfilePath, "utf8"),
|
|
||||||
) as {
|
|
||||||
profiles?: Record<string, { access?: string }>;
|
profiles?: Record<string, { access?: string }>;
|
||||||
};
|
};
|
||||||
expect(persisted.profiles?.["chutes:default"]?.access).toBe("at_new");
|
expect(persisted.profiles?.["chutes:default"]?.access).toBe("at_new");
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import { ensureAuthProfileStore } from "./auth-profiles.js";
|
|||||||
|
|
||||||
describe("ensureAuthProfileStore", () => {
|
describe("ensureAuthProfileStore", () => {
|
||||||
it("migrates legacy auth.json and deletes it (PR #368)", () => {
|
it("migrates legacy auth.json and deletes it (PR #368)", () => {
|
||||||
const agentDir = fs.mkdtempSync(
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-auth-profiles-"));
|
||||||
path.join(os.tmpdir(), "clawdbot-auth-profiles-"),
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
const legacyPath = path.join(agentDir, "auth.json");
|
const legacyPath = path.join(agentDir, "auth.json");
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@@ -3,16 +3,11 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||||
import {
|
import { CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js";
|
||||||
CLAUDE_CLI_PROFILE_ID,
|
|
||||||
ensureAuthProfileStore,
|
|
||||||
} from "./auth-profiles.js";
|
|
||||||
|
|
||||||
describe("external CLI credential sync", () => {
|
describe("external CLI credential sync", () => {
|
||||||
it("does not overwrite API keys when syncing external CLI creds", async () => {
|
it("does not overwrite API keys when syncing external CLI creds", async () => {
|
||||||
const agentDir = fs.mkdtempSync(
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-no-overwrite-"));
|
||||||
path.join(os.tmpdir(), "clawdbot-no-overwrite-"),
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await withTempHome(
|
await withTempHome(
|
||||||
async (tempHome) => {
|
async (tempHome) => {
|
||||||
@@ -26,10 +21,7 @@ describe("external CLI credential sync", () => {
|
|||||||
expiresAt: Date.now() + 30 * 60 * 1000,
|
expiresAt: Date.now() + 30 * 60 * 1000,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(claudeCreds));
|
||||||
path.join(claudeDir, ".credentials.json"),
|
|
||||||
JSON.stringify(claudeCreds),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create auth-profiles.json with an API key
|
// Create auth-profiles.json with an API key
|
||||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||||
@@ -50,9 +42,7 @@ describe("external CLI credential sync", () => {
|
|||||||
const store = ensureAuthProfileStore(agentDir);
|
const store = ensureAuthProfileStore(agentDir);
|
||||||
|
|
||||||
// Should keep the store's API key and still add the CLI profile.
|
// Should keep the store's API key and still add the CLI profile.
|
||||||
expect(
|
expect((store.profiles["anthropic:default"] as { key: string }).key).toBe("sk-store");
|
||||||
(store.profiles["anthropic:default"] as { key: string }).key,
|
|
||||||
).toBe("sk-store");
|
|
||||||
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
|
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
|
||||||
},
|
},
|
||||||
{ prefix: "clawdbot-home-" },
|
{ prefix: "clawdbot-home-" },
|
||||||
@@ -62,9 +52,7 @@ describe("external CLI credential sync", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
it("prefers oauth over token even if token has later expiry (oauth enables auto-refresh)", async () => {
|
it("prefers oauth over token even if token has later expiry (oauth enables auto-refresh)", async () => {
|
||||||
const agentDir = fs.mkdtempSync(
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-oauth-preferred-"));
|
||||||
path.join(os.tmpdir(), "clawdbot-cli-oauth-preferred-"),
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await withTempHome(
|
await withTempHome(
|
||||||
async (tempHome) => {
|
async (tempHome) => {
|
||||||
@@ -103,9 +91,7 @@ describe("external CLI credential sync", () => {
|
|||||||
// OAuth should be preferred over token because it can auto-refresh
|
// OAuth should be preferred over token because it can auto-refresh
|
||||||
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||||
expect(cliProfile.type).toBe("oauth");
|
expect(cliProfile.type).toBe("oauth");
|
||||||
expect((cliProfile as { access: string }).access).toBe(
|
expect((cliProfile as { access: string }).access).toBe("cli-oauth-access");
|
||||||
"cli-oauth-access",
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{ prefix: "clawdbot-home-" },
|
{ prefix: "clawdbot-home-" },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,16 +3,11 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||||
import {
|
import { CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js";
|
||||||
CLAUDE_CLI_PROFILE_ID,
|
|
||||||
ensureAuthProfileStore,
|
|
||||||
} from "./auth-profiles.js";
|
|
||||||
|
|
||||||
describe("external CLI credential sync", () => {
|
describe("external CLI credential sync", () => {
|
||||||
it("does not overwrite fresher store oauth with older CLI oauth", async () => {
|
it("does not overwrite fresher store oauth with older CLI oauth", async () => {
|
||||||
const agentDir = fs.mkdtempSync(
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-oauth-no-downgrade-"));
|
||||||
path.join(os.tmpdir(), "clawdbot-cli-oauth-no-downgrade-"),
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await withTempHome(
|
await withTempHome(
|
||||||
async (tempHome) => {
|
async (tempHome) => {
|
||||||
@@ -52,9 +47,7 @@ describe("external CLI credential sync", () => {
|
|||||||
// Fresher store oauth should be kept
|
// Fresher store oauth should be kept
|
||||||
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||||
expect(cliProfile.type).toBe("oauth");
|
expect(cliProfile.type).toBe("oauth");
|
||||||
expect((cliProfile as { access: string }).access).toBe(
|
expect((cliProfile as { access: string }).access).toBe("store-oauth-access");
|
||||||
"store-oauth-access",
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{ prefix: "clawdbot-home-" },
|
{ prefix: "clawdbot-home-" },
|
||||||
);
|
);
|
||||||
@@ -63,9 +56,7 @@ describe("external CLI credential sync", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
it("does not downgrade store oauth to token when CLI lacks refresh token", async () => {
|
it("does not downgrade store oauth to token when CLI lacks refresh token", async () => {
|
||||||
const agentDir = fs.mkdtempSync(
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-no-downgrade-oauth-"));
|
||||||
path.join(os.tmpdir(), "clawdbot-cli-no-downgrade-oauth-"),
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await withTempHome(
|
await withTempHome(
|
||||||
async (tempHome) => {
|
async (tempHome) => {
|
||||||
@@ -104,9 +95,7 @@ describe("external CLI credential sync", () => {
|
|||||||
// Keep oauth to preserve auto-refresh capability
|
// Keep oauth to preserve auto-refresh capability
|
||||||
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||||
expect(cliProfile.type).toBe("oauth");
|
expect(cliProfile.type).toBe("oauth");
|
||||||
expect((cliProfile as { access: string }).access).toBe(
|
expect((cliProfile as { access: string }).access).toBe("store-oauth-access");
|
||||||
"store-oauth-access",
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{ prefix: "clawdbot-home-" },
|
{ prefix: "clawdbot-home-" },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,16 +3,11 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||||
import {
|
import { CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js";
|
||||||
CLAUDE_CLI_PROFILE_ID,
|
|
||||||
ensureAuthProfileStore,
|
|
||||||
} from "./auth-profiles.js";
|
|
||||||
|
|
||||||
describe("external CLI credential sync", () => {
|
describe("external CLI credential sync", () => {
|
||||||
it("syncs Claude CLI OAuth credentials into anthropic:claude-cli", async () => {
|
it("syncs Claude CLI OAuth credentials into anthropic:claude-cli", async () => {
|
||||||
const agentDir = fs.mkdtempSync(
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-sync-"));
|
||||||
path.join(os.tmpdir(), "clawdbot-cli-sync-"),
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
// Create a temp home with Claude CLI credentials
|
// Create a temp home with Claude CLI credentials
|
||||||
await withTempHome(
|
await withTempHome(
|
||||||
@@ -27,10 +22,7 @@ describe("external CLI credential sync", () => {
|
|||||||
expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now
|
expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(claudeCreds));
|
||||||
path.join(claudeDir, ".credentials.json"),
|
|
||||||
JSON.stringify(claudeCreds),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create empty auth-profiles.json
|
// Create empty auth-profiles.json
|
||||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||||
@@ -52,22 +44,14 @@ describe("external CLI credential sync", () => {
|
|||||||
const store = ensureAuthProfileStore(agentDir);
|
const store = ensureAuthProfileStore(agentDir);
|
||||||
|
|
||||||
expect(store.profiles["anthropic:default"]).toBeDefined();
|
expect(store.profiles["anthropic:default"]).toBeDefined();
|
||||||
expect(
|
expect((store.profiles["anthropic:default"] as { key: string }).key).toBe("sk-default");
|
||||||
(store.profiles["anthropic:default"] as { key: string }).key,
|
|
||||||
).toBe("sk-default");
|
|
||||||
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
|
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
|
||||||
// Should be stored as OAuth credential (type: "oauth") for auto-refresh
|
// Should be stored as OAuth credential (type: "oauth") for auto-refresh
|
||||||
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||||
expect(cliProfile.type).toBe("oauth");
|
expect(cliProfile.type).toBe("oauth");
|
||||||
expect((cliProfile as { access: string }).access).toBe(
|
expect((cliProfile as { access: string }).access).toBe("fresh-access-token");
|
||||||
"fresh-access-token",
|
expect((cliProfile as { refresh: string }).refresh).toBe("fresh-refresh-token");
|
||||||
);
|
expect((cliProfile as { expires: number }).expires).toBeGreaterThan(Date.now());
|
||||||
expect((cliProfile as { refresh: string }).refresh).toBe(
|
|
||||||
"fresh-refresh-token",
|
|
||||||
);
|
|
||||||
expect((cliProfile as { expires: number }).expires).toBeGreaterThan(
|
|
||||||
Date.now(),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{ prefix: "clawdbot-home-" },
|
{ prefix: "clawdbot-home-" },
|
||||||
);
|
);
|
||||||
@@ -76,9 +60,7 @@ describe("external CLI credential sync", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
it("syncs Claude CLI credentials without refreshToken as token type", async () => {
|
it("syncs Claude CLI credentials without refreshToken as token type", async () => {
|
||||||
const agentDir = fs.mkdtempSync(
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-token-sync-"));
|
||||||
path.join(os.tmpdir(), "clawdbot-cli-token-sync-"),
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await withTempHome(
|
await withTempHome(
|
||||||
async (tempHome) => {
|
async (tempHome) => {
|
||||||
@@ -92,16 +74,10 @@ describe("external CLI credential sync", () => {
|
|||||||
expiresAt: Date.now() + 60 * 60 * 1000,
|
expiresAt: Date.now() + 60 * 60 * 1000,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(claudeCreds));
|
||||||
path.join(claudeDir, ".credentials.json"),
|
|
||||||
JSON.stringify(claudeCreds),
|
|
||||||
);
|
|
||||||
|
|
||||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(authPath, JSON.stringify({ version: 1, profiles: {} }));
|
||||||
authPath,
|
|
||||||
JSON.stringify({ version: 1, profiles: {} }),
|
|
||||||
);
|
|
||||||
|
|
||||||
const store = ensureAuthProfileStore(agentDir);
|
const store = ensureAuthProfileStore(agentDir);
|
||||||
|
|
||||||
@@ -109,9 +85,7 @@ describe("external CLI credential sync", () => {
|
|||||||
// Should be stored as token type (no refresh capability)
|
// Should be stored as token type (no refresh capability)
|
||||||
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||||
expect(cliProfile.type).toBe("token");
|
expect(cliProfile.type).toBe("token");
|
||||||
expect((cliProfile as { token: string }).token).toBe(
|
expect((cliProfile as { token: string }).token).toBe("access-only-token");
|
||||||
"access-only-token",
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{ prefix: "clawdbot-home-" },
|
{ prefix: "clawdbot-home-" },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,16 +3,11 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { withTempHome } from "../../test/helpers/temp-home.js";
|
import { withTempHome } from "../../test/helpers/temp-home.js";
|
||||||
import {
|
import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js";
|
||||||
CODEX_CLI_PROFILE_ID,
|
|
||||||
ensureAuthProfileStore,
|
|
||||||
} from "./auth-profiles.js";
|
|
||||||
|
|
||||||
describe("external CLI credential sync", () => {
|
describe("external CLI credential sync", () => {
|
||||||
it("updates codex-cli profile when Codex CLI refresh token changes", async () => {
|
it("updates codex-cli profile when Codex CLI refresh token changes", async () => {
|
||||||
const agentDir = fs.mkdtempSync(
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-refresh-sync-"));
|
||||||
path.join(os.tmpdir(), "clawdbot-codex-refresh-sync-"),
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await withTempHome(
|
await withTempHome(
|
||||||
async (tempHome) => {
|
async (tempHome) => {
|
||||||
@@ -48,10 +43,9 @@ describe("external CLI credential sync", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const store = ensureAuthProfileStore(agentDir);
|
const store = ensureAuthProfileStore(agentDir);
|
||||||
expect(
|
expect((store.profiles[CODEX_CLI_PROFILE_ID] as { refresh: string }).refresh).toBe(
|
||||||
(store.profiles[CODEX_CLI_PROFILE_ID] as { refresh: string })
|
"new-refresh",
|
||||||
.refresh,
|
);
|
||||||
).toBe("new-refresh");
|
|
||||||
},
|
},
|
||||||
{ prefix: "clawdbot-home-" },
|
{ prefix: "clawdbot-home-" },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ import {
|
|||||||
|
|
||||||
describe("external CLI credential sync", () => {
|
describe("external CLI credential sync", () => {
|
||||||
it("upgrades token to oauth when Claude CLI gets refreshToken", async () => {
|
it("upgrades token to oauth when Claude CLI gets refreshToken", async () => {
|
||||||
const agentDir = fs.mkdtempSync(
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-upgrade-"));
|
||||||
path.join(os.tmpdir(), "clawdbot-cli-upgrade-"),
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await withTempHome(
|
await withTempHome(
|
||||||
async (tempHome) => {
|
async (tempHome) => {
|
||||||
@@ -53,12 +51,8 @@ describe("external CLI credential sync", () => {
|
|||||||
// Should upgrade from token to oauth
|
// Should upgrade from token to oauth
|
||||||
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||||
expect(cliProfile.type).toBe("oauth");
|
expect(cliProfile.type).toBe("oauth");
|
||||||
expect((cliProfile as { access: string }).access).toBe(
|
expect((cliProfile as { access: string }).access).toBe("new-oauth-access");
|
||||||
"new-oauth-access",
|
expect((cliProfile as { refresh: string }).refresh).toBe("new-refresh-token");
|
||||||
);
|
|
||||||
expect((cliProfile as { refresh: string }).refresh).toBe(
|
|
||||||
"new-refresh-token",
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
{ prefix: "clawdbot-home-" },
|
{ prefix: "clawdbot-home-" },
|
||||||
);
|
);
|
||||||
@@ -67,9 +61,7 @@ describe("external CLI credential sync", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
it("syncs Codex CLI credentials into openai-codex:codex-cli", async () => {
|
it("syncs Codex CLI credentials into openai-codex:codex-cli", async () => {
|
||||||
const agentDir = fs.mkdtempSync(
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-sync-"));
|
||||||
path.join(os.tmpdir(), "clawdbot-codex-sync-"),
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await withTempHome(
|
await withTempHome(
|
||||||
async (tempHome) => {
|
async (tempHome) => {
|
||||||
@@ -98,9 +90,9 @@ describe("external CLI credential sync", () => {
|
|||||||
const store = ensureAuthProfileStore(agentDir);
|
const store = ensureAuthProfileStore(agentDir);
|
||||||
|
|
||||||
expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeDefined();
|
expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeDefined();
|
||||||
expect(
|
expect((store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access).toBe(
|
||||||
(store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access,
|
"codex-access-token",
|
||||||
).toBe("codex-access-token");
|
);
|
||||||
},
|
},
|
||||||
{ prefix: "clawdbot-home-" },
|
{ prefix: "clawdbot-home-" },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import fs from "node:fs";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import { ensureAuthProfileStore, markAuthProfileFailure } from "./auth-profiles.js";
|
||||||
ensureAuthProfileStore,
|
|
||||||
markAuthProfileFailure,
|
|
||||||
} from "./auth-profiles.js";
|
|
||||||
|
|
||||||
describe("markAuthProfileFailure", () => {
|
describe("markAuthProfileFailure", () => {
|
||||||
it("disables billing failures for ~5 hours by default", async () => {
|
it("disables billing failures for ~5 hours by default", async () => {
|
||||||
@@ -35,8 +32,7 @@ describe("markAuthProfileFailure", () => {
|
|||||||
agentDir,
|
agentDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
const disabledUntil =
|
const disabledUntil = store.usageStats?.["anthropic:default"]?.disabledUntil;
|
||||||
store.usageStats?.["anthropic:default"]?.disabledUntil;
|
|
||||||
expect(typeof disabledUntil).toBe("number");
|
expect(typeof disabledUntil).toBe("number");
|
||||||
const remainingMs = (disabledUntil as number) - startedAt;
|
const remainingMs = (disabledUntil as number) - startedAt;
|
||||||
expect(remainingMs).toBeGreaterThan(4.5 * 60 * 60 * 1000);
|
expect(remainingMs).toBeGreaterThan(4.5 * 60 * 60 * 1000);
|
||||||
@@ -80,8 +76,7 @@ describe("markAuthProfileFailure", () => {
|
|||||||
} as never,
|
} as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
const disabledUntil =
|
const disabledUntil = store.usageStats?.["anthropic:default"]?.disabledUntil;
|
||||||
store.usageStats?.["anthropic:default"]?.disabledUntil;
|
|
||||||
expect(typeof disabledUntil).toBe("number");
|
expect(typeof disabledUntil).toBe("number");
|
||||||
const remainingMs = (disabledUntil as number) - startedAt;
|
const remainingMs = (disabledUntil as number) - startedAt;
|
||||||
expect(remainingMs).toBeGreaterThan(0.8 * 60 * 60 * 1000);
|
expect(remainingMs).toBeGreaterThan(0.8 * 60 * 60 * 1000);
|
||||||
@@ -128,9 +123,7 @@ describe("markAuthProfileFailure", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(1);
|
expect(store.usageStats?.["anthropic:default"]?.errorCount).toBe(1);
|
||||||
expect(
|
expect(store.usageStats?.["anthropic:default"]?.failureCounts?.billing).toBe(1);
|
||||||
store.usageStats?.["anthropic:default"]?.failureCounts?.billing,
|
|
||||||
).toBe(1);
|
|
||||||
} finally {
|
} finally {
|
||||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,10 +91,6 @@ describe("resolveAuthProfileOrder", () => {
|
|||||||
},
|
},
|
||||||
provider: "anthropic",
|
provider: "anthropic",
|
||||||
});
|
});
|
||||||
expect(order).toEqual([
|
expect(order).toEqual(["anthropic:ready", "anthropic:cool2", "anthropic:cool1"]);
|
||||||
"anthropic:ready",
|
|
||||||
"anthropic:cool2",
|
|
||||||
"anthropic:cool1",
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
export {
|
export { CLAUDE_CLI_PROFILE_ID, CODEX_CLI_PROFILE_ID } from "./auth-profiles/constants.js";
|
||||||
CLAUDE_CLI_PROFILE_ID,
|
|
||||||
CODEX_CLI_PROFILE_ID,
|
|
||||||
} from "./auth-profiles/constants.js";
|
|
||||||
export { resolveAuthProfileDisplayLabel } from "./auth-profiles/display.js";
|
export { resolveAuthProfileDisplayLabel } from "./auth-profiles/display.js";
|
||||||
export { formatAuthDoctorHint } from "./auth-profiles/doctor.js";
|
export { formatAuthDoctorHint } from "./auth-profiles/doctor.js";
|
||||||
export { resolveApiKeyForProfile } from "./auth-profiles/oauth.js";
|
export { resolveApiKeyForProfile } from "./auth-profiles/oauth.js";
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ export function resolveAuthProfileDisplayLabel(params: {
|
|||||||
const configEmail = cfg?.auth?.profiles?.[profileId]?.email?.trim();
|
const configEmail = cfg?.auth?.profiles?.[profileId]?.email?.trim();
|
||||||
const email =
|
const email =
|
||||||
configEmail ||
|
configEmail ||
|
||||||
(profile && "email" in profile
|
(profile && "email" in profile ? (profile.email as string | undefined)?.trim() : undefined);
|
||||||
? (profile.email as string | undefined)?.trim()
|
|
||||||
: undefined);
|
|
||||||
if (email) return `${profileId} (${email})`;
|
if (email) return `${profileId} (${email})`;
|
||||||
return profileId;
|
return profileId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ export function formatAuthDoctorHint(params: {
|
|||||||
"Doctor hint (for GitHub issue):",
|
"Doctor hint (for GitHub issue):",
|
||||||
`- provider: ${providerKey}`,
|
`- provider: ${providerKey}`,
|
||||||
`- config: ${legacyProfileId}${
|
`- config: ${legacyProfileId}${
|
||||||
cfgProvider || cfgMode
|
cfgProvider || cfgMode ? ` (provider=${cfgProvider ?? "?"}, mode=${cfgMode ?? "?"})` : ""
|
||||||
? ` (provider=${cfgProvider ?? "?"}, mode=${cfgMode ?? "?"})`
|
|
||||||
: ""
|
|
||||||
}`,
|
}`,
|
||||||
`- auth store oauth profiles: ${storeOauthProfiles || "(none)"}`,
|
`- auth store oauth profiles: ${storeOauthProfiles || "(none)"}`,
|
||||||
`- suggested profile: ${suggested}`,
|
`- suggested profile: ${suggested}`,
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ import type {
|
|||||||
TokenCredential,
|
TokenCredential,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
function shallowEqualOAuthCredentials(
|
function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean {
|
||||||
a: OAuthCredential | undefined,
|
|
||||||
b: OAuthCredential,
|
|
||||||
): boolean {
|
|
||||||
if (!a) return false;
|
if (!a) return false;
|
||||||
if (a.type !== "oauth") return false;
|
if (a.type !== "oauth") return false;
|
||||||
return (
|
return (
|
||||||
@@ -34,10 +31,7 @@ function shallowEqualOAuthCredentials(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shallowEqualTokenCredentials(
|
function shallowEqualTokenCredentials(a: TokenCredential | undefined, b: TokenCredential): boolean {
|
||||||
a: TokenCredential | undefined,
|
|
||||||
b: TokenCredential,
|
|
||||||
): boolean {
|
|
||||||
if (!a) return false;
|
if (!a) return false;
|
||||||
if (a.type !== "token") return false;
|
if (a.type !== "token") return false;
|
||||||
return (
|
return (
|
||||||
@@ -48,10 +42,7 @@ function shallowEqualTokenCredentials(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isExternalProfileFresh(
|
function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean {
|
||||||
cred: AuthProfileCredential | undefined,
|
|
||||||
now: number,
|
|
||||||
): boolean {
|
|
||||||
if (!cred) return false;
|
if (!cred) return false;
|
||||||
if (cred.type !== "oauth" && cred.type !== "token") return false;
|
if (cred.type !== "oauth" && cred.type !== "token") return false;
|
||||||
if (cred.provider !== "anthropic" && cred.provider !== "openai-codex") {
|
if (cred.provider !== "anthropic" && cred.provider !== "openai-codex") {
|
||||||
@@ -104,8 +95,7 @@ export function syncExternalCliCredentials(
|
|||||||
!existingOAuth ||
|
!existingOAuth ||
|
||||||
existingOAuth.provider !== "anthropic" ||
|
existingOAuth.provider !== "anthropic" ||
|
||||||
existingOAuth.expires <= now ||
|
existingOAuth.expires <= now ||
|
||||||
(claudeCredsExpires > now &&
|
(claudeCredsExpires > now && claudeCredsExpires > existingOAuth.expires);
|
||||||
claudeCredsExpires > existingOAuth.expires);
|
|
||||||
} else {
|
} else {
|
||||||
const existingToken = existing?.type === "token" ? existing : undefined;
|
const existingToken = existing?.type === "token" ? existing : undefined;
|
||||||
isEqual = shallowEqualTokenCredentials(existingToken, claudeCreds);
|
isEqual = shallowEqualTokenCredentials(existingToken, claudeCreds);
|
||||||
@@ -114,8 +104,7 @@ export function syncExternalCliCredentials(
|
|||||||
!existingToken ||
|
!existingToken ||
|
||||||
existingToken.provider !== "anthropic" ||
|
existingToken.provider !== "anthropic" ||
|
||||||
(existingToken.expires ?? 0) <= now ||
|
(existingToken.expires ?? 0) <= now ||
|
||||||
(claudeCredsExpires > now &&
|
(claudeCredsExpires > now && claudeCredsExpires > (existingToken.expires ?? 0));
|
||||||
claudeCredsExpires > (existingToken.expires ?? 0));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also update if credential type changed (token -> oauth upgrade)
|
// Also update if credential type changed (token -> oauth upgrade)
|
||||||
@@ -166,10 +155,7 @@ export function syncExternalCliCredentials(
|
|||||||
existingOAuth.expires <= now ||
|
existingOAuth.expires <= now ||
|
||||||
codexCreds.expires > existingOAuth.expires;
|
codexCreds.expires > existingOAuth.expires;
|
||||||
|
|
||||||
if (
|
if (shouldUpdate && !shallowEqualOAuthCredentials(existingOAuth, codexCreds)) {
|
||||||
shouldUpdate &&
|
|
||||||
!shallowEqualOAuthCredentials(existingOAuth, codexCreds)
|
|
||||||
) {
|
|
||||||
store.profiles[CODEX_CLI_PROFILE_ID] = codexCreds;
|
store.profiles[CODEX_CLI_PROFILE_ID] = codexCreds;
|
||||||
mutated = true;
|
mutated = true;
|
||||||
log.info("synced openai-codex credentials from codex cli", {
|
log.info("synced openai-codex credentials from codex cli", {
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { getOAuthApiKey, type OAuthCredentials, type OAuthProvider } from "@mariozechner/pi-ai";
|
||||||
getOAuthApiKey,
|
|
||||||
type OAuthCredentials,
|
|
||||||
type OAuthProvider,
|
|
||||||
} from "@mariozechner/pi-ai";
|
|
||||||
import lockfile from "proper-lockfile";
|
import lockfile from "proper-lockfile";
|
||||||
|
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
@@ -15,12 +11,8 @@ import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js";
|
|||||||
import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js";
|
import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js";
|
||||||
import type { AuthProfileStore } from "./types.js";
|
import type { AuthProfileStore } from "./types.js";
|
||||||
|
|
||||||
function buildOAuthApiKey(
|
function buildOAuthApiKey(provider: string, credentials: OAuthCredentials): string {
|
||||||
provider: string,
|
const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity";
|
||||||
credentials: OAuthCredentials,
|
|
||||||
): string {
|
|
||||||
const needsProjectId =
|
|
||||||
provider === "google-gemini-cli" || provider === "google-antigravity";
|
|
||||||
return needsProjectId
|
return needsProjectId
|
||||||
? JSON.stringify({
|
? JSON.stringify({
|
||||||
token: credentials.access,
|
token: credentials.access,
|
||||||
@@ -76,10 +68,7 @@ async function refreshOAuthTokenWithLock(params: {
|
|||||||
|
|
||||||
// Sync refreshed credentials back to Claude CLI if this is the claude-cli profile
|
// Sync refreshed credentials back to Claude CLI if this is the claude-cli profile
|
||||||
// This ensures Claude Code continues to work after ClawdBot refreshes the token
|
// This ensures Claude Code continues to work after ClawdBot refreshes the token
|
||||||
if (
|
if (params.profileId === CLAUDE_CLI_PROFILE_ID && cred.provider === "anthropic") {
|
||||||
params.profileId === CLAUDE_CLI_PROFILE_ID &&
|
|
||||||
cred.provider === "anthropic"
|
|
||||||
) {
|
|
||||||
writeClaudeCliCredentials(result.newCredentials);
|
writeClaudeCliCredentials(result.newCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,17 +43,12 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
const explicitOrder = storedOrder ?? configuredOrder;
|
const explicitOrder = storedOrder ?? configuredOrder;
|
||||||
const explicitProfiles = cfg?.auth?.profiles
|
const explicitProfiles = cfg?.auth?.profiles
|
||||||
? Object.entries(cfg.auth.profiles)
|
? Object.entries(cfg.auth.profiles)
|
||||||
.filter(
|
.filter(([, profile]) => normalizeProviderId(profile.provider) === providerKey)
|
||||||
([, profile]) =>
|
|
||||||
normalizeProviderId(profile.provider) === providerKey,
|
|
||||||
)
|
|
||||||
.map(([profileId]) => profileId)
|
.map(([profileId]) => profileId)
|
||||||
: [];
|
: [];
|
||||||
const baseOrder =
|
const baseOrder =
|
||||||
explicitOrder ??
|
explicitOrder ??
|
||||||
(explicitProfiles.length > 0
|
(explicitProfiles.length > 0 ? explicitProfiles : listProfilesForProvider(store, providerKey));
|
||||||
? explicitProfiles
|
|
||||||
: listProfilesForProvider(store, providerKey));
|
|
||||||
if (baseOrder.length === 0) return [];
|
if (baseOrder.length === 0) return [];
|
||||||
|
|
||||||
const filtered = baseOrder.filter((profileId) => {
|
const filtered = baseOrder.filter((profileId) => {
|
||||||
@@ -66,8 +61,7 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (profileConfig.mode !== cred.type) {
|
if (profileConfig.mode !== cred.type) {
|
||||||
const oauthCompatible =
|
const oauthCompatible = profileConfig.mode === "oauth" && cred.type === "token";
|
||||||
profileConfig.mode === "oauth" && cred.type === "token";
|
|
||||||
if (!oauthCompatible) return false;
|
if (!oauthCompatible) return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,8 +98,7 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
const inCooldown: Array<{ profileId: string; cooldownUntil: number }> = [];
|
const inCooldown: Array<{ profileId: string; cooldownUntil: number }> = [];
|
||||||
|
|
||||||
for (const profileId of deduped) {
|
for (const profileId of deduped) {
|
||||||
const cooldownUntil =
|
const cooldownUntil = resolveProfileUnusableUntil(store.usageStats?.[profileId] ?? {}) ?? 0;
|
||||||
resolveProfileUnusableUntil(store.usageStats?.[profileId] ?? {}) ?? 0;
|
|
||||||
if (
|
if (
|
||||||
typeof cooldownUntil === "number" &&
|
typeof cooldownUntil === "number" &&
|
||||||
Number.isFinite(cooldownUntil) &&
|
Number.isFinite(cooldownUntil) &&
|
||||||
@@ -126,10 +119,7 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
|
|
||||||
// Still put preferredProfile first if specified
|
// Still put preferredProfile first if specified
|
||||||
if (preferredProfile && ordered.includes(preferredProfile)) {
|
if (preferredProfile && ordered.includes(preferredProfile)) {
|
||||||
return [
|
return [preferredProfile, ...ordered.filter((e) => e !== preferredProfile)];
|
||||||
preferredProfile,
|
|
||||||
...ordered.filter((e) => e !== preferredProfile),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
return ordered;
|
return ordered;
|
||||||
}
|
}
|
||||||
@@ -146,10 +136,7 @@ export function resolveAuthProfileOrder(params: {
|
|||||||
return sorted;
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
function orderProfilesByMode(
|
function orderProfilesByMode(order: string[], store: AuthProfileStore): string[] {
|
||||||
order: string[],
|
|
||||||
store: AuthProfileStore,
|
|
||||||
): string[] {
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// Partition into available and in-cooldown
|
// Partition into available and in-cooldown
|
||||||
@@ -168,8 +155,7 @@ function orderProfilesByMode(
|
|||||||
// Then by lastUsed (oldest first = round-robin within type)
|
// Then by lastUsed (oldest first = round-robin within type)
|
||||||
const scored = available.map((profileId) => {
|
const scored = available.map((profileId) => {
|
||||||
const type = store.profiles[profileId]?.type;
|
const type = store.profiles[profileId]?.type;
|
||||||
const typeScore =
|
const typeScore = type === "oauth" ? 0 : type === "token" ? 1 : type === "api_key" ? 2 : 3;
|
||||||
type === "oauth" ? 0 : type === "token" ? 1 : type === "api_key" ? 2 : 3;
|
|
||||||
const lastUsed = store.usageStats?.[profileId]?.lastUsed ?? 0;
|
const lastUsed = store.usageStats?.[profileId]?.lastUsed ?? 0;
|
||||||
return { profileId, typeScore, lastUsed };
|
return { profileId, typeScore, lastUsed };
|
||||||
});
|
});
|
||||||
@@ -189,8 +175,7 @@ function orderProfilesByMode(
|
|||||||
const cooldownSorted = inCooldown
|
const cooldownSorted = inCooldown
|
||||||
.map((profileId) => ({
|
.map((profileId) => ({
|
||||||
profileId,
|
profileId,
|
||||||
cooldownUntil:
|
cooldownUntil: resolveProfileUnusableUntil(store.usageStats?.[profileId] ?? {}) ?? now,
|
||||||
resolveProfileUnusableUntil(store.usageStats?.[profileId] ?? {}) ?? now,
|
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.cooldownUntil - b.cooldownUntil)
|
.sort((a, b) => a.cooldownUntil - b.cooldownUntil)
|
||||||
.map((entry) => entry.profileId);
|
.map((entry) => entry.profileId);
|
||||||
|
|||||||
@@ -4,11 +4,7 @@ import path from "node:path";
|
|||||||
import { saveJsonFile } from "../../infra/json-file.js";
|
import { saveJsonFile } from "../../infra/json-file.js";
|
||||||
import { resolveUserPath } from "../../utils.js";
|
import { resolveUserPath } from "../../utils.js";
|
||||||
import { resolveClawdbotAgentDir } from "../agent-paths.js";
|
import { resolveClawdbotAgentDir } from "../agent-paths.js";
|
||||||
import {
|
import { AUTH_PROFILE_FILENAME, AUTH_STORE_VERSION, LEGACY_AUTH_FILENAME } from "./constants.js";
|
||||||
AUTH_PROFILE_FILENAME,
|
|
||||||
AUTH_STORE_VERSION,
|
|
||||||
LEGACY_AUTH_FILENAME,
|
|
||||||
} from "./constants.js";
|
|
||||||
import type { AuthProfileStore } from "./types.js";
|
import type { AuthProfileStore } from "./types.js";
|
||||||
|
|
||||||
export function resolveAuthStorePath(agentDir?: string): string {
|
export function resolveAuthStorePath(agentDir?: string): string {
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ export function upsertAuthProfile(params: {
|
|||||||
saveAuthProfileStore(store, params.agentDir);
|
saveAuthProfileStore(store, params.agentDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listProfilesForProvider(
|
export function listProfilesForProvider(store: AuthProfileStore, provider: string): string[] {
|
||||||
store: AuthProfileStore,
|
|
||||||
provider: string,
|
|
||||||
): string[] {
|
|
||||||
const providerKey = normalizeProviderId(provider);
|
const providerKey = normalizeProviderId(provider);
|
||||||
return Object.entries(store.profiles)
|
return Object.entries(store.profiles)
|
||||||
.filter(([, cred]) => normalizeProviderId(cred.provider) === providerKey)
|
.filter(([, cred]) => normalizeProviderId(cred.provider) === providerKey)
|
||||||
|
|||||||
@@ -35,10 +35,9 @@ export function suggestOAuthProfileIdForLegacyDefault(params: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oauthProfiles = listProfilesForProvider(
|
const oauthProfiles = listProfilesForProvider(params.store, providerKey).filter(
|
||||||
params.store,
|
(id) => params.store.profiles[id]?.type === "oauth",
|
||||||
providerKey,
|
);
|
||||||
).filter((id) => params.store.profiles[id]?.type === "oauth");
|
|
||||||
if (oauthProfiles.length === 0) return null;
|
if (oauthProfiles.length === 0) return null;
|
||||||
|
|
||||||
const configuredEmail = legacyCfg?.email?.trim();
|
const configuredEmail = legacyCfg?.email?.trim();
|
||||||
@@ -47,16 +46,12 @@ export function suggestOAuthProfileIdForLegacyDefault(params: {
|
|||||||
const cred = params.store.profiles[id];
|
const cred = params.store.profiles[id];
|
||||||
if (!cred || cred.type !== "oauth") return false;
|
if (!cred || cred.type !== "oauth") return false;
|
||||||
const email = (cred.email as string | undefined)?.trim();
|
const email = (cred.email as string | undefined)?.trim();
|
||||||
return (
|
return email === configuredEmail || id === `${providerKey}:${configuredEmail}`;
|
||||||
email === configuredEmail || id === `${providerKey}:${configuredEmail}`
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
if (byEmail) return byEmail;
|
if (byEmail) return byEmail;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastGood =
|
const lastGood = params.store.lastGood?.[providerKey] ?? params.store.lastGood?.[params.provider];
|
||||||
params.store.lastGood?.[providerKey] ??
|
|
||||||
params.store.lastGood?.[params.provider];
|
|
||||||
if (lastGood && oauthProfiles.includes(lastGood)) return lastGood;
|
if (lastGood && oauthProfiles.includes(lastGood)) return lastGood;
|
||||||
|
|
||||||
const nonLegacy = oauthProfiles.filter((id) => id !== params.legacyProfileId);
|
const nonLegacy = oauthProfiles.filter((id) => id !== params.legacyProfileId);
|
||||||
@@ -83,10 +78,7 @@ export function repairOAuthProfileIdMismatch(params: {
|
|||||||
if (legacyCfg.mode !== "oauth") {
|
if (legacyCfg.mode !== "oauth") {
|
||||||
return { config: params.cfg, changes: [], migrated: false };
|
return { config: params.cfg, changes: [], migrated: false };
|
||||||
}
|
}
|
||||||
if (
|
if (normalizeProviderId(legacyCfg.provider) !== normalizeProviderId(params.provider)) {
|
||||||
normalizeProviderId(legacyCfg.provider) !==
|
|
||||||
normalizeProviderId(params.provider)
|
|
||||||
) {
|
|
||||||
return { config: params.cfg, changes: [], migrated: false };
|
return { config: params.cfg, changes: [], migrated: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,14 +94,10 @@ export function repairOAuthProfileIdMismatch(params: {
|
|||||||
|
|
||||||
const toCred = params.store.profiles[toProfileId];
|
const toCred = params.store.profiles[toProfileId];
|
||||||
const toEmail =
|
const toEmail =
|
||||||
toCred?.type === "oauth"
|
toCred?.type === "oauth" ? (toCred.email as string | undefined)?.trim() : undefined;
|
||||||
? (toCred.email as string | undefined)?.trim()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const nextProfiles = {
|
const nextProfiles = {
|
||||||
...(params.cfg.auth?.profiles as
|
...(params.cfg.auth?.profiles as Record<string, AuthProfileConfig> | undefined),
|
||||||
| Record<string, AuthProfileConfig>
|
|
||||||
| undefined),
|
|
||||||
} as Record<string, AuthProfileConfig>;
|
} as Record<string, AuthProfileConfig>;
|
||||||
delete nextProfiles[legacyProfileId];
|
delete nextProfiles[legacyProfileId];
|
||||||
nextProfiles[toProfileId] = {
|
nextProfiles[toProfileId] = {
|
||||||
@@ -121,17 +109,13 @@ export function repairOAuthProfileIdMismatch(params: {
|
|||||||
const nextOrder = (() => {
|
const nextOrder = (() => {
|
||||||
const order = params.cfg.auth?.order;
|
const order = params.cfg.auth?.order;
|
||||||
if (!order) return undefined;
|
if (!order) return undefined;
|
||||||
const resolvedKey = Object.keys(order).find(
|
const resolvedKey = Object.keys(order).find((key) => normalizeProviderId(key) === providerKey);
|
||||||
(key) => normalizeProviderId(key) === providerKey,
|
|
||||||
);
|
|
||||||
if (!resolvedKey) return order;
|
if (!resolvedKey) return order;
|
||||||
const existing = order[resolvedKey];
|
const existing = order[resolvedKey];
|
||||||
if (!Array.isArray(existing)) return order;
|
if (!Array.isArray(existing)) return order;
|
||||||
const replaced = existing
|
const replaced = existing
|
||||||
.map((id) => (id === legacyProfileId ? toProfileId : id))
|
.map((id) => (id === legacyProfileId ? toProfileId : id))
|
||||||
.filter(
|
.filter((id): id is string => typeof id === "string" && id.trim().length > 0);
|
||||||
(id): id is string => typeof id === "string" && id.trim().length > 0,
|
|
||||||
);
|
|
||||||
const deduped: string[] = [];
|
const deduped: string[] = [];
|
||||||
for (const entry of replaced) {
|
for (const entry of replaced) {
|
||||||
if (!deduped.includes(entry)) deduped.push(entry);
|
if (!deduped.includes(entry)) deduped.push(entry);
|
||||||
@@ -148,9 +132,7 @@ export function repairOAuthProfileIdMismatch(params: {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const changes = [
|
const changes = [`Auth: migrate ${legacyProfileId} → ${toProfileId} (OAuth profile id)`];
|
||||||
`Auth: migrate ${legacyProfileId} → ${toProfileId} (OAuth profile id)`,
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config: nextCfg,
|
config: nextCfg,
|
||||||
|
|||||||
@@ -3,29 +3,14 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
|||||||
import lockfile from "proper-lockfile";
|
import lockfile from "proper-lockfile";
|
||||||
import { resolveOAuthPath } from "../../config/paths.js";
|
import { resolveOAuthPath } from "../../config/paths.js";
|
||||||
import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js";
|
import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js";
|
||||||
import {
|
import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION, log } from "./constants.js";
|
||||||
AUTH_STORE_LOCK_OPTIONS,
|
|
||||||
AUTH_STORE_VERSION,
|
|
||||||
log,
|
|
||||||
} from "./constants.js";
|
|
||||||
import { syncExternalCliCredentials } from "./external-cli-sync.js";
|
import { syncExternalCliCredentials } from "./external-cli-sync.js";
|
||||||
import {
|
import { ensureAuthStoreFile, resolveAuthStorePath, resolveLegacyAuthStorePath } from "./paths.js";
|
||||||
ensureAuthStoreFile,
|
import type { AuthProfileCredential, AuthProfileStore, ProfileUsageStats } from "./types.js";
|
||||||
resolveAuthStorePath,
|
|
||||||
resolveLegacyAuthStorePath,
|
|
||||||
} from "./paths.js";
|
|
||||||
import type {
|
|
||||||
AuthProfileCredential,
|
|
||||||
AuthProfileStore,
|
|
||||||
ProfileUsageStats,
|
|
||||||
} from "./types.js";
|
|
||||||
|
|
||||||
type LegacyAuthStore = Record<string, AuthProfileCredential>;
|
type LegacyAuthStore = Record<string, AuthProfileCredential>;
|
||||||
|
|
||||||
function _syncAuthProfileStore(
|
function _syncAuthProfileStore(target: AuthProfileStore, source: AuthProfileStore): void {
|
||||||
target: AuthProfileStore,
|
|
||||||
source: AuthProfileStore,
|
|
||||||
): void {
|
|
||||||
target.version = source.version;
|
target.version = source.version;
|
||||||
target.profiles = source.profiles;
|
target.profiles = source.profiles;
|
||||||
target.order = source.order;
|
target.order = source.order;
|
||||||
@@ -70,11 +55,7 @@ function coerceLegacyStore(raw: unknown): LegacyAuthStore | null {
|
|||||||
for (const [key, value] of Object.entries(record)) {
|
for (const [key, value] of Object.entries(record)) {
|
||||||
if (!value || typeof value !== "object") continue;
|
if (!value || typeof value !== "object") continue;
|
||||||
const typed = value as Partial<AuthProfileCredential>;
|
const typed = value as Partial<AuthProfileCredential>;
|
||||||
if (
|
if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") {
|
||||||
typed.type !== "api_key" &&
|
|
||||||
typed.type !== "oauth" &&
|
|
||||||
typed.type !== "token"
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
entries[key] = {
|
entries[key] = {
|
||||||
@@ -94,11 +75,7 @@ function coerceAuthStore(raw: unknown): AuthProfileStore | null {
|
|||||||
for (const [key, value] of Object.entries(profiles)) {
|
for (const [key, value] of Object.entries(profiles)) {
|
||||||
if (!value || typeof value !== "object") continue;
|
if (!value || typeof value !== "object") continue;
|
||||||
const typed = value as Partial<AuthProfileCredential>;
|
const typed = value as Partial<AuthProfileCredential>;
|
||||||
if (
|
if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") {
|
||||||
typed.type !== "api_key" &&
|
|
||||||
typed.type !== "oauth" &&
|
|
||||||
typed.type !== "token"
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!typed.provider) continue;
|
if (!typed.provider) continue;
|
||||||
@@ -188,9 +165,7 @@ export function loadAuthProfileStore(): AuthProfileStore {
|
|||||||
type: "token",
|
type: "token",
|
||||||
provider: String(cred.provider ?? provider),
|
provider: String(cred.provider ?? provider),
|
||||||
token: cred.token,
|
token: cred.token,
|
||||||
...(typeof cred.expires === "number"
|
...(typeof cred.expires === "number" ? { expires: cred.expires } : {}),
|
||||||
? { expires: cred.expires }
|
|
||||||
: {}),
|
|
||||||
...(cred.email ? { email: cred.email } : {}),
|
...(cred.email ? { email: cred.email } : {}),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -253,9 +228,7 @@ export function ensureAuthProfileStore(
|
|||||||
type: "token",
|
type: "token",
|
||||||
provider: String(cred.provider ?? provider),
|
provider: String(cred.provider ?? provider),
|
||||||
token: cred.token,
|
token: cred.token,
|
||||||
...(typeof cred.expires === "number"
|
...(typeof cred.expires === "number" ? { expires: cred.expires } : {}),
|
||||||
? { expires: cred.expires }
|
|
||||||
: {}),
|
|
||||||
...(cred.email ? { email: cred.email } : {}),
|
...(cred.email ? { email: cred.email } : {}),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -301,10 +274,7 @@ export function ensureAuthProfileStore(
|
|||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveAuthProfileStore(
|
export function saveAuthProfileStore(store: AuthProfileStore, agentDir?: string): void {
|
||||||
store: AuthProfileStore,
|
|
||||||
agentDir?: string,
|
|
||||||
): void {
|
|
||||||
const authPath = resolveAuthStorePath(agentDir);
|
const authPath = resolveAuthStorePath(agentDir);
|
||||||
const payload = {
|
const payload = {
|
||||||
version: AUTH_STORE_VERSION,
|
version: AUTH_STORE_VERSION,
|
||||||
|
|||||||
@@ -29,10 +29,7 @@ export type OAuthCredential = OAuthCredentials & {
|
|||||||
email?: string;
|
email?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AuthProfileCredential =
|
export type AuthProfileCredential = ApiKeyCredential | TokenCredential | OAuthCredential;
|
||||||
| ApiKeyCredential
|
|
||||||
| TokenCredential
|
|
||||||
| OAuthCredential;
|
|
||||||
|
|
||||||
export type AuthProfileFailureReason =
|
export type AuthProfileFailureReason =
|
||||||
| "auth"
|
| "auth"
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { normalizeProviderId } from "../model-selection.js";
|
import { normalizeProviderId } from "../model-selection.js";
|
||||||
import {
|
import { saveAuthProfileStore, updateAuthProfileStoreWithLock } from "./store.js";
|
||||||
saveAuthProfileStore,
|
import type { AuthProfileFailureReason, AuthProfileStore, ProfileUsageStats } from "./types.js";
|
||||||
updateAuthProfileStoreWithLock,
|
|
||||||
} from "./store.js";
|
|
||||||
import type {
|
|
||||||
AuthProfileFailureReason,
|
|
||||||
AuthProfileStore,
|
|
||||||
ProfileUsageStats,
|
|
||||||
} from "./types.js";
|
|
||||||
|
|
||||||
function resolveProfileUnusableUntil(stats: ProfileUsageStats): number | null {
|
function resolveProfileUnusableUntil(stats: ProfileUsageStats): number | null {
|
||||||
const values = [stats.cooldownUntil, stats.disabledUntil]
|
const values = [stats.cooldownUntil, stats.disabledUntil]
|
||||||
@@ -21,10 +14,7 @@ function resolveProfileUnusableUntil(stats: ProfileUsageStats): number | null {
|
|||||||
/**
|
/**
|
||||||
* Check if a profile is currently in cooldown (due to rate limiting or errors).
|
* Check if a profile is currently in cooldown (due to rate limiting or errors).
|
||||||
*/
|
*/
|
||||||
export function isProfileInCooldown(
|
export function isProfileInCooldown(store: AuthProfileStore, profileId: string): boolean {
|
||||||
store: AuthProfileStore,
|
|
||||||
profileId: string,
|
|
||||||
): boolean {
|
|
||||||
const stats = store.usageStats?.[profileId];
|
const stats = store.usageStats?.[profileId];
|
||||||
if (!stats) return false;
|
if (!stats) return false;
|
||||||
const unusableUntil = resolveProfileUnusableUntil(stats);
|
const unusableUntil = resolveProfileUnusableUntil(stats);
|
||||||
@@ -102,9 +92,7 @@ function resolveAuthCooldownConfig(params: {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const resolveHours = (value: unknown, fallback: number) =>
|
const resolveHours = (value: unknown, fallback: number) =>
|
||||||
typeof value === "number" && Number.isFinite(value) && value > 0
|
typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
||||||
? value
|
|
||||||
: fallback;
|
|
||||||
|
|
||||||
const cooldowns = params.cfg?.auth?.cooldowns;
|
const cooldowns = params.cfg?.auth?.cooldowns;
|
||||||
const billingOverride = (() => {
|
const billingOverride = (() => {
|
||||||
@@ -120,10 +108,7 @@ function resolveAuthCooldownConfig(params: {
|
|||||||
billingOverride ?? cooldowns?.billingBackoffHours,
|
billingOverride ?? cooldowns?.billingBackoffHours,
|
||||||
defaults.billingBackoffHours,
|
defaults.billingBackoffHours,
|
||||||
);
|
);
|
||||||
const billingMaxHours = resolveHours(
|
const billingMaxHours = resolveHours(cooldowns?.billingMaxHours, defaults.billingMaxHours);
|
||||||
cooldowns?.billingMaxHours,
|
|
||||||
defaults.billingMaxHours,
|
|
||||||
);
|
|
||||||
const failureWindowHours = resolveHours(
|
const failureWindowHours = resolveHours(
|
||||||
cooldowns?.failureWindowHours,
|
cooldowns?.failureWindowHours,
|
||||||
defaults.failureWindowHours,
|
defaults.failureWindowHours,
|
||||||
@@ -172,9 +157,7 @@ function computeNextProfileUsageStats(params: {
|
|||||||
|
|
||||||
const baseErrorCount = windowExpired ? 0 : (params.existing.errorCount ?? 0);
|
const baseErrorCount = windowExpired ? 0 : (params.existing.errorCount ?? 0);
|
||||||
const nextErrorCount = baseErrorCount + 1;
|
const nextErrorCount = baseErrorCount + 1;
|
||||||
const failureCounts = windowExpired
|
const failureCounts = windowExpired ? {} : { ...params.existing.failureCounts };
|
||||||
? {}
|
|
||||||
: { ...params.existing.failureCounts };
|
|
||||||
failureCounts[params.reason] = (failureCounts[params.reason] ?? 0) + 1;
|
failureCounts[params.reason] = (failureCounts[params.reason] ?? 0) + 1;
|
||||||
|
|
||||||
const updatedStats: ProfileUsageStats = {
|
const updatedStats: ProfileUsageStats = {
|
||||||
@@ -246,9 +229,7 @@ export async function markAuthProfileFailure(params: {
|
|||||||
store.usageStats = store.usageStats ?? {};
|
store.usageStats = store.usageStats ?? {};
|
||||||
const existing = store.usageStats[profileId] ?? {};
|
const existing = store.usageStats[profileId] ?? {};
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const providerKey = normalizeProviderId(
|
const providerKey = normalizeProviderId(store.profiles[profileId]?.provider ?? "");
|
||||||
store.profiles[profileId]?.provider ?? "",
|
|
||||||
);
|
|
||||||
const cfgResolved = resolveAuthCooldownConfig({
|
const cfgResolved = resolveAuthCooldownConfig({
|
||||||
cfg,
|
cfg,
|
||||||
providerId: providerKey,
|
providerId: providerKey,
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ function clampTtl(value: number | undefined) {
|
|||||||
return Math.min(Math.max(value, MIN_JOB_TTL_MS), MAX_JOB_TTL_MS);
|
return Math.min(Math.max(value, MIN_JOB_TTL_MS), MAX_JOB_TTL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
let jobTtlMs = clampTtl(
|
let jobTtlMs = clampTtl(Number.parseInt(process.env.PI_BASH_JOB_TTL_MS ?? "", 10));
|
||||||
Number.parseInt(process.env.PI_BASH_JOB_TTL_MS ?? "", 10),
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ProcessStatus = "running" | "completed" | "failed" | "killed";
|
export type ProcessStatus = "running" | "completed" | "failed" | "killed";
|
||||||
|
|
||||||
@@ -75,24 +73,15 @@ export function deleteSession(id: string) {
|
|||||||
finishedSessions.delete(id);
|
finishedSessions.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appendOutput(
|
export function appendOutput(session: ProcessSession, stream: "stdout" | "stderr", chunk: string) {
|
||||||
session: ProcessSession,
|
|
||||||
stream: "stdout" | "stderr",
|
|
||||||
chunk: string,
|
|
||||||
) {
|
|
||||||
session.pendingStdout ??= [];
|
session.pendingStdout ??= [];
|
||||||
session.pendingStderr ??= [];
|
session.pendingStderr ??= [];
|
||||||
const buffer =
|
const buffer = stream === "stdout" ? session.pendingStdout : session.pendingStderr;
|
||||||
stream === "stdout" ? session.pendingStdout : session.pendingStderr;
|
|
||||||
buffer.push(chunk);
|
buffer.push(chunk);
|
||||||
session.totalOutputChars += chunk.length;
|
session.totalOutputChars += chunk.length;
|
||||||
const aggregated = trimWithCap(
|
const aggregated = trimWithCap(session.aggregated + chunk, session.maxOutputChars);
|
||||||
session.aggregated + chunk,
|
|
||||||
session.maxOutputChars,
|
|
||||||
);
|
|
||||||
session.truncated =
|
session.truncated =
|
||||||
session.truncated ||
|
session.truncated || aggregated.length < session.aggregated.length + chunk.length;
|
||||||
aggregated.length < session.aggregated.length + chunk.length;
|
|
||||||
session.aggregated = aggregated;
|
session.aggregated = aggregated;
|
||||||
session.tail = tail(session.aggregated, 2000);
|
session.tail = tail(session.aggregated, 2000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,7 @@ import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
|||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
|
|
||||||
import { logInfo } from "../logger.js";
|
import { logInfo } from "../logger.js";
|
||||||
import {
|
import { addSession, appendOutput, markBackgrounded, markExited } from "./bash-process-registry.js";
|
||||||
addSession,
|
|
||||||
appendOutput,
|
|
||||||
markBackgrounded,
|
|
||||||
markExited,
|
|
||||||
} from "./bash-process-registry.js";
|
|
||||||
import type { BashSandboxConfig } from "./bash-tools.shared.js";
|
import type { BashSandboxConfig } from "./bash-tools.shared.js";
|
||||||
import {
|
import {
|
||||||
buildDockerExecArgs,
|
buildDockerExecArgs,
|
||||||
@@ -32,8 +27,7 @@ const DEFAULT_MAX_OUTPUT = clampNumber(
|
|||||||
150_000,
|
150_000,
|
||||||
);
|
);
|
||||||
const DEFAULT_PATH =
|
const DEFAULT_PATH =
|
||||||
process.env.PATH ??
|
process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
||||||
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
||||||
|
|
||||||
export type ExecToolDefaults = {
|
export type ExecToolDefaults = {
|
||||||
backgroundMs?: number;
|
backgroundMs?: number;
|
||||||
@@ -55,18 +49,14 @@ export type ExecElevatedDefaults = {
|
|||||||
|
|
||||||
const execSchema = Type.Object({
|
const execSchema = Type.Object({
|
||||||
command: Type.String({ description: "Shell command to execute" }),
|
command: Type.String({ description: "Shell command to execute" }),
|
||||||
workdir: Type.Optional(
|
workdir: Type.Optional(Type.String({ description: "Working directory (defaults to cwd)" })),
|
||||||
Type.String({ description: "Working directory (defaults to cwd)" }),
|
|
||||||
),
|
|
||||||
env: Type.Optional(Type.Record(Type.String(), Type.String())),
|
env: Type.Optional(Type.Record(Type.String(), Type.String())),
|
||||||
yieldMs: Type.Optional(
|
yieldMs: Type.Optional(
|
||||||
Type.Number({
|
Type.Number({
|
||||||
description: "Milliseconds to wait before backgrounding (default 10000)",
|
description: "Milliseconds to wait before backgrounding (default 10000)",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
background: Type.Optional(
|
background: Type.Optional(Type.Boolean({ description: "Run in background immediately" })),
|
||||||
Type.Boolean({ description: "Run in background immediately" }),
|
|
||||||
),
|
|
||||||
timeout: Type.Optional(
|
timeout: Type.Optional(
|
||||||
Type.Number({
|
Type.Number({
|
||||||
description: "Timeout in seconds (optional, kills process on expiry)",
|
description: "Timeout in seconds (optional, kills process on expiry)",
|
||||||
@@ -140,19 +130,12 @@ export function createExecTool(
|
|||||||
const backgroundRequested = params.background === true;
|
const backgroundRequested = params.background === true;
|
||||||
const yieldRequested = typeof params.yieldMs === "number";
|
const yieldRequested = typeof params.yieldMs === "number";
|
||||||
if (!allowBackground && (backgroundRequested || yieldRequested)) {
|
if (!allowBackground && (backgroundRequested || yieldRequested)) {
|
||||||
warnings.push(
|
warnings.push("Warning: background execution is disabled; running synchronously.");
|
||||||
"Warning: background execution is disabled; running synchronously.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const yieldWindow = allowBackground
|
const yieldWindow = allowBackground
|
||||||
? backgroundRequested
|
? backgroundRequested
|
||||||
? 0
|
? 0
|
||||||
: clampNumber(
|
: clampNumber(params.yieldMs ?? defaultBackgroundMs, defaultBackgroundMs, 10, 120_000)
|
||||||
params.yieldMs ?? defaultBackgroundMs,
|
|
||||||
defaultBackgroundMs,
|
|
||||||
10,
|
|
||||||
120_000,
|
|
||||||
)
|
|
||||||
: null;
|
: null;
|
||||||
const elevatedDefaults = defaults?.elevated;
|
const elevatedDefaults = defaults?.elevated;
|
||||||
const elevatedDefaultOn =
|
const elevatedDefaultOn =
|
||||||
@@ -160,17 +143,13 @@ export function createExecTool(
|
|||||||
elevatedDefaults.enabled &&
|
elevatedDefaults.enabled &&
|
||||||
elevatedDefaults.allowed;
|
elevatedDefaults.allowed;
|
||||||
const elevatedRequested =
|
const elevatedRequested =
|
||||||
typeof params.elevated === "boolean"
|
typeof params.elevated === "boolean" ? params.elevated : elevatedDefaultOn;
|
||||||
? params.elevated
|
|
||||||
: elevatedDefaultOn;
|
|
||||||
if (elevatedRequested) {
|
if (elevatedRequested) {
|
||||||
if (!elevatedDefaults?.enabled || !elevatedDefaults.allowed) {
|
if (!elevatedDefaults?.enabled || !elevatedDefaults.allowed) {
|
||||||
const runtime = defaults?.sandbox ? "sandboxed" : "direct";
|
const runtime = defaults?.sandbox ? "sandboxed" : "direct";
|
||||||
const gates: string[] = [];
|
const gates: string[] = [];
|
||||||
if (!elevatedDefaults?.enabled) {
|
if (!elevatedDefaults?.enabled) {
|
||||||
gates.push(
|
gates.push("enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled)");
|
||||||
"enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled)",
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
gates.push(
|
gates.push(
|
||||||
"allowFrom (tools.elevated.allowFrom.<provider> / agents.list[].tools.elevated.allowFrom.<provider>)",
|
"allowFrom (tools.elevated.allowFrom.<provider> / agents.list[].tools.elevated.allowFrom.<provider>)",
|
||||||
@@ -197,8 +176,7 @@ export function createExecTool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sandbox = elevatedRequested ? undefined : defaults?.sandbox;
|
const sandbox = elevatedRequested ? undefined : defaults?.sandbox;
|
||||||
const rawWorkdir =
|
const rawWorkdir = params.workdir?.trim() || defaults?.cwd || process.cwd();
|
||||||
params.workdir?.trim() || defaults?.cwd || process.cwd();
|
|
||||||
let workdir = rawWorkdir;
|
let workdir = rawWorkdir;
|
||||||
let containerWorkdir = sandbox?.containerWorkdir;
|
let containerWorkdir = sandbox?.containerWorkdir;
|
||||||
if (sandbox) {
|
if (sandbox) {
|
||||||
@@ -335,121 +313,111 @@ export function createExecTool(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise<AgentToolResult<ExecToolDetails>>(
|
return new Promise<AgentToolResult<ExecToolDetails>>((resolve, reject) => {
|
||||||
(resolve, reject) => {
|
const resolveRunning = () => {
|
||||||
const resolveRunning = () => {
|
settle(() =>
|
||||||
settle(() =>
|
resolve({
|
||||||
resolve({
|
content: [
|
||||||
content: [
|
{
|
||||||
{
|
type: "text",
|
||||||
type: "text",
|
text:
|
||||||
text:
|
`${warnings.length ? `${warnings.join("\n")}\n\n` : ""}` +
|
||||||
`${warnings.length ? `${warnings.join("\n")}\n\n` : ""}` +
|
`Command still running (session ${sessionId}, pid ${session.pid ?? "n/a"}). ` +
|
||||||
`Command still running (session ${sessionId}, pid ${session.pid ?? "n/a"}). ` +
|
"Use process (list/poll/log/write/kill/clear/remove) for follow-up.",
|
||||||
"Use process (list/poll/log/write/kill/clear/remove) for follow-up.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
details: {
|
|
||||||
status: "running",
|
|
||||||
sessionId,
|
|
||||||
pid: session.pid ?? undefined,
|
|
||||||
startedAt,
|
|
||||||
cwd: session.cwd,
|
|
||||||
tail: session.tail,
|
|
||||||
},
|
},
|
||||||
}),
|
],
|
||||||
);
|
details: {
|
||||||
};
|
status: "running",
|
||||||
|
sessionId,
|
||||||
|
pid: session.pid ?? undefined,
|
||||||
|
startedAt,
|
||||||
|
cwd: session.cwd,
|
||||||
|
tail: session.tail,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const onYieldNow = () => {
|
const onYieldNow = () => {
|
||||||
if (yieldTimer) clearTimeout(yieldTimer);
|
if (yieldTimer) clearTimeout(yieldTimer);
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
yielded = true;
|
yielded = true;
|
||||||
markBackgrounded(session);
|
markBackgrounded(session);
|
||||||
resolveRunning();
|
resolveRunning();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (allowBackground && yieldWindow !== null) {
|
if (allowBackground && yieldWindow !== null) {
|
||||||
if (yieldWindow === 0) {
|
if (yieldWindow === 0) {
|
||||||
onYieldNow();
|
onYieldNow();
|
||||||
} else {
|
} else {
|
||||||
yieldTimer = setTimeout(() => {
|
yieldTimer = setTimeout(() => {
|
||||||
if (settled) return;
|
if (settled) return;
|
||||||
yielded = true;
|
yielded = true;
|
||||||
markBackgrounded(session);
|
markBackgrounded(session);
|
||||||
resolveRunning();
|
resolveRunning();
|
||||||
}, yieldWindow);
|
}, yieldWindow);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExit = (code: number | null, exitSignal: NodeJS.Signals | number | null) => {
|
||||||
|
if (yieldTimer) clearTimeout(yieldTimer);
|
||||||
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
||||||
|
const durationMs = Date.now() - startedAt;
|
||||||
|
const wasSignal = exitSignal != null;
|
||||||
|
const isSuccess = code === 0 && !wasSignal && !signal?.aborted && !timedOut;
|
||||||
|
const status: "completed" | "failed" = isSuccess ? "completed" : "failed";
|
||||||
|
markExited(session, code, exitSignal, status);
|
||||||
|
|
||||||
|
if (yielded || session.backgrounded) return;
|
||||||
|
|
||||||
|
const aggregated = session.aggregated.trim();
|
||||||
|
if (!isSuccess) {
|
||||||
|
const reason = timedOut
|
||||||
|
? `Command timed out after ${effectiveTimeout} seconds`
|
||||||
|
: wasSignal && exitSignal
|
||||||
|
? `Command aborted by signal ${exitSignal}`
|
||||||
|
: code === null
|
||||||
|
? "Command aborted before exit code was captured"
|
||||||
|
: `Command exited with code ${code}`;
|
||||||
|
const message = aggregated ? `${aggregated}\n\n${reason}` : reason;
|
||||||
|
settle(() => reject(new Error(message)));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExit = (
|
settle(() =>
|
||||||
code: number | null,
|
resolve({
|
||||||
exitSignal: NodeJS.Signals | number | null,
|
content: [
|
||||||
) => {
|
{
|
||||||
if (yieldTimer) clearTimeout(yieldTimer);
|
type: "text",
|
||||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
text:
|
||||||
const durationMs = Date.now() - startedAt;
|
`${warnings.length ? `${warnings.join("\n")}\n\n` : ""}` +
|
||||||
const wasSignal = exitSignal != null;
|
(aggregated || "(no output)"),
|
||||||
const isSuccess =
|
|
||||||
code === 0 && !wasSignal && !signal?.aborted && !timedOut;
|
|
||||||
const status: "completed" | "failed" = isSuccess
|
|
||||||
? "completed"
|
|
||||||
: "failed";
|
|
||||||
markExited(session, code, exitSignal, status);
|
|
||||||
|
|
||||||
if (yielded || session.backgrounded) return;
|
|
||||||
|
|
||||||
const aggregated = session.aggregated.trim();
|
|
||||||
if (!isSuccess) {
|
|
||||||
const reason = timedOut
|
|
||||||
? `Command timed out after ${effectiveTimeout} seconds`
|
|
||||||
: wasSignal && exitSignal
|
|
||||||
? `Command aborted by signal ${exitSignal}`
|
|
||||||
: code === null
|
|
||||||
? "Command aborted before exit code was captured"
|
|
||||||
: `Command exited with code ${code}`;
|
|
||||||
const message = aggregated
|
|
||||||
? `${aggregated}\n\n${reason}`
|
|
||||||
: reason;
|
|
||||||
settle(() => reject(new Error(message)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
settle(() =>
|
|
||||||
resolve({
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text:
|
|
||||||
`${warnings.length ? `${warnings.join("\n")}\n\n` : ""}` +
|
|
||||||
(aggregated || "(no output)"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
details: {
|
|
||||||
status: "completed",
|
|
||||||
exitCode: code ?? 0,
|
|
||||||
durationMs,
|
|
||||||
aggregated,
|
|
||||||
cwd: session.cwd,
|
|
||||||
},
|
},
|
||||||
}),
|
],
|
||||||
);
|
details: {
|
||||||
};
|
status: "completed",
|
||||||
|
exitCode: code ?? 0,
|
||||||
|
durationMs,
|
||||||
|
aggregated,
|
||||||
|
cwd: session.cwd,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// `exit` can fire before stdio fully flushes (notably on Windows).
|
// `exit` can fire before stdio fully flushes (notably on Windows).
|
||||||
// `close` waits for streams to close, so aggregated output is complete.
|
// `close` waits for streams to close, so aggregated output is complete.
|
||||||
child.once("close", (code, exitSignal) => {
|
child.once("close", (code, exitSignal) => {
|
||||||
handleExit(code, exitSignal);
|
handleExit(code, exitSignal);
|
||||||
});
|
});
|
||||||
|
|
||||||
child.once("error", (err) => {
|
child.once("error", (err) => {
|
||||||
if (yieldTimer) clearTimeout(yieldTimer);
|
if (yieldTimer) clearTimeout(yieldTimer);
|
||||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
||||||
markExited(session, null, null, "failed");
|
markExited(session, null, null, "failed");
|
||||||
settle(() => reject(err));
|
settle(() => reject(err));
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ export type ProcessToolDefaults = {
|
|||||||
|
|
||||||
const processSchema = Type.Object({
|
const processSchema = Type.Object({
|
||||||
action: Type.String({ description: "Process action" }),
|
action: Type.String({ description: "Process action" }),
|
||||||
sessionId: Type.Optional(
|
sessionId: Type.Optional(Type.String({ description: "Session id for actions other than list" })),
|
||||||
Type.String({ description: "Session id for actions other than list" }),
|
|
||||||
),
|
|
||||||
data: Type.Optional(Type.String({ description: "Data to write for write" })),
|
data: Type.Optional(Type.String({ description: "Data to write for write" })),
|
||||||
eof: Type.Optional(Type.Boolean({ description: "Close stdin after write" })),
|
eof: Type.Optional(Type.Boolean({ description: "Close stdin after write" })),
|
||||||
offset: Type.Optional(Type.Number({ description: "Log offset" })),
|
offset: Type.Optional(Type.Number({ description: "Log offset" })),
|
||||||
@@ -96,9 +94,7 @@ export function createProcessTool(
|
|||||||
const lines = [...running, ...finished]
|
const lines = [...running, ...finished]
|
||||||
.sort((a, b) => b.startedAt - a.startedAt)
|
.sort((a, b) => b.startedAt - a.startedAt)
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
const label = s.name
|
const label = s.name ? truncateMiddle(s.name, 80) : truncateMiddle(s.command, 120);
|
||||||
? truncateMiddle(s.name, 80)
|
|
||||||
: truncateMiddle(s.command, 120);
|
|
||||||
return `${s.sessionId.slice(0, 8)} ${pad(
|
return `${s.sessionId.slice(0, 8)} ${pad(
|
||||||
s.status,
|
s.status,
|
||||||
9,
|
9,
|
||||||
@@ -117,9 +113,7 @@ export function createProcessTool(
|
|||||||
|
|
||||||
if (!params.sessionId) {
|
if (!params.sessionId) {
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [{ type: "text", text: "sessionId is required for this action." }],
|
||||||
{ type: "text", text: "sessionId is required for this action." },
|
|
||||||
],
|
|
||||||
details: { status: "failed" },
|
details: { status: "failed" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -150,10 +144,7 @@ export function createProcessTool(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
details: {
|
details: {
|
||||||
status:
|
status: scopedFinished.status === "completed" ? "completed" : "failed",
|
||||||
scopedFinished.status === "completed"
|
|
||||||
? "completed"
|
|
||||||
: "failed",
|
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
exitCode: scopedFinished.exitCode ?? undefined,
|
exitCode: scopedFinished.exitCode ?? undefined,
|
||||||
aggregated: scopedFinished.aggregated,
|
aggregated: scopedFinished.aggregated,
|
||||||
@@ -187,8 +178,7 @@ export function createProcessTool(
|
|||||||
const exitCode = scopedSession.exitCode ?? 0;
|
const exitCode = scopedSession.exitCode ?? 0;
|
||||||
const exitSignal = scopedSession.exitSignal ?? undefined;
|
const exitSignal = scopedSession.exitSignal ?? undefined;
|
||||||
if (exited) {
|
if (exited) {
|
||||||
const status =
|
const status = exitCode === 0 && exitSignal == null ? "completed" : "failed";
|
||||||
exitCode === 0 && exitSignal == null ? "completed" : "failed";
|
|
||||||
markExited(
|
markExited(
|
||||||
scopedSession,
|
scopedSession,
|
||||||
scopedSession.exitCode ?? null,
|
scopedSession.exitCode ?? null,
|
||||||
@@ -201,10 +191,7 @@ export function createProcessTool(
|
|||||||
? "completed"
|
? "completed"
|
||||||
: "failed"
|
: "failed"
|
||||||
: "running";
|
: "running";
|
||||||
const output = [stdout.trimEnd(), stderr.trimEnd()]
|
const output = [stdout.trimEnd(), stderr.trimEnd()].filter(Boolean).join("\n").trim();
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n")
|
|
||||||
.trim();
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -265,12 +252,9 @@ export function createProcessTool(
|
|||||||
params.offset,
|
params.offset,
|
||||||
params.limit,
|
params.limit,
|
||||||
);
|
);
|
||||||
const status =
|
const status = scopedFinished.status === "completed" ? "completed" : "failed";
|
||||||
scopedFinished.status === "completed" ? "completed" : "failed";
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [{ type: "text", text: slice || "(no output recorded)" }],
|
||||||
{ type: "text", text: slice || "(no output recorded)" },
|
|
||||||
],
|
|
||||||
details: {
|
details: {
|
||||||
status,
|
status,
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
@@ -318,10 +302,7 @@ export function createProcessTool(
|
|||||||
details: { status: "failed" },
|
details: { status: "failed" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (
|
if (!scopedSession.child?.stdin || scopedSession.child.stdin.destroyed) {
|
||||||
!scopedSession.child?.stdin ||
|
|
||||||
scopedSession.child.stdin.destroyed
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -353,9 +334,7 @@ export function createProcessTool(
|
|||||||
details: {
|
details: {
|
||||||
status: "running",
|
status: "running",
|
||||||
sessionId: params.sessionId,
|
sessionId: params.sessionId,
|
||||||
name: scopedSession
|
name: scopedSession ? deriveSessionName(scopedSession.command) : undefined,
|
||||||
? deriveSessionName(scopedSession.command)
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -386,14 +365,10 @@ export function createProcessTool(
|
|||||||
killSession(scopedSession);
|
killSession(scopedSession);
|
||||||
markExited(scopedSession, null, "SIGKILL", "failed");
|
markExited(scopedSession, null, "SIGKILL", "failed");
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [{ type: "text", text: `Killed session ${params.sessionId}.` }],
|
||||||
{ type: "text", text: `Killed session ${params.sessionId}.` },
|
|
||||||
],
|
|
||||||
details: {
|
details: {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
name: scopedSession
|
name: scopedSession ? deriveSessionName(scopedSession.command) : undefined,
|
||||||
? deriveSessionName(scopedSession.command)
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -402,9 +377,7 @@ export function createProcessTool(
|
|||||||
if (scopedFinished) {
|
if (scopedFinished) {
|
||||||
deleteSession(params.sessionId);
|
deleteSession(params.sessionId);
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [{ type: "text", text: `Cleared session ${params.sessionId}.` }],
|
||||||
{ type: "text", text: `Cleared session ${params.sessionId}.` },
|
|
||||||
],
|
|
||||||
details: { status: "completed" },
|
details: { status: "completed" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -424,23 +397,17 @@ export function createProcessTool(
|
|||||||
killSession(scopedSession);
|
killSession(scopedSession);
|
||||||
markExited(scopedSession, null, "SIGKILL", "failed");
|
markExited(scopedSession, null, "SIGKILL", "failed");
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [{ type: "text", text: `Removed session ${params.sessionId}.` }],
|
||||||
{ type: "text", text: `Removed session ${params.sessionId}.` },
|
|
||||||
],
|
|
||||||
details: {
|
details: {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
name: scopedSession
|
name: scopedSession ? deriveSessionName(scopedSession.command) : undefined,
|
||||||
? deriveSessionName(scopedSession.command)
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (scopedFinished) {
|
if (scopedFinished) {
|
||||||
deleteSession(params.sessionId);
|
deleteSession(params.sessionId);
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [{ type: "text", text: `Removed session ${params.sessionId}.` }],
|
||||||
{ type: "text", text: `Removed session ${params.sessionId}.` },
|
|
||||||
],
|
|
||||||
details: { status: "completed" },
|
details: { status: "completed" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -457,9 +424,7 @@ export function createProcessTool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [{ type: "text", text: `Unknown action ${params.action as string}` }],
|
||||||
{ type: "text", text: `Unknown action ${params.action as string}` },
|
|
||||||
],
|
|
||||||
details: { status: "failed" },
|
details: { status: "failed" },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -98,10 +98,7 @@ export async function resolveSandboxWorkdir(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function killSession(session: {
|
export function killSession(session: { pid?: number; child?: ChildProcessWithoutNullStreams }) {
|
||||||
pid?: number;
|
|
||||||
child?: ChildProcessWithoutNullStreams;
|
|
||||||
}) {
|
|
||||||
const pid = session.pid ?? session.child?.pid;
|
const pid = session.pid ?? session.child?.pid;
|
||||||
if (pid) {
|
if (pid) {
|
||||||
killProcessTree(pid);
|
killProcessTree(pid);
|
||||||
@@ -117,9 +114,7 @@ export function resolveWorkdir(workdir: string, warnings: string[]) {
|
|||||||
} catch {
|
} catch {
|
||||||
// ignore, fallback below
|
// ignore, fallback below
|
||||||
}
|
}
|
||||||
warnings.push(
|
warnings.push(`Warning: workdir "${workdir}" is unavailable; using "${fallback}".`);
|
||||||
`Warning: workdir "${workdir}" is unavailable; using "${fallback}".`,
|
|
||||||
);
|
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,9 +172,7 @@ export function sliceLogLines(
|
|||||||
const totalLines = lines.length;
|
const totalLines = lines.length;
|
||||||
const totalChars = text.length;
|
const totalChars = text.length;
|
||||||
let start =
|
let start =
|
||||||
typeof offset === "number" && Number.isFinite(offset)
|
typeof offset === "number" && Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
|
||||||
? Math.max(0, Math.floor(offset))
|
|
||||||
: 0;
|
|
||||||
if (limit !== undefined && offset === undefined) {
|
if (limit !== undefined && offset === undefined) {
|
||||||
const tailCount = Math.max(0, Math.floor(limit));
|
const tailCount = Math.max(0, Math.floor(limit));
|
||||||
start = Math.max(totalLines - tailCount, 0);
|
start = Math.max(totalLines - tailCount, 0);
|
||||||
@@ -203,8 +196,7 @@ export function deriveSessionName(command: string): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tokenizeCommand(command: string): string[] {
|
function tokenizeCommand(command: string): string[] {
|
||||||
const matches =
|
const matches = command.match(/(?:[^\s"']+|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+/g) ?? [];
|
||||||
command.match(/(?:[^\s"']+|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+/g) ?? [];
|
|
||||||
return matches.map((token) => stripQuotes(token)).filter(Boolean);
|
return matches.map((token) => stripQuotes(token)).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { resetProcessRegistryForTests } from "./bash-process-registry.js";
|
import { resetProcessRegistryForTests } from "./bash-process-registry.js";
|
||||||
import {
|
import { createExecTool, createProcessTool, execTool, processTool } from "./bash-tools.js";
|
||||||
createExecTool,
|
|
||||||
createProcessTool,
|
|
||||||
execTool,
|
|
||||||
processTool,
|
|
||||||
} from "./bash-tools.js";
|
|
||||||
import { sanitizeBinaryOutput } from "./shell-utils.js";
|
import { sanitizeBinaryOutput } from "./shell-utils.js";
|
||||||
|
|
||||||
const isWin = process.platform === "win32";
|
const isWin = process.platform === "win32";
|
||||||
@@ -15,10 +10,8 @@ const yieldDelayCmd = isWin ? "Start-Sleep -Milliseconds 200" : "sleep 0.2";
|
|||||||
const longDelayCmd = isWin ? "Start-Sleep -Seconds 2" : "sleep 2";
|
const longDelayCmd = isWin ? "Start-Sleep -Seconds 2" : "sleep 2";
|
||||||
// Both PowerShell and bash use ; for command separation
|
// Both PowerShell and bash use ; for command separation
|
||||||
const joinCommands = (commands: string[]) => commands.join("; ");
|
const joinCommands = (commands: string[]) => commands.join("; ");
|
||||||
const echoAfterDelay = (message: string) =>
|
const echoAfterDelay = (message: string) => joinCommands([shortDelayCmd, `echo ${message}`]);
|
||||||
joinCommands([shortDelayCmd, `echo ${message}`]);
|
const echoLines = (lines: string[]) => joinCommands(lines.map((line) => `echo ${line}`));
|
||||||
const echoLines = (lines: string[]) =>
|
|
||||||
joinCommands(lines.map((line) => `echo ${line}`));
|
|
||||||
const normalizeText = (value?: string) =>
|
const normalizeText = (value?: string) =>
|
||||||
sanitizeBinaryOutput(value ?? "")
|
sanitizeBinaryOutput(value ?? "")
|
||||||
.replace(/\r\n/g, "\n")
|
.replace(/\r\n/g, "\n")
|
||||||
@@ -74,8 +67,7 @@ describe("exec tool backgrounding", () => {
|
|||||||
|
|
||||||
let status = "running";
|
let status = "running";
|
||||||
let output = "";
|
let output = "";
|
||||||
const deadline =
|
const deadline = Date.now() + (process.platform === "win32" ? 8000 : 2000);
|
||||||
Date.now() + (process.platform === "win32" ? 8000 : 2000);
|
|
||||||
|
|
||||||
while (Date.now() < deadline && status === "running") {
|
while (Date.now() < deadline && status === "running") {
|
||||||
const poll = await processTool.execute("call2", {
|
const poll = await processTool.execute("call2", {
|
||||||
@@ -106,9 +98,7 @@ describe("exec tool backgrounding", () => {
|
|||||||
const sessionId = (result.details as { sessionId: string }).sessionId;
|
const sessionId = (result.details as { sessionId: string }).sessionId;
|
||||||
|
|
||||||
const list = await processTool.execute("call2", { action: "list" });
|
const list = await processTool.execute("call2", { action: "list" });
|
||||||
const sessions = (
|
const sessions = (list.details as { sessions: Array<{ sessionId: string }> }).sessions;
|
||||||
list.details as { sessions: Array<{ sessionId: string }> }
|
|
||||||
).sessions;
|
|
||||||
expect(sessions.some((s) => s.sessionId === sessionId)).toBe(true);
|
expect(sessions.some((s) => s.sessionId === sessionId)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,9 +111,8 @@ describe("exec tool backgrounding", () => {
|
|||||||
await sleep(25);
|
await sleep(25);
|
||||||
|
|
||||||
const list = await processTool.execute("call2", { action: "list" });
|
const list = await processTool.execute("call2", { action: "list" });
|
||||||
const sessions = (
|
const sessions = (list.details as { sessions: Array<{ sessionId: string; name?: string }> })
|
||||||
list.details as { sessions: Array<{ sessionId: string; name?: string }> }
|
.sessions;
|
||||||
).sessions;
|
|
||||||
const entry = sessions.find((s) => s.sessionId === sessionId);
|
const entry = sessions.find((s) => s.sessionId === sessionId);
|
||||||
expect(entry?.name).toBe("echo hello");
|
expect(entry?.name).toBe("echo hello");
|
||||||
});
|
});
|
||||||
@@ -239,9 +228,7 @@ describe("exec tool backgrounding", () => {
|
|||||||
const sessionB = (resultB.details as { sessionId: string }).sessionId;
|
const sessionB = (resultB.details as { sessionId: string }).sessionId;
|
||||||
|
|
||||||
const listA = await processA.execute("call3", { action: "list" });
|
const listA = await processA.execute("call3", { action: "list" });
|
||||||
const sessionsA = (
|
const sessionsA = (listA.details as { sessions: Array<{ sessionId: string }> }).sessions;
|
||||||
listA.details as { sessions: Array<{ sessionId: string }> }
|
|
||||||
).sessions;
|
|
||||||
expect(sessionsA.some((s) => s.sessionId === sessionA)).toBe(true);
|
expect(sessionsA.some((s) => s.sessionId === sessionA)).toBe(true);
|
||||||
expect(sessionsA.some((s) => s.sessionId === sessionB)).toBe(false);
|
expect(sessionsA.some((s) => s.sessionId === sessionB)).toBe(false);
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { listChannelPlugins } from "../channels/plugins/index.js";
|
|||||||
import type { ChannelAgentTool } from "../channels/plugins/types.js";
|
import type { ChannelAgentTool } from "../channels/plugins/types.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
|
||||||
export function listChannelAgentTools(params: {
|
export function listChannelAgentTools(params: { cfg?: ClawdbotConfig }): ChannelAgentTool[] {
|
||||||
cfg?: ClawdbotConfig;
|
|
||||||
}): ChannelAgentTool[] {
|
|
||||||
// Channel docking: aggregate channel-owned tools (login, etc.).
|
// Channel docking: aggregate channel-owned tools (login, etc.).
|
||||||
const tools: ChannelAgentTool[] = [];
|
const tools: ChannelAgentTool[] = [];
|
||||||
for (const plugin of listChannelPlugins()) {
|
for (const plugin of listChannelPlugins()) {
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ describe("chutes-oauth", () => {
|
|||||||
if (url === CHUTES_TOKEN_ENDPOINT) {
|
if (url === CHUTES_TOKEN_ENDPOINT) {
|
||||||
expect(init?.method).toBe("POST");
|
expect(init?.method).toBe("POST");
|
||||||
expect(
|
expect(
|
||||||
String(
|
String(init?.headers && (init.headers as Record<string, string>)["Content-Type"]),
|
||||||
init?.headers &&
|
|
||||||
(init.headers as Record<string, string>)["Content-Type"],
|
|
||||||
),
|
|
||||||
).toContain("application/x-www-form-urlencoded");
|
).toContain("application/x-www-form-urlencoded");
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -30,18 +27,12 @@ describe("chutes-oauth", () => {
|
|||||||
}
|
}
|
||||||
if (url === CHUTES_USERINFO_ENDPOINT) {
|
if (url === CHUTES_USERINFO_ENDPOINT) {
|
||||||
expect(
|
expect(
|
||||||
String(
|
String(init?.headers && (init.headers as Record<string, string>).Authorization),
|
||||||
init?.headers &&
|
|
||||||
(init.headers as Record<string, string>).Authorization,
|
|
||||||
),
|
|
||||||
).toBe("Bearer at_123");
|
).toBe("Bearer at_123");
|
||||||
return new Response(
|
return new Response(JSON.stringify({ username: "fred", sub: "sub_1" }), {
|
||||||
JSON.stringify({ username: "fred", sub: "sub_1" }),
|
status: 200,
|
||||||
{
|
headers: { "Content-Type": "application/json" },
|
||||||
status: 200,
|
});
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return new Response("not found", { status: 404 });
|
return new Response("not found", { status: 404 });
|
||||||
};
|
};
|
||||||
@@ -62,20 +53,15 @@ describe("chutes-oauth", () => {
|
|||||||
expect(creds.access).toBe("at_123");
|
expect(creds.access).toBe("at_123");
|
||||||
expect(creds.refresh).toBe("rt_123");
|
expect(creds.refresh).toBe("rt_123");
|
||||||
expect(creds.email).toBe("fred");
|
expect(creds.email).toBe("fred");
|
||||||
expect((creds as unknown as { accountId?: string }).accountId).toBe(
|
expect((creds as unknown as { accountId?: string }).accountId).toBe("sub_1");
|
||||||
"sub_1",
|
expect((creds as unknown as { clientId?: string }).clientId).toBe("cid_test");
|
||||||
);
|
|
||||||
expect((creds as unknown as { clientId?: string }).clientId).toBe(
|
|
||||||
"cid_test",
|
|
||||||
);
|
|
||||||
expect(creds.expires).toBe(now + 3600 * 1000 - 5 * 60 * 1000);
|
expect(creds.expires).toBe(now + 3600 * 1000 - 5 * 60 * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("refreshes tokens using stored client id and falls back to old refresh token", async () => {
|
it("refreshes tokens using stored client id and falls back to old refresh token", async () => {
|
||||||
const fetchFn: typeof fetch = async (input, init) => {
|
const fetchFn: typeof fetch = async (input, init) => {
|
||||||
const url = String(input);
|
const url = String(input);
|
||||||
if (url !== CHUTES_TOKEN_ENDPOINT)
|
if (url !== CHUTES_TOKEN_ENDPOINT) return new Response("not found", { status: 404 });
|
||||||
return new Response("not found", { status: 404 });
|
|
||||||
expect(init?.method).toBe("POST");
|
expect(init?.method).toBe("POST");
|
||||||
const body = init?.body as URLSearchParams;
|
const body = init?.body as URLSearchParams;
|
||||||
expect(String(body.get("grant_type"))).toBe("refresh_token");
|
expect(String(body.get("grant_type"))).toBe("refresh_token");
|
||||||
|
|||||||
@@ -59,10 +59,7 @@ export function parseOAuthCallbackInput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function coerceExpiresAt(expiresInSeconds: number, now: number): number {
|
function coerceExpiresAt(expiresInSeconds: number, now: number): number {
|
||||||
const value =
|
const value = now + Math.max(0, Math.floor(expiresInSeconds)) * 1000 - DEFAULT_EXPIRES_BUFFER_MS;
|
||||||
now +
|
|
||||||
Math.max(0, Math.floor(expiresInSeconds)) * 1000 -
|
|
||||||
DEFAULT_EXPIRES_BUFFER_MS;
|
|
||||||
return Math.max(value, now + 30_000);
|
return Math.max(value, now + 30_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +119,7 @@ export async function exchangeChutesCodeForTokens(params: {
|
|||||||
const refresh = data.refresh_token?.trim();
|
const refresh = data.refresh_token?.trim();
|
||||||
const expiresIn = data.expires_in ?? 0;
|
const expiresIn = data.expires_in ?? 0;
|
||||||
|
|
||||||
if (!access)
|
if (!access) throw new Error("Chutes token exchange returned no access_token");
|
||||||
throw new Error("Chutes token exchange returned no access_token");
|
|
||||||
if (!refresh) {
|
if (!refresh) {
|
||||||
throw new Error("Chutes token exchange returned no refresh_token");
|
throw new Error("Chutes token exchange returned no refresh_token");
|
||||||
}
|
}
|
||||||
@@ -153,12 +149,9 @@ export async function refreshChutesTokens(params: {
|
|||||||
throw new Error("Chutes OAuth credential is missing refresh token");
|
throw new Error("Chutes OAuth credential is missing refresh token");
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientId =
|
const clientId = params.credential.clientId?.trim() ?? process.env.CHUTES_CLIENT_ID?.trim();
|
||||||
params.credential.clientId?.trim() ?? process.env.CHUTES_CLIENT_ID?.trim();
|
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
throw new Error(
|
throw new Error("Missing CHUTES_CLIENT_ID for Chutes OAuth refresh (set env var or re-auth).");
|
||||||
"Missing CHUTES_CLIENT_ID for Chutes OAuth refresh (set env var or re-auth).",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const clientSecret = process.env.CHUTES_CLIENT_SECRET?.trim() || undefined;
|
const clientSecret = process.env.CHUTES_CLIENT_SECRET?.trim() || undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ function createDeferred<T>() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForCalls(
|
async function waitForCalls(mockFn: { mock: { calls: unknown[][] } }, count: number) {
|
||||||
mockFn: { mock: { calls: unknown[][] } },
|
|
||||||
count: number,
|
|
||||||
) {
|
|
||||||
for (let i = 0; i < 50; i += 1) {
|
for (let i = 0; i < 50; i += 1) {
|
||||||
if (mockFn.mock.calls.length >= count) return;
|
if (mockFn.mock.calls.length >= count) return;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
@@ -30,8 +27,7 @@ async function waitForCalls(
|
|||||||
}
|
}
|
||||||
|
|
||||||
vi.mock("../process/exec.js", () => ({
|
vi.mock("../process/exec.js", () => ({
|
||||||
runCommandWithTimeout: (...args: unknown[]) =>
|
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
|
||||||
runCommandWithTimeoutMock(...args),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("runClaudeCliAgent", () => {
|
describe("runClaudeCliAgent", () => {
|
||||||
|
|||||||
@@ -41,9 +41,7 @@ describe("gateway tool", () => {
|
|||||||
payload?: { kind?: string; doctorHint?: string | null };
|
payload?: { kind?: string; doctorHint?: string | null };
|
||||||
};
|
};
|
||||||
expect(parsed.payload?.kind).toBe("restart");
|
expect(parsed.payload?.kind).toBe("restart");
|
||||||
expect(parsed.payload?.doctorHint).toBe(
|
expect(parsed.payload?.doctorHint).toBe("Run: clawdbot doctor --non-interactive");
|
||||||
"Run: clawdbot doctor --non-interactive",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(kill).not.toHaveBeenCalled();
|
expect(kill).not.toHaveBeenCalled();
|
||||||
await vi.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
let configOverride: ReturnType<
|
let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = {
|
||||||
typeof import("../config/config.js")["loadConfig"]
|
|
||||||
> = {
|
|
||||||
session: {
|
session: {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
@@ -41,8 +39,7 @@ describe("agents_list", () => {
|
|||||||
requester: "main",
|
requester: "main",
|
||||||
allowAny: false,
|
allowAny: false,
|
||||||
});
|
});
|
||||||
const agents = (result.details as { agents?: Array<{ id: string }> })
|
const agents = (result.details as { agents?: Array<{ id: string }> }).agents;
|
||||||
.agents;
|
|
||||||
expect(agents?.map((agent) => agent.id)).toEqual(["main"]);
|
expect(agents?.map((agent) => agent.id)).toEqual(["main"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,11 +120,7 @@ describe("agents_list", () => {
|
|||||||
agents?: Array<{ id: string }>;
|
agents?: Array<{ id: string }>;
|
||||||
}
|
}
|
||||||
).agents;
|
).agents;
|
||||||
expect(agents?.map((agent) => agent.id)).toEqual([
|
expect(agents?.map((agent) => agent.id)).toEqual(["main", "coder", "research"]);
|
||||||
"main",
|
|
||||||
"coder",
|
|
||||||
"research",
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("marks allowlisted-but-unconfigured agents", async () => {
|
it("marks allowlisted-but-unconfigured agents", async () => {
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ describe("nodes camera_snap", () => {
|
|||||||
throw new Error(`unexpected method: ${String(method)}`);
|
throw new Error(`unexpected method: ${String(method)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const tool = createClawdbotTools().find(
|
const tool = createClawdbotTools().find((candidate) => candidate.name === "nodes");
|
||||||
(candidate) => candidate.name === "nodes",
|
|
||||||
);
|
|
||||||
if (!tool) throw new Error("missing nodes tool");
|
if (!tool) throw new Error("missing nodes tool");
|
||||||
|
|
||||||
const result = await tool.execute("call1", {
|
const result = await tool.execute("call1", {
|
||||||
@@ -46,9 +44,7 @@ describe("nodes camera_snap", () => {
|
|||||||
facing: "front",
|
facing: "front",
|
||||||
});
|
});
|
||||||
|
|
||||||
const images = (result.content ?? []).filter(
|
const images = (result.content ?? []).filter((block) => block.type === "image");
|
||||||
(block) => block.type === "image",
|
|
||||||
);
|
|
||||||
expect(images).toHaveLength(1);
|
expect(images).toHaveLength(1);
|
||||||
expect(images[0]?.mimeType).toBe("image/jpeg");
|
expect(images[0]?.mimeType).toBe("image/jpeg");
|
||||||
});
|
});
|
||||||
@@ -75,9 +71,7 @@ describe("nodes camera_snap", () => {
|
|||||||
throw new Error(`unexpected method: ${String(method)}`);
|
throw new Error(`unexpected method: ${String(method)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const tool = createClawdbotTools().find(
|
const tool = createClawdbotTools().find((candidate) => candidate.name === "nodes");
|
||||||
(candidate) => candidate.name === "nodes",
|
|
||||||
);
|
|
||||||
if (!tool) throw new Error("missing nodes tool");
|
if (!tool) throw new Error("missing nodes tool");
|
||||||
|
|
||||||
await tool.execute("call1", {
|
await tool.execute("call1", {
|
||||||
@@ -118,9 +112,7 @@ describe("nodes run", () => {
|
|||||||
throw new Error(`unexpected method: ${String(method)}`);
|
throw new Error(`unexpected method: ${String(method)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const tool = createClawdbotTools().find(
|
const tool = createClawdbotTools().find((candidate) => candidate.name === "nodes");
|
||||||
(candidate) => candidate.name === "nodes",
|
|
||||||
);
|
|
||||||
if (!tool) throw new Error("missing nodes tool");
|
if (!tool) throw new Error("missing nodes tool");
|
||||||
|
|
||||||
await tool.execute("call1", {
|
await tool.execute("call1", {
|
||||||
|
|||||||
@@ -54,9 +54,7 @@ describe("sessions tools", () => {
|
|||||||
expect(schemaProp("sessions_list", "activeMinutes").type).toBe("number");
|
expect(schemaProp("sessions_list", "activeMinutes").type).toBe("number");
|
||||||
expect(schemaProp("sessions_list", "messageLimit").type).toBe("number");
|
expect(schemaProp("sessions_list", "messageLimit").type).toBe("number");
|
||||||
expect(schemaProp("sessions_send", "timeoutSeconds").type).toBe("number");
|
expect(schemaProp("sessions_send", "timeoutSeconds").type).toBe("number");
|
||||||
expect(schemaProp("sessions_spawn", "runTimeoutSeconds").type).toBe(
|
expect(schemaProp("sessions_spawn", "runTimeoutSeconds").type).toBe("number");
|
||||||
"number",
|
|
||||||
);
|
|
||||||
expect(schemaProp("sessions_spawn", "timeoutSeconds").type).toBe("number");
|
expect(schemaProp("sessions_spawn", "timeoutSeconds").type).toBe("number");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,9 +106,7 @@ describe("sessions tools", () => {
|
|||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
||||||
const tool = createClawdbotTools().find(
|
const tool = createClawdbotTools().find((candidate) => candidate.name === "sessions_list");
|
||||||
(candidate) => candidate.name === "sessions_list",
|
|
||||||
);
|
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing sessions_list tool");
|
if (!tool) throw new Error("missing sessions_list tool");
|
||||||
|
|
||||||
@@ -147,9 +143,7 @@ describe("sessions tools", () => {
|
|||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
||||||
const tool = createClawdbotTools().find(
|
const tool = createClawdbotTools().find((candidate) => candidate.name === "sessions_history");
|
||||||
(candidate) => candidate.name === "sessions_history",
|
|
||||||
);
|
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing sessions_history tool");
|
if (!tool) throw new Error("missing sessions_history tool");
|
||||||
|
|
||||||
@@ -181,9 +175,7 @@ describe("sessions tools", () => {
|
|||||||
if (request.method === "agent") {
|
if (request.method === "agent") {
|
||||||
agentCallCount += 1;
|
agentCallCount += 1;
|
||||||
const runId = `run-${agentCallCount}`;
|
const runId = `run-${agentCallCount}`;
|
||||||
const params = request.params as
|
const params = request.params as { message?: string; sessionKey?: string } | undefined;
|
||||||
| { message?: string; sessionKey?: string }
|
|
||||||
| undefined;
|
|
||||||
const message = params?.message ?? "";
|
const message = params?.message ?? "";
|
||||||
let reply = "REPLY_SKIP";
|
let reply = "REPLY_SKIP";
|
||||||
if (message === "ping" || message === "wait") {
|
if (message === "ping" || message === "wait") {
|
||||||
@@ -207,8 +199,7 @@ describe("sessions tools", () => {
|
|||||||
}
|
}
|
||||||
if (request.method === "chat.history") {
|
if (request.method === "chat.history") {
|
||||||
_historyCallCount += 1;
|
_historyCallCount += 1;
|
||||||
const text =
|
const text = (lastWaitedRunId && replyByRunId.get(lastWaitedRunId)) ?? "";
|
||||||
(lastWaitedRunId && replyByRunId.get(lastWaitedRunId)) ?? "";
|
|
||||||
return {
|
return {
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
@@ -268,9 +259,7 @@ describe("sessions tools", () => {
|
|||||||
|
|
||||||
const agentCalls = calls.filter((call) => call.method === "agent");
|
const agentCalls = calls.filter((call) => call.method === "agent");
|
||||||
const waitCalls = calls.filter((call) => call.method === "agent.wait");
|
const waitCalls = calls.filter((call) => call.method === "agent.wait");
|
||||||
const historyOnlyCalls = calls.filter(
|
const historyOnlyCalls = calls.filter((call) => call.method === "chat.history");
|
||||||
(call) => call.method === "chat.history",
|
|
||||||
);
|
|
||||||
expect(agentCalls).toHaveLength(8);
|
expect(agentCalls).toHaveLength(8);
|
||||||
for (const call of agentCalls) {
|
for (const call of agentCalls) {
|
||||||
expect(call.params).toMatchObject({
|
expect(call.params).toMatchObject({
|
||||||
@@ -281,31 +270,28 @@ describe("sessions tools", () => {
|
|||||||
expect(
|
expect(
|
||||||
agentCalls.some(
|
agentCalls.some(
|
||||||
(call) =>
|
(call) =>
|
||||||
typeof (call.params as { extraSystemPrompt?: string })
|
typeof (call.params as { extraSystemPrompt?: string })?.extraSystemPrompt === "string" &&
|
||||||
?.extraSystemPrompt === "string" &&
|
(call.params as { extraSystemPrompt?: string })?.extraSystemPrompt?.includes(
|
||||||
(
|
"Agent-to-agent message context",
|
||||||
call.params as { extraSystemPrompt?: string }
|
),
|
||||||
)?.extraSystemPrompt?.includes("Agent-to-agent message context"),
|
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
agentCalls.some(
|
agentCalls.some(
|
||||||
(call) =>
|
(call) =>
|
||||||
typeof (call.params as { extraSystemPrompt?: string })
|
typeof (call.params as { extraSystemPrompt?: string })?.extraSystemPrompt === "string" &&
|
||||||
?.extraSystemPrompt === "string" &&
|
(call.params as { extraSystemPrompt?: string })?.extraSystemPrompt?.includes(
|
||||||
(
|
"Agent-to-agent reply step",
|
||||||
call.params as { extraSystemPrompt?: string }
|
),
|
||||||
)?.extraSystemPrompt?.includes("Agent-to-agent reply step"),
|
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
agentCalls.some(
|
agentCalls.some(
|
||||||
(call) =>
|
(call) =>
|
||||||
typeof (call.params as { extraSystemPrompt?: string })
|
typeof (call.params as { extraSystemPrompt?: string })?.extraSystemPrompt === "string" &&
|
||||||
?.extraSystemPrompt === "string" &&
|
(call.params as { extraSystemPrompt?: string })?.extraSystemPrompt?.includes(
|
||||||
(
|
"Agent-to-agent announce step",
|
||||||
call.params as { extraSystemPrompt?: string }
|
),
|
||||||
)?.extraSystemPrompt?.includes("Agent-to-agent announce step"),
|
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(waitCalls).toHaveLength(8);
|
expect(waitCalls).toHaveLength(8);
|
||||||
@@ -339,9 +325,7 @@ describe("sessions tools", () => {
|
|||||||
if (params?.extraSystemPrompt?.includes("Agent-to-agent reply step")) {
|
if (params?.extraSystemPrompt?.includes("Agent-to-agent reply step")) {
|
||||||
reply = params.sessionKey === requesterKey ? "pong-1" : "pong-2";
|
reply = params.sessionKey === requesterKey ? "pong-1" : "pong-2";
|
||||||
}
|
}
|
||||||
if (
|
if (params?.extraSystemPrompt?.includes("Agent-to-agent announce step")) {
|
||||||
params?.extraSystemPrompt?.includes("Agent-to-agent announce step")
|
|
||||||
) {
|
|
||||||
reply = "announce now";
|
reply = "announce now";
|
||||||
}
|
}
|
||||||
replyByRunId.set(runId, reply);
|
replyByRunId.set(runId, reply);
|
||||||
@@ -357,8 +341,7 @@ describe("sessions tools", () => {
|
|||||||
return { runId: params?.runId ?? "run-1", status: "ok" };
|
return { runId: params?.runId ?? "run-1", status: "ok" };
|
||||||
}
|
}
|
||||||
if (request.method === "chat.history") {
|
if (request.method === "chat.history") {
|
||||||
const text =
|
const text = (lastWaitedRunId && replyByRunId.get(lastWaitedRunId)) ?? "";
|
||||||
(lastWaitedRunId && replyByRunId.get(lastWaitedRunId)) ?? "";
|
|
||||||
return {
|
return {
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
@@ -414,11 +397,10 @@ describe("sessions tools", () => {
|
|||||||
const replySteps = calls.filter(
|
const replySteps = calls.filter(
|
||||||
(call) =>
|
(call) =>
|
||||||
call.method === "agent" &&
|
call.method === "agent" &&
|
||||||
typeof (call.params as { extraSystemPrompt?: string })
|
typeof (call.params as { extraSystemPrompt?: string })?.extraSystemPrompt === "string" &&
|
||||||
?.extraSystemPrompt === "string" &&
|
(call.params as { extraSystemPrompt?: string })?.extraSystemPrompt?.includes(
|
||||||
(
|
"Agent-to-agent reply step",
|
||||||
call.params as { extraSystemPrompt?: string }
|
),
|
||||||
)?.extraSystemPrompt?.includes("Agent-to-agent reply step"),
|
|
||||||
);
|
);
|
||||||
expect(replySteps).toHaveLength(2);
|
expect(replySteps).toHaveLength(2);
|
||||||
expect(sendParams).toMatchObject({
|
expect(sendParams).toMatchObject({
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({
|
|||||||
callGateway: (opts: unknown) => callGatewayMock(opts),
|
callGateway: (opts: unknown) => callGatewayMock(opts),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let configOverride: ReturnType<
|
let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = {
|
||||||
typeof import("../config/config.js")["loadConfig"]
|
|
||||||
> = {
|
|
||||||
session: {
|
session: {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({
|
|||||||
callGateway: (opts: unknown) => callGatewayMock(opts),
|
callGateway: (opts: unknown) => callGatewayMock(opts),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let configOverride: ReturnType<
|
let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = {
|
||||||
typeof import("../config/config.js")["loadConfig"]
|
|
||||||
> = {
|
|
||||||
session: {
|
session: {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
@@ -78,9 +76,7 @@ describe("clawdbot-tools: subagents", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (request.method === "agent.wait") {
|
if (request.method === "agent.wait") {
|
||||||
const params = request.params as
|
const params = request.params as { runId?: string; timeoutMs?: number } | undefined;
|
||||||
| { runId?: string; timeoutMs?: number }
|
|
||||||
| undefined;
|
|
||||||
waitCalls.push(params ?? {});
|
waitCalls.push(params ?? {});
|
||||||
return {
|
return {
|
||||||
runId: params?.runId ?? "run-1",
|
runId: params?.runId ?? "run-1",
|
||||||
@@ -91,8 +87,7 @@ describe("clawdbot-tools: subagents", () => {
|
|||||||
}
|
}
|
||||||
if (request.method === "chat.history") {
|
if (request.method === "chat.history") {
|
||||||
const params = request.params as { sessionKey?: string } | undefined;
|
const params = request.params as { sessionKey?: string } | undefined;
|
||||||
const text =
|
const text = sessionLastAssistantText.get(params?.sessionKey ?? "") ?? "";
|
||||||
sessionLastAssistantText.get(params?.sessionKey ?? "") ?? "";
|
|
||||||
return {
|
return {
|
||||||
messages: [{ role: "assistant", content: [{ type: "text", text }] }],
|
messages: [{ role: "assistant", content: [{ type: "text", text }] }],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({
|
|||||||
callGateway: (opts: unknown) => callGatewayMock(opts),
|
callGateway: (opts: unknown) => callGatewayMock(opts),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let configOverride: ReturnType<
|
let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = {
|
||||||
typeof import("../config/config.js")["loadConfig"]
|
|
||||||
> = {
|
|
||||||
session: {
|
session: {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
@@ -79,17 +77,14 @@ describe("clawdbot-tools: subagents", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (request.method === "agent.wait") {
|
if (request.method === "agent.wait") {
|
||||||
const params = request.params as
|
const params = request.params as { runId?: string; timeoutMs?: number } | undefined;
|
||||||
| { runId?: string; timeoutMs?: number }
|
|
||||||
| undefined;
|
|
||||||
waitCalls.push(params ?? {});
|
waitCalls.push(params ?? {});
|
||||||
const status = params?.runId === childRunId ? "timeout" : "ok";
|
const status = params?.runId === childRunId ? "timeout" : "ok";
|
||||||
return { runId: params?.runId ?? "run-1", status };
|
return { runId: params?.runId ?? "run-1", status };
|
||||||
}
|
}
|
||||||
if (request.method === "chat.history") {
|
if (request.method === "chat.history") {
|
||||||
const params = request.params as { sessionKey?: string } | undefined;
|
const params = request.params as { sessionKey?: string } | undefined;
|
||||||
const text =
|
const text = sessionLastAssistantText.get(params?.sessionKey ?? "") ?? "";
|
||||||
sessionLastAssistantText.get(params?.sessionKey ?? "") ?? "";
|
|
||||||
return {
|
return {
|
||||||
messages: [{ role: "assistant", content: [{ type: "text", text }] }],
|
messages: [{ role: "assistant", content: [{ type: "text", text }] }],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({
|
|||||||
callGateway: (opts: unknown) => callGatewayMock(opts),
|
callGateway: (opts: unknown) => callGatewayMock(opts),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let configOverride: ReturnType<
|
let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = {
|
||||||
typeof import("../config/config.js")["loadConfig"]
|
|
||||||
> = {
|
|
||||||
session: {
|
session: {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
@@ -83,9 +81,7 @@ describe("clawdbot-tools: subagents", () => {
|
|||||||
modelApplied: true,
|
modelApplied: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const patchIndex = calls.findIndex(
|
const patchIndex = calls.findIndex((call) => call.method === "sessions.patch");
|
||||||
(call) => call.method === "sessions.patch",
|
|
||||||
);
|
|
||||||
const agentIndex = calls.findIndex((call) => call.method === "agent");
|
const agentIndex = calls.findIndex((call) => call.method === "agent");
|
||||||
expect(patchIndex).toBeGreaterThan(-1);
|
expect(patchIndex).toBeGreaterThan(-1);
|
||||||
expect(agentIndex).toBeGreaterThan(-1);
|
expect(agentIndex).toBeGreaterThan(-1);
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({
|
|||||||
callGateway: (opts: unknown) => callGatewayMock(opts),
|
callGateway: (opts: unknown) => callGatewayMock(opts),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let configOverride: ReturnType<
|
let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = {
|
||||||
typeof import("../config/config.js")["loadConfig"]
|
|
||||||
> = {
|
|
||||||
session: {
|
session: {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({
|
|||||||
callGateway: (opts: unknown) => callGatewayMock(opts),
|
callGateway: (opts: unknown) => callGatewayMock(opts),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let configOverride: ReturnType<
|
let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = {
|
||||||
typeof import("../config/config.js")["loadConfig"]
|
|
||||||
> = {
|
|
||||||
session: {
|
session: {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
@@ -124,9 +122,9 @@ describe("clawdbot-tools: subagents", () => {
|
|||||||
status: "accepted",
|
status: "accepted",
|
||||||
modelApplied: false,
|
modelApplied: false,
|
||||||
});
|
});
|
||||||
expect(
|
expect(String((result.details as { warning?: string }).warning ?? "")).toContain(
|
||||||
String((result.details as { warning?: string }).warning ?? ""),
|
"invalid model",
|
||||||
).toContain("invalid model");
|
);
|
||||||
expect(calls.some((call) => call.method === "agent")).toBe(true);
|
expect(calls.some((call) => call.method === "agent")).toBe(true);
|
||||||
});
|
});
|
||||||
it("sessions_spawn supports legacy timeoutSeconds alias", async () => {
|
it("sessions_spawn supports legacy timeoutSeconds alias", async () => {
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ vi.mock("../gateway/call.js", () => ({
|
|||||||
callGateway: (opts: unknown) => callGatewayMock(opts),
|
callGateway: (opts: unknown) => callGatewayMock(opts),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let configOverride: ReturnType<
|
let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = {
|
||||||
typeof import("../config/config.js")["loadConfig"]
|
|
||||||
> = {
|
|
||||||
session: {
|
session: {
|
||||||
mainKey: "main",
|
mainKey: "main",
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
@@ -85,17 +83,14 @@ describe("clawdbot-tools: subagents", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (request.method === "agent.wait") {
|
if (request.method === "agent.wait") {
|
||||||
const params = request.params as
|
const params = request.params as { runId?: string; timeoutMs?: number } | undefined;
|
||||||
| { runId?: string; timeoutMs?: number }
|
|
||||||
| undefined;
|
|
||||||
waitCalls.push(params ?? {});
|
waitCalls.push(params ?? {});
|
||||||
const status = params?.runId === childRunId ? "timeout" : "ok";
|
const status = params?.runId === childRunId ? "timeout" : "ok";
|
||||||
return { runId: params?.runId ?? "run-1", status };
|
return { runId: params?.runId ?? "run-1", status };
|
||||||
}
|
}
|
||||||
if (request.method === "chat.history") {
|
if (request.method === "chat.history") {
|
||||||
const params = request.params as { sessionKey?: string } | undefined;
|
const params = request.params as { sessionKey?: string } | undefined;
|
||||||
const text =
|
const text = sessionLastAssistantText.get(params?.sessionKey ?? "") ?? "";
|
||||||
sessionLastAssistantText.get(params?.sessionKey ?? "") ?? "";
|
|
||||||
return {
|
return {
|
||||||
messages: [{ role: "assistant", content: [{ type: "text", text }] }],
|
messages: [{ role: "assistant", content: [{ type: "text", text }] }],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ import type { AnyAgentTool } from "./tools/common.js";
|
|||||||
import { createCronTool } from "./tools/cron-tool.js";
|
import { createCronTool } from "./tools/cron-tool.js";
|
||||||
import { createGatewayTool } from "./tools/gateway-tool.js";
|
import { createGatewayTool } from "./tools/gateway-tool.js";
|
||||||
import { createImageTool } from "./tools/image-tool.js";
|
import { createImageTool } from "./tools/image-tool.js";
|
||||||
import {
|
import { createMemoryGetTool, createMemorySearchTool } from "./tools/memory-tool.js";
|
||||||
createMemoryGetTool,
|
|
||||||
createMemorySearchTool,
|
|
||||||
} from "./tools/memory-tool.js";
|
|
||||||
import { createMessageTool } from "./tools/message-tool.js";
|
import { createMessageTool } from "./tools/message-tool.js";
|
||||||
import { createNodesTool } from "./tools/nodes-tool.js";
|
import { createNodesTool } from "./tools/nodes-tool.js";
|
||||||
import { createSessionStatusTool } from "./tools/session-status-tool.js";
|
import { createSessionStatusTool } from "./tools/session-status-tool.js";
|
||||||
@@ -105,9 +102,7 @@ export function createClawdbotTools(options?: {
|
|||||||
agentSessionKey: options?.agentSessionKey,
|
agentSessionKey: options?.agentSessionKey,
|
||||||
config: options?.config,
|
config: options?.config,
|
||||||
}),
|
}),
|
||||||
...(memorySearchTool && memoryGetTool
|
...(memorySearchTool && memoryGetTool ? [memorySearchTool, memoryGetTool] : []),
|
||||||
? [memorySearchTool, memoryGetTool]
|
|
||||||
: []),
|
|
||||||
...(imageTool ? [imageTool] : []),
|
...(imageTool ? [imageTool] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -34,12 +34,7 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = {
|
|||||||
modelAliases: CLAUDE_MODEL_ALIASES,
|
modelAliases: CLAUDE_MODEL_ALIASES,
|
||||||
sessionArg: "--session-id",
|
sessionArg: "--session-id",
|
||||||
sessionMode: "always",
|
sessionMode: "always",
|
||||||
sessionIdFields: [
|
sessionIdFields: ["session_id", "sessionId", "conversation_id", "conversationId"],
|
||||||
"session_id",
|
|
||||||
"sessionId",
|
|
||||||
"conversation_id",
|
|
||||||
"conversationId",
|
|
||||||
],
|
|
||||||
systemPromptArg: "--append-system-prompt",
|
systemPromptArg: "--append-system-prompt",
|
||||||
systemPromptMode: "append",
|
systemPromptMode: "append",
|
||||||
systemPromptWhen: "first",
|
systemPromptWhen: "first",
|
||||||
@@ -49,15 +44,7 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = {
|
|||||||
|
|
||||||
const DEFAULT_CODEX_BACKEND: CliBackendConfig = {
|
const DEFAULT_CODEX_BACKEND: CliBackendConfig = {
|
||||||
command: "codex",
|
command: "codex",
|
||||||
args: [
|
args: ["exec", "--json", "--color", "never", "--sandbox", "read-only", "--skip-git-repo-check"],
|
||||||
"exec",
|
|
||||||
"--json",
|
|
||||||
"--color",
|
|
||||||
"never",
|
|
||||||
"--sandbox",
|
|
||||||
"read-only",
|
|
||||||
"--skip-git-repo-check",
|
|
||||||
],
|
|
||||||
resumeArgs: [
|
resumeArgs: [
|
||||||
"exec",
|
"exec",
|
||||||
"resume",
|
"resume",
|
||||||
@@ -93,10 +80,7 @@ function pickBackendConfig(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeBackendConfig(
|
function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig): CliBackendConfig {
|
||||||
base: CliBackendConfig,
|
|
||||||
override?: CliBackendConfig,
|
|
||||||
): CliBackendConfig {
|
|
||||||
if (!override) return { ...base };
|
if (!override) return { ...base };
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
@@ -104,9 +88,7 @@ function mergeBackendConfig(
|
|||||||
args: override.args ?? base.args,
|
args: override.args ?? base.args,
|
||||||
env: { ...base.env, ...override.env },
|
env: { ...base.env, ...override.env },
|
||||||
modelAliases: { ...base.modelAliases, ...override.modelAliases },
|
modelAliases: { ...base.modelAliases, ...override.modelAliases },
|
||||||
clearEnv: Array.from(
|
clearEnv: Array.from(new Set([...(base.clearEnv ?? []), ...(override.clearEnv ?? [])])),
|
||||||
new Set([...(base.clearEnv ?? []), ...(override.clearEnv ?? [])]),
|
|
||||||
),
|
|
||||||
sessionIdFields: override.sessionIdFields ?? base.sessionIdFields,
|
sessionIdFields: override.sessionIdFields ?? base.sessionIdFields,
|
||||||
sessionArgs: override.sessionArgs ?? base.sessionArgs,
|
sessionArgs: override.sessionArgs ?? base.sessionArgs,
|
||||||
resumeArgs: override.resumeArgs ?? base.resumeArgs,
|
resumeArgs: override.resumeArgs ?? base.resumeArgs,
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ describe("cli credentials", () => {
|
|||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
|
|
||||||
const { writeClaudeCliKeychainCredentials } = await import(
|
const { writeClaudeCliKeychainCredentials } = await import("./cli-credentials.js");
|
||||||
"./cli-credentials.js"
|
|
||||||
);
|
|
||||||
|
|
||||||
const ok = writeClaudeCliKeychainCredentials({
|
const ok = writeClaudeCliKeychainCredentials({
|
||||||
access: "new-access",
|
access: "new-access",
|
||||||
@@ -53,13 +51,9 @@ describe("cli credentials", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(ok).toBe(true);
|
expect(ok).toBe(true);
|
||||||
expect(
|
expect(commands.some((cmd) => cmd.includes("delete-generic-password"))).toBe(false);
|
||||||
commands.some((cmd) => cmd.includes("delete-generic-password")),
|
|
||||||
).toBe(false);
|
|
||||||
|
|
||||||
const updateCommand = commands.find((cmd) =>
|
const updateCommand = commands.find((cmd) => cmd.includes("add-generic-password"));
|
||||||
cmd.includes("add-generic-password"),
|
|
||||||
);
|
|
||||||
expect(updateCommand).toContain("-U");
|
expect(updateCommand).toContain("-U");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,9 +124,7 @@ describe("cli credentials", () => {
|
|||||||
|
|
||||||
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"));
|
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"));
|
||||||
|
|
||||||
const { readClaudeCliCredentialsCached } = await import(
|
const { readClaudeCliCredentialsCached } = await import("./cli-credentials.js");
|
||||||
"./cli-credentials.js"
|
|
||||||
);
|
|
||||||
|
|
||||||
const first = readClaudeCliCredentialsCached({
|
const first = readClaudeCliCredentialsCached({
|
||||||
allowKeychainPrompt: true,
|
allowKeychainPrompt: true,
|
||||||
@@ -163,9 +155,7 @@ describe("cli credentials", () => {
|
|||||||
|
|
||||||
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"));
|
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"));
|
||||||
|
|
||||||
const { readClaudeCliCredentialsCached } = await import(
|
const { readClaudeCliCredentialsCached } = await import("./cli-credentials.js");
|
||||||
"./cli-credentials.js"
|
|
||||||
);
|
|
||||||
|
|
||||||
const first = readClaudeCliCredentialsCached({
|
const first = readClaudeCliCredentialsCached({
|
||||||
allowKeychainPrompt: true,
|
allowKeychainPrompt: true,
|
||||||
|
|||||||
@@ -56,10 +56,7 @@ type ClaudeCliFileOptions = {
|
|||||||
type ClaudeCliWriteOptions = ClaudeCliFileOptions & {
|
type ClaudeCliWriteOptions = ClaudeCliFileOptions & {
|
||||||
platform?: NodeJS.Platform;
|
platform?: NodeJS.Platform;
|
||||||
writeKeychain?: (credentials: OAuthCredentials) => boolean;
|
writeKeychain?: (credentials: OAuthCredentials) => boolean;
|
||||||
writeFile?: (
|
writeFile?: (credentials: OAuthCredentials, options?: ClaudeCliFileOptions) => boolean;
|
||||||
credentials: OAuthCredentials,
|
|
||||||
options?: ClaudeCliFileOptions,
|
|
||||||
) => boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveClaudeCliCredentialsPath(homeDir?: string) {
|
function resolveClaudeCliCredentialsPath(homeDir?: string) {
|
||||||
@@ -73,9 +70,7 @@ function resolveCodexCliAuthPath() {
|
|||||||
|
|
||||||
function resolveCodexHomePath() {
|
function resolveCodexHomePath() {
|
||||||
const configured = process.env.CODEX_HOME;
|
const configured = process.env.CODEX_HOME;
|
||||||
const home = configured
|
const home = configured ? resolveUserPath(configured) : resolveUserPath("~/.codex");
|
||||||
? resolveUserPath(configured)
|
|
||||||
: resolveUserPath("~/.codex");
|
|
||||||
try {
|
try {
|
||||||
return fs.realpathSync.native(home);
|
return fs.realpathSync.native(home);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -98,10 +93,11 @@ function readCodexKeychainCredentials(options?: {
|
|||||||
const account = computeCodexKeychainAccount(codexHome);
|
const account = computeCodexKeychainAccount(codexHome);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const secret = execSync(
|
const secret = execSync(`security find-generic-password -s "Codex Auth" -a "${account}" -w`, {
|
||||||
`security find-generic-password -s "Codex Auth" -a "${account}" -w`,
|
encoding: "utf8",
|
||||||
{ encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
|
timeout: 5000,
|
||||||
).trim();
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
}).trim();
|
||||||
|
|
||||||
const parsed = JSON.parse(secret) as Record<string, unknown>;
|
const parsed = JSON.parse(secret) as Record<string, unknown>;
|
||||||
const tokens = parsed.tokens as Record<string, unknown> | undefined;
|
const tokens = parsed.tokens as Record<string, unknown> | undefined;
|
||||||
@@ -253,9 +249,7 @@ export function readClaudeCliCredentialsCached(options?: {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function writeClaudeCliKeychainCredentials(
|
export function writeClaudeCliKeychainCredentials(newCredentials: OAuthCredentials): boolean {
|
||||||
newCredentials: OAuthCredentials,
|
|
||||||
): boolean {
|
|
||||||
try {
|
try {
|
||||||
const existingResult = execSync(
|
const existingResult = execSync(
|
||||||
`security find-generic-password -s "${CLAUDE_CLI_KEYCHAIN_SERVICE}" -w 2>/dev/null`,
|
`security find-generic-password -s "${CLAUDE_CLI_KEYCHAIN_SERVICE}" -w 2>/dev/null`,
|
||||||
@@ -309,9 +303,7 @@ export function writeClaudeCliFileCredentials(
|
|||||||
if (!raw || typeof raw !== "object") return false;
|
if (!raw || typeof raw !== "object") return false;
|
||||||
|
|
||||||
const data = raw as Record<string, unknown>;
|
const data = raw as Record<string, unknown>;
|
||||||
const existingOauth = data.claudeAiOauth as
|
const existingOauth = data.claudeAiOauth as Record<string, unknown> | undefined;
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined;
|
|
||||||
if (!existingOauth || typeof existingOauth !== "object") return false;
|
if (!existingOauth || typeof existingOauth !== "object") return false;
|
||||||
|
|
||||||
data.claudeAiOauth = {
|
data.claudeAiOauth = {
|
||||||
@@ -339,12 +331,10 @@ export function writeClaudeCliCredentials(
|
|||||||
options?: ClaudeCliWriteOptions,
|
options?: ClaudeCliWriteOptions,
|
||||||
): boolean {
|
): boolean {
|
||||||
const platform = options?.platform ?? process.platform;
|
const platform = options?.platform ?? process.platform;
|
||||||
const writeKeychain =
|
const writeKeychain = options?.writeKeychain ?? writeClaudeCliKeychainCredentials;
|
||||||
options?.writeKeychain ?? writeClaudeCliKeychainCredentials;
|
|
||||||
const writeFile =
|
const writeFile =
|
||||||
options?.writeFile ??
|
options?.writeFile ??
|
||||||
((credentials, fileOptions) =>
|
((credentials, fileOptions) => writeClaudeCliFileCredentials(credentials, fileOptions));
|
||||||
writeClaudeCliFileCredentials(credentials, fileOptions));
|
|
||||||
|
|
||||||
if (platform === "darwin") {
|
if (platform === "darwin") {
|
||||||
const didWriteKeychain = writeKeychain(newCredentials);
|
const didWriteKeychain = writeKeychain(newCredentials);
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ const runCommandWithTimeoutMock = vi.fn();
|
|||||||
const runExecMock = vi.fn();
|
const runExecMock = vi.fn();
|
||||||
|
|
||||||
vi.mock("../process/exec.js", () => ({
|
vi.mock("../process/exec.js", () => ({
|
||||||
runCommandWithTimeout: (...args: unknown[]) =>
|
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
|
||||||
runCommandWithTimeoutMock(...args),
|
|
||||||
runExec: (...args: unknown[]) => runExecMock(...args),
|
runExec: (...args: unknown[]) => runExecMock(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,7 @@ import {
|
|||||||
resolveBootstrapMaxChars,
|
resolveBootstrapMaxChars,
|
||||||
} from "./pi-embedded-helpers.js";
|
} from "./pi-embedded-helpers.js";
|
||||||
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js";
|
||||||
import {
|
import { filterBootstrapFilesForSession, loadWorkspaceBootstrapFiles } from "./workspace.js";
|
||||||
filterBootstrapFilesForSession,
|
|
||||||
loadWorkspaceBootstrapFiles,
|
|
||||||
} from "./workspace.js";
|
|
||||||
|
|
||||||
const log = createSubsystemLogger("agent/claude-cli");
|
const log = createSubsystemLogger("agent/claude-cli");
|
||||||
|
|
||||||
@@ -58,10 +55,7 @@ export async function runCliAgent(params: {
|
|||||||
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||||
const workspaceDir = resolvedWorkspace;
|
const workspaceDir = resolvedWorkspace;
|
||||||
|
|
||||||
const backendResolved = resolveCliBackendConfig(
|
const backendResolved = resolveCliBackendConfig(params.provider, params.config);
|
||||||
params.provider,
|
|
||||||
params.config,
|
|
||||||
);
|
|
||||||
if (!backendResolved) {
|
if (!backendResolved) {
|
||||||
throw new Error(`Unknown CLI backend: ${params.provider}`);
|
throw new Error(`Unknown CLI backend: ${params.provider}`);
|
||||||
}
|
}
|
||||||
@@ -92,9 +86,7 @@ export async function runCliAgent(params: {
|
|||||||
});
|
});
|
||||||
const heartbeatPrompt =
|
const heartbeatPrompt =
|
||||||
sessionAgentId === defaultAgentId
|
sessionAgentId === defaultAgentId
|
||||||
? resolveHeartbeatPrompt(
|
? resolveHeartbeatPrompt(params.config?.agents?.defaults?.heartbeat?.prompt)
|
||||||
params.config?.agents?.defaults?.heartbeat?.prompt,
|
|
||||||
)
|
|
||||||
: undefined;
|
: undefined;
|
||||||
const systemPrompt = buildSystemPrompt({
|
const systemPrompt = buildSystemPrompt({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
@@ -114,14 +106,12 @@ export async function runCliAgent(params: {
|
|||||||
});
|
});
|
||||||
const useResume = Boolean(
|
const useResume = Boolean(
|
||||||
params.cliSessionId &&
|
params.cliSessionId &&
|
||||||
cliSessionIdToSend &&
|
cliSessionIdToSend &&
|
||||||
backend.resumeArgs &&
|
backend.resumeArgs &&
|
||||||
backend.resumeArgs.length > 0,
|
backend.resumeArgs.length > 0,
|
||||||
);
|
);
|
||||||
const sessionIdSent = cliSessionIdToSend
|
const sessionIdSent = cliSessionIdToSend
|
||||||
? useResume ||
|
? useResume || Boolean(backend.sessionArg) || Boolean(backend.sessionArgs?.length)
|
||||||
Boolean(backend.sessionArg) ||
|
|
||||||
Boolean(backend.sessionArgs?.length)
|
|
||||||
? cliSessionIdToSend
|
? cliSessionIdToSend
|
||||||
: undefined
|
: undefined
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -148,13 +138,9 @@ export async function runCliAgent(params: {
|
|||||||
prompt,
|
prompt,
|
||||||
});
|
});
|
||||||
const stdinPayload = stdin ?? "";
|
const stdinPayload = stdin ?? "";
|
||||||
const baseArgs = useResume
|
const baseArgs = useResume ? (backend.resumeArgs ?? backend.args ?? []) : (backend.args ?? []);
|
||||||
? (backend.resumeArgs ?? backend.args ?? [])
|
|
||||||
: (backend.args ?? []);
|
|
||||||
const resolvedArgs = useResume
|
const resolvedArgs = useResume
|
||||||
? baseArgs.map((entry) =>
|
? baseArgs.map((entry) => entry.replaceAll("{sessionId}", cliSessionIdToSend ?? ""))
|
||||||
entry.replaceAll("{sessionId}", cliSessionIdToSend ?? ""),
|
|
||||||
)
|
|
||||||
: baseArgs;
|
: baseArgs;
|
||||||
const args = buildCliArgs({
|
const args = buildCliArgs({
|
||||||
backend,
|
backend,
|
||||||
@@ -168,9 +154,7 @@ export async function runCliAgent(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const serialize = backend.serialize ?? true;
|
const serialize = backend.serialize ?? true;
|
||||||
const queueKey = serialize
|
const queueKey = serialize ? backendResolved.id : `${backendResolved.id}:${params.runId}`;
|
||||||
? backendResolved.id
|
|
||||||
: `${backendResolved.id}:${params.runId}`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const output = await enqueueCliRun(queueKey, async () => {
|
const output = await enqueueCliRun(queueKey, async () => {
|
||||||
@@ -184,10 +168,7 @@ export async function runCliAgent(params: {
|
|||||||
const arg = args[i] ?? "";
|
const arg = args[i] ?? "";
|
||||||
if (arg === backend.systemPromptArg) {
|
if (arg === backend.systemPromptArg) {
|
||||||
const systemPromptValue = args[i + 1] ?? "";
|
const systemPromptValue = args[i + 1] ?? "";
|
||||||
logArgs.push(
|
logArgs.push(arg, `<systemPrompt:${systemPromptValue.length} chars>`);
|
||||||
arg,
|
|
||||||
`<systemPrompt:${systemPromptValue.length} chars>`,
|
|
||||||
);
|
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -259,9 +240,7 @@ export async function runCliAgent(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputMode = useResume
|
const outputMode = useResume ? (backend.resumeOutput ?? backend.output) : backend.output;
|
||||||
? (backend.resumeOutput ?? backend.output)
|
|
||||||
: backend.output;
|
|
||||||
|
|
||||||
if (outputMode === "text") {
|
if (outputMode === "text") {
|
||||||
return { text: stdout, sessionId: undefined };
|
return { text: stdout, sessionId: undefined };
|
||||||
@@ -283,8 +262,7 @@ export async function runCliAgent(params: {
|
|||||||
meta: {
|
meta: {
|
||||||
durationMs: Date.now() - started,
|
durationMs: Date.now() - started,
|
||||||
agentMeta: {
|
agentMeta: {
|
||||||
sessionId:
|
sessionId: output.sessionId ?? sessionIdSent ?? params.sessionId ?? "",
|
||||||
output.sessionId ?? sessionIdSent ?? params.sessionId ?? "",
|
|
||||||
provider: params.provider,
|
provider: params.provider,
|
||||||
model: modelId,
|
model: modelId,
|
||||||
usage: output.usage,
|
usage: output.usage,
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ export async function cleanupResumeProcesses(
|
|||||||
const commandToken = path.basename(backend.command ?? "").trim();
|
const commandToken = path.basename(backend.command ?? "").trim();
|
||||||
if (!commandToken) return;
|
if (!commandToken) return;
|
||||||
|
|
||||||
const resumeTokens = resumeArgs.map((arg) =>
|
const resumeTokens = resumeArgs.map((arg) => arg.replaceAll("{sessionId}", sessionId));
|
||||||
arg.replaceAll("{sessionId}", sessionId),
|
|
||||||
);
|
|
||||||
const pattern = [commandToken, ...resumeTokens]
|
const pattern = [commandToken, ...resumeTokens]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((token) => escapeRegex(token))
|
.map((token) => escapeRegex(token))
|
||||||
@@ -45,10 +43,7 @@ export async function cleanupResumeProcesses(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enqueueCliRun<T>(
|
export function enqueueCliRun<T>(key: string, task: () => Promise<T>): Promise<T> {
|
||||||
key: string,
|
|
||||||
task: () => Promise<T>,
|
|
||||||
): Promise<T> {
|
|
||||||
const prior = CLI_RUN_QUEUE.get(key) ?? Promise.resolve();
|
const prior = CLI_RUN_QUEUE.get(key) ?? Promise.resolve();
|
||||||
const chained = prior.catch(() => undefined).then(task);
|
const chained = prior.catch(() => undefined).then(task);
|
||||||
const tracked = chained.finally(() => {
|
const tracked = chained.finally(() => {
|
||||||
@@ -78,9 +73,7 @@ function resolveUserTimezone(configured?: string): string {
|
|||||||
const trimmed = configured?.trim();
|
const trimmed = configured?.trim();
|
||||||
if (trimmed) {
|
if (trimmed) {
|
||||||
try {
|
try {
|
||||||
new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format(
|
new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format(new Date());
|
||||||
new Date(),
|
|
||||||
);
|
|
||||||
return trimmed;
|
return trimmed;
|
||||||
} catch {
|
} catch {
|
||||||
// ignore invalid timezone
|
// ignore invalid timezone
|
||||||
@@ -106,14 +99,7 @@ function formatUserTime(date: Date, timeZone: string): string | undefined {
|
|||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (part.type !== "literal") map[part.type] = part.value;
|
if (part.type !== "literal") map[part.type] = part.value;
|
||||||
}
|
}
|
||||||
if (
|
if (!map.weekday || !map.year || !map.month || !map.day || !map.hour || !map.minute)
|
||||||
!map.weekday ||
|
|
||||||
!map.year ||
|
|
||||||
!map.month ||
|
|
||||||
!map.day ||
|
|
||||||
!map.hour ||
|
|
||||||
!map.minute
|
|
||||||
)
|
|
||||||
return undefined;
|
return undefined;
|
||||||
return `${map.weekday} ${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}`;
|
return `${map.weekday} ${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}`;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -127,9 +113,7 @@ function buildModelAliasLines(cfg?: ClawdbotConfig) {
|
|||||||
for (const [keyRaw, entryRaw] of Object.entries(models)) {
|
for (const [keyRaw, entryRaw] of Object.entries(models)) {
|
||||||
const model = String(keyRaw ?? "").trim();
|
const model = String(keyRaw ?? "").trim();
|
||||||
if (!model) continue;
|
if (!model) continue;
|
||||||
const alias = String(
|
const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim();
|
||||||
(entryRaw as { alias?: string } | undefined)?.alias ?? "",
|
|
||||||
).trim();
|
|
||||||
if (!alias) continue;
|
if (!alias) continue;
|
||||||
entries.push({ alias, model });
|
entries.push({ alias, model });
|
||||||
}
|
}
|
||||||
@@ -149,9 +133,7 @@ export function buildSystemPrompt(params: {
|
|||||||
contextFiles?: EmbeddedContextFile[];
|
contextFiles?: EmbeddedContextFile[];
|
||||||
modelDisplay: string;
|
modelDisplay: string;
|
||||||
}) {
|
}) {
|
||||||
const userTimezone = resolveUserTimezone(
|
const userTimezone = resolveUserTimezone(params.config?.agents?.defaults?.userTimezone);
|
||||||
params.config?.agents?.defaults?.userTimezone,
|
|
||||||
);
|
|
||||||
const userTime = formatUserTime(new Date(), userTimezone);
|
const userTime = formatUserTime(new Date(), userTimezone);
|
||||||
return buildAgentSystemPrompt({
|
return buildAgentSystemPrompt({
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
@@ -175,10 +157,7 @@ export function buildSystemPrompt(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCliModel(
|
export function normalizeCliModel(modelId: string, backend: CliBackendConfig): string {
|
||||||
modelId: string,
|
|
||||||
backend: CliBackendConfig,
|
|
||||||
): string {
|
|
||||||
const trimmed = modelId.trim();
|
const trimmed = modelId.trim();
|
||||||
if (!trimmed) return trimmed;
|
if (!trimmed) return trimmed;
|
||||||
const direct = backend.modelAliases?.[trimmed];
|
const direct = backend.modelAliases?.[trimmed];
|
||||||
@@ -191,19 +170,14 @@ export function normalizeCliModel(
|
|||||||
|
|
||||||
function toUsage(raw: Record<string, unknown>): CliUsage | undefined {
|
function toUsage(raw: Record<string, unknown>): CliUsage | undefined {
|
||||||
const pick = (key: string) =>
|
const pick = (key: string) =>
|
||||||
typeof raw[key] === "number" && raw[key] > 0
|
typeof raw[key] === "number" && raw[key] > 0 ? (raw[key] as number) : undefined;
|
||||||
? (raw[key] as number)
|
|
||||||
: undefined;
|
|
||||||
const input = pick("input_tokens") ?? pick("inputTokens");
|
const input = pick("input_tokens") ?? pick("inputTokens");
|
||||||
const output = pick("output_tokens") ?? pick("outputTokens");
|
const output = pick("output_tokens") ?? pick("outputTokens");
|
||||||
const cacheRead =
|
const cacheRead =
|
||||||
pick("cache_read_input_tokens") ??
|
pick("cache_read_input_tokens") ?? pick("cached_input_tokens") ?? pick("cacheRead");
|
||||||
pick("cached_input_tokens") ??
|
|
||||||
pick("cacheRead");
|
|
||||||
const cacheWrite = pick("cache_write_input_tokens") ?? pick("cacheWrite");
|
const cacheWrite = pick("cache_write_input_tokens") ?? pick("cacheWrite");
|
||||||
const total = pick("total_tokens") ?? pick("total");
|
const total = pick("total_tokens") ?? pick("total");
|
||||||
if (!input && !output && !cacheRead && !cacheWrite && !total)
|
if (!input && !output && !cacheRead && !cacheWrite && !total) return undefined;
|
||||||
return undefined;
|
|
||||||
return { input, output, cacheRead, cacheWrite, total };
|
return { input, output, cacheRead, cacheWrite, total };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,8 +188,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
function collectText(value: unknown): string {
|
function collectText(value: unknown): string {
|
||||||
if (!value) return "";
|
if (!value) return "";
|
||||||
if (typeof value === "string") return value;
|
if (typeof value === "string") return value;
|
||||||
if (Array.isArray(value))
|
if (Array.isArray(value)) return value.map((entry) => collectText(entry)).join("");
|
||||||
return value.map((entry) => collectText(entry)).join("");
|
|
||||||
if (!isRecord(value)) return "";
|
if (!isRecord(value)) return "";
|
||||||
if (typeof value.text === "string") return value.text;
|
if (typeof value.text === "string") return value.text;
|
||||||
if (typeof value.content === "string") return value.content;
|
if (typeof value.content === "string") return value.content;
|
||||||
@@ -242,10 +215,7 @@ function pickSessionId(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCliJson(
|
export function parseCliJson(raw: string, backend: CliBackendConfig): CliOutput | null {
|
||||||
raw: string,
|
|
||||||
backend: CliBackendConfig,
|
|
||||||
): CliOutput | null {
|
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
let parsed: unknown;
|
let parsed: unknown;
|
||||||
@@ -265,10 +235,7 @@ export function parseCliJson(
|
|||||||
return { text: text.trim(), sessionId, usage };
|
return { text: text.trim(), sessionId, usage };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCliJsonl(
|
export function parseCliJsonl(raw: string, backend: CliBackendConfig): CliOutput | null {
|
||||||
raw: string,
|
|
||||||
backend: CliBackendConfig,
|
|
||||||
): CliOutput | null {
|
|
||||||
const lines = raw
|
const lines = raw
|
||||||
.split(/\r?\n/g)
|
.split(/\r?\n/g)
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
@@ -331,18 +298,15 @@ export function resolveSessionIdToSend(params: {
|
|||||||
return { sessionId: crypto.randomUUID(), isNew: true };
|
return { sessionId: crypto.randomUUID(), isNew: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolvePromptInput(params: {
|
export function resolvePromptInput(params: { backend: CliBackendConfig; prompt: string }): {
|
||||||
backend: CliBackendConfig;
|
argsPrompt?: string;
|
||||||
prompt: string;
|
stdin?: string;
|
||||||
}): { argsPrompt?: string; stdin?: string } {
|
} {
|
||||||
const inputMode = params.backend.input ?? "arg";
|
const inputMode = params.backend.input ?? "arg";
|
||||||
if (inputMode === "stdin") {
|
if (inputMode === "stdin") {
|
||||||
return { stdin: params.prompt };
|
return { stdin: params.prompt };
|
||||||
}
|
}
|
||||||
if (
|
if (params.backend.maxPromptArgChars && params.prompt.length > params.backend.maxPromptArgChars) {
|
||||||
params.backend.maxPromptArgChars &&
|
|
||||||
params.prompt.length > params.backend.maxPromptArgChars
|
|
||||||
) {
|
|
||||||
return { stdin: params.prompt };
|
return { stdin: params.prompt };
|
||||||
}
|
}
|
||||||
return { argsPrompt: params.prompt };
|
return { argsPrompt: params.prompt };
|
||||||
@@ -357,10 +321,7 @@ function resolveImageExtension(mimeType: string): string {
|
|||||||
return "bin";
|
return "bin";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appendImagePathsToPrompt(
|
export function appendImagePathsToPrompt(prompt: string, paths: string[]): string {
|
||||||
prompt: string,
|
|
||||||
paths: string[],
|
|
||||||
): string {
|
|
||||||
if (!paths.length) return prompt;
|
if (!paths.length) return prompt;
|
||||||
const trimmed = prompt.trimEnd();
|
const trimmed = prompt.trimEnd();
|
||||||
const separator = trimmed ? "\n\n" : "";
|
const separator = trimmed ? "\n\n" : "";
|
||||||
@@ -370,9 +331,7 @@ export function appendImagePathsToPrompt(
|
|||||||
export async function writeCliImages(
|
export async function writeCliImages(
|
||||||
images: ImageContent[],
|
images: ImageContent[],
|
||||||
): Promise<{ paths: string[]; cleanup: () => Promise<void> }> {
|
): Promise<{ paths: string[]; cleanup: () => Promise<void> }> {
|
||||||
const tempDir = await fs.mkdtemp(
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-cli-images-"));
|
||||||
path.join(os.tmpdir(), "clawdbot-cli-images-"),
|
|
||||||
);
|
|
||||||
const paths: string[] = [];
|
const paths: string[] = [];
|
||||||
for (let i = 0; i < images.length; i += 1) {
|
for (let i = 0; i < images.length; i += 1) {
|
||||||
const image = images[i];
|
const image = images[i];
|
||||||
@@ -402,11 +361,7 @@ export function buildCliArgs(params: {
|
|||||||
if (!params.useResume && params.backend.modelArg && params.modelId) {
|
if (!params.useResume && params.backend.modelArg && params.modelId) {
|
||||||
args.push(params.backend.modelArg, params.modelId);
|
args.push(params.backend.modelArg, params.modelId);
|
||||||
}
|
}
|
||||||
if (
|
if (!params.useResume && params.systemPrompt && params.backend.systemPromptArg) {
|
||||||
!params.useResume &&
|
|
||||||
params.systemPrompt &&
|
|
||||||
params.backend.systemPromptArg
|
|
||||||
) {
|
|
||||||
args.push(params.backend.systemPromptArg, params.systemPrompt);
|
args.push(params.backend.systemPromptArg, params.systemPrompt);
|
||||||
}
|
}
|
||||||
if (!params.useResume && params.sessionId) {
|
if (!params.useResume && params.sessionId) {
|
||||||
|
|||||||
@@ -16,11 +16,7 @@ export function getCliSessionId(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setCliSessionId(
|
export function setCliSessionId(entry: SessionEntry, provider: string, sessionId: string): void {
|
||||||
entry: SessionEntry,
|
|
||||||
provider: string,
|
|
||||||
sessionId: string,
|
|
||||||
): void {
|
|
||||||
const normalized = normalizeProviderId(provider);
|
const normalized = normalizeProviderId(provider);
|
||||||
const trimmed = sessionId.trim();
|
const trimmed = sessionId.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
|
|||||||
@@ -3,11 +3,7 @@ import type { ClawdbotConfig } from "../config/config.js";
|
|||||||
export const CONTEXT_WINDOW_HARD_MIN_TOKENS = 16_000;
|
export const CONTEXT_WINDOW_HARD_MIN_TOKENS = 16_000;
|
||||||
export const CONTEXT_WINDOW_WARN_BELOW_TOKENS = 32_000;
|
export const CONTEXT_WINDOW_WARN_BELOW_TOKENS = 32_000;
|
||||||
|
|
||||||
export type ContextWindowSource =
|
export type ContextWindowSource = "model" | "modelsConfig" | "agentContextTokens" | "default";
|
||||||
| "model"
|
|
||||||
| "modelsConfig"
|
|
||||||
| "agentContextTokens"
|
|
||||||
| "default";
|
|
||||||
|
|
||||||
export type ContextWindowInfo = {
|
export type ContextWindowInfo = {
|
||||||
tokens: number;
|
tokens: number;
|
||||||
@@ -32,26 +28,17 @@ export function resolveContextWindowInfo(params: {
|
|||||||
|
|
||||||
const fromModelsConfig = (() => {
|
const fromModelsConfig = (() => {
|
||||||
const providers = params.cfg?.models?.providers as
|
const providers = params.cfg?.models?.providers as
|
||||||
| Record<
|
| Record<string, { models?: Array<{ id?: string; contextWindow?: number }> }>
|
||||||
string,
|
|
||||||
{ models?: Array<{ id?: string; contextWindow?: number }> }
|
|
||||||
>
|
|
||||||
| undefined;
|
| undefined;
|
||||||
const providerEntry = providers?.[params.provider];
|
const providerEntry = providers?.[params.provider];
|
||||||
const models = Array.isArray(providerEntry?.models)
|
const models = Array.isArray(providerEntry?.models) ? providerEntry.models : [];
|
||||||
? providerEntry.models
|
|
||||||
: [];
|
|
||||||
const match = models.find((m) => m?.id === params.modelId);
|
const match = models.find((m) => m?.id === params.modelId);
|
||||||
return normalizePositiveInt(match?.contextWindow);
|
return normalizePositiveInt(match?.contextWindow);
|
||||||
})();
|
})();
|
||||||
if (fromModelsConfig)
|
if (fromModelsConfig) return { tokens: fromModelsConfig, source: "modelsConfig" };
|
||||||
return { tokens: fromModelsConfig, source: "modelsConfig" };
|
|
||||||
|
|
||||||
const fromAgentConfig = normalizePositiveInt(
|
const fromAgentConfig = normalizePositiveInt(params.cfg?.agents?.defaults?.contextTokens);
|
||||||
params.cfg?.agents?.defaults?.contextTokens,
|
if (fromAgentConfig) return { tokens: fromAgentConfig, source: "agentContextTokens" };
|
||||||
);
|
|
||||||
if (fromAgentConfig)
|
|
||||||
return { tokens: fromAgentConfig, source: "agentContextTokens" };
|
|
||||||
|
|
||||||
return { tokens: Math.floor(params.defaultTokens), source: "default" };
|
return { tokens: Math.floor(params.defaultTokens), source: "default" };
|
||||||
}
|
}
|
||||||
@@ -70,10 +57,7 @@ export function evaluateContextWindowGuard(params: {
|
|||||||
1,
|
1,
|
||||||
Math.floor(params.warnBelowTokens ?? CONTEXT_WINDOW_WARN_BELOW_TOKENS),
|
Math.floor(params.warnBelowTokens ?? CONTEXT_WINDOW_WARN_BELOW_TOKENS),
|
||||||
);
|
);
|
||||||
const hardMin = Math.max(
|
const hardMin = Math.max(1, Math.floor(params.hardMinTokens ?? CONTEXT_WINDOW_HARD_MIN_TOKENS));
|
||||||
1,
|
|
||||||
Math.floor(params.hardMinTokens ?? CONTEXT_WINDOW_HARD_MIN_TOKENS),
|
|
||||||
);
|
|
||||||
const tokens = Math.max(0, Math.floor(params.info.tokens));
|
const tokens = Math.max(0, Math.floor(params.info.tokens));
|
||||||
return {
|
return {
|
||||||
...params.info,
|
...params.info,
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ type ModelEntry = { id: string; contextWindow?: number };
|
|||||||
const MODEL_CACHE = new Map<string, number>();
|
const MODEL_CACHE = new Map<string, number>();
|
||||||
const loadPromise = (async () => {
|
const loadPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
const { discoverAuthStorage, discoverModels } = await import(
|
const { discoverAuthStorage, discoverModels } = await import("@mariozechner/pi-coding-agent");
|
||||||
"@mariozechner/pi-coding-agent"
|
|
||||||
);
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
await ensureClawdbotModelsJson(cfg);
|
await ensureClawdbotModelsJson(cfg);
|
||||||
const agentDir = resolveClawdbotAgentDir();
|
const agentDir = resolveClawdbotAgentDir();
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import {
|
|||||||
describe("failover-error", () => {
|
describe("failover-error", () => {
|
||||||
it("infers failover reason from HTTP status", () => {
|
it("infers failover reason from HTTP status", () => {
|
||||||
expect(resolveFailoverReasonFromError({ status: 402 })).toBe("billing");
|
expect(resolveFailoverReasonFromError({ status: 402 })).toBe("billing");
|
||||||
expect(resolveFailoverReasonFromError({ statusCode: "429" })).toBe(
|
expect(resolveFailoverReasonFromError({ statusCode: "429" })).toBe("rate_limit");
|
||||||
"rate_limit",
|
|
||||||
);
|
|
||||||
expect(resolveFailoverReasonFromError({ status: 403 })).toBe("auth");
|
expect(resolveFailoverReasonFromError({ status: 403 })).toBe("auth");
|
||||||
expect(resolveFailoverReasonFromError({ status: 408 })).toBe("timeout");
|
expect(resolveFailoverReasonFromError({ status: 408 })).toBe("timeout");
|
||||||
});
|
});
|
||||||
@@ -24,12 +22,8 @@ describe("failover-error", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("infers timeout from common node error codes", () => {
|
it("infers timeout from common node error codes", () => {
|
||||||
expect(resolveFailoverReasonFromError({ code: "ETIMEDOUT" })).toBe(
|
expect(resolveFailoverReasonFromError({ code: "ETIMEDOUT" })).toBe("timeout");
|
||||||
"timeout",
|
expect(resolveFailoverReasonFromError({ code: "ECONNRESET" })).toBe("timeout");
|
||||||
);
|
|
||||||
expect(resolveFailoverReasonFromError({ code: "ECONNRESET" })).toBe(
|
|
||||||
"timeout",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("coerces failover-worthy errors into FailoverError with metadata", () => {
|
it("coerces failover-worthy errors into FailoverError with metadata", () => {
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { classifyFailoverReason, type FailoverReason } from "./pi-embedded-helpers.js";
|
||||||
classifyFailoverReason,
|
|
||||||
type FailoverReason,
|
|
||||||
} from "./pi-embedded-helpers.js";
|
|
||||||
|
|
||||||
export class FailoverError extends Error {
|
export class FailoverError extends Error {
|
||||||
readonly reason: FailoverReason;
|
readonly reason: FailoverReason;
|
||||||
@@ -38,9 +35,7 @@ export function isFailoverError(err: unknown): err is FailoverError {
|
|||||||
return err instanceof FailoverError;
|
return err instanceof FailoverError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveFailoverStatus(
|
export function resolveFailoverStatus(reason: FailoverReason): number | undefined {
|
||||||
reason: FailoverReason,
|
|
||||||
): number | undefined {
|
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case "billing":
|
case "billing":
|
||||||
return 402;
|
return 402;
|
||||||
@@ -80,11 +75,7 @@ function getErrorCode(err: unknown): string | undefined {
|
|||||||
function getErrorMessage(err: unknown): string {
|
function getErrorMessage(err: unknown): string {
|
||||||
if (err instanceof Error) return err.message;
|
if (err instanceof Error) return err.message;
|
||||||
if (typeof err === "string") return err;
|
if (typeof err === "string") return err;
|
||||||
if (
|
if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
|
||||||
typeof err === "number" ||
|
|
||||||
typeof err === "boolean" ||
|
|
||||||
typeof err === "bigint"
|
|
||||||
) {
|
|
||||||
return String(err);
|
return String(err);
|
||||||
}
|
}
|
||||||
if (typeof err === "symbol") return err.description ?? "";
|
if (typeof err === "symbol") return err.description ?? "";
|
||||||
@@ -95,9 +86,7 @@ function getErrorMessage(err: unknown): string {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveFailoverReasonFromError(
|
export function resolveFailoverReasonFromError(err: unknown): FailoverReason | null {
|
||||||
err: unknown,
|
|
||||||
): FailoverReason | null {
|
|
||||||
if (isFailoverError(err)) return err.reason;
|
if (isFailoverError(err)) return err.reason;
|
||||||
|
|
||||||
const status = getStatusCode(err);
|
const status = getStatusCode(err);
|
||||||
@@ -107,11 +96,7 @@ export function resolveFailoverReasonFromError(
|
|||||||
if (status === 408) return "timeout";
|
if (status === 408) return "timeout";
|
||||||
|
|
||||||
const code = (getErrorCode(err) ?? "").toUpperCase();
|
const code = (getErrorCode(err) ?? "").toUpperCase();
|
||||||
if (
|
if (["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "ECONNABORTED"].includes(code)) {
|
||||||
["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "ECONNABORTED"].includes(
|
|
||||||
code,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return "timeout";
|
return "timeout";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import type {
|
import type { ClawdbotConfig, HumanDelayConfig, IdentityConfig } from "../config/config.js";
|
||||||
ClawdbotConfig,
|
|
||||||
HumanDelayConfig,
|
|
||||||
IdentityConfig,
|
|
||||||
} from "../config/config.js";
|
|
||||||
import { resolveAgentConfig } from "./agent-scope.js";
|
import { resolveAgentConfig } from "./agent-scope.js";
|
||||||
|
|
||||||
const DEFAULT_ACK_REACTION = "👀";
|
const DEFAULT_ACK_REACTION = "👀";
|
||||||
@@ -14,10 +10,7 @@ export function resolveAgentIdentity(
|
|||||||
return resolveAgentConfig(cfg, agentId)?.identity;
|
return resolveAgentConfig(cfg, agentId)?.identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveAckReaction(
|
export function resolveAckReaction(cfg: ClawdbotConfig, agentId: string): string {
|
||||||
cfg: ClawdbotConfig,
|
|
||||||
agentId: string,
|
|
||||||
): string {
|
|
||||||
const configured = cfg.messages?.ackReaction;
|
const configured = cfg.messages?.ackReaction;
|
||||||
if (configured !== undefined) return configured.trim();
|
if (configured !== undefined) return configured.trim();
|
||||||
const emoji = resolveAgentIdentity(cfg, agentId)?.emoji?.trim();
|
const emoji = resolveAgentIdentity(cfg, agentId)?.emoji?.trim();
|
||||||
@@ -44,15 +37,10 @@ export function resolveMessagePrefix(
|
|||||||
const hasAllowFrom = opts?.hasAllowFrom === true;
|
const hasAllowFrom = opts?.hasAllowFrom === true;
|
||||||
if (hasAllowFrom) return "";
|
if (hasAllowFrom) return "";
|
||||||
|
|
||||||
return (
|
return resolveIdentityNamePrefix(cfg, agentId) ?? opts?.fallback ?? "[clawdbot]";
|
||||||
resolveIdentityNamePrefix(cfg, agentId) ?? opts?.fallback ?? "[clawdbot]"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveResponsePrefix(
|
export function resolveResponsePrefix(cfg: ClawdbotConfig, agentId: string): string | undefined {
|
||||||
cfg: ClawdbotConfig,
|
|
||||||
agentId: string,
|
|
||||||
): string | undefined {
|
|
||||||
const configured = cfg.messages?.responsePrefix;
|
const configured = cfg.messages?.responsePrefix;
|
||||||
if (configured !== undefined) {
|
if (configured !== undefined) {
|
||||||
if (configured === "auto") {
|
if (configured === "auto") {
|
||||||
|
|||||||
@@ -3,11 +3,7 @@ export type ModelRef = {
|
|||||||
id?: string | null;
|
id?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ANTHROPIC_PREFIXES = [
|
const ANTHROPIC_PREFIXES = ["claude-opus-4-5", "claude-sonnet-4-5", "claude-haiku-4-5"];
|
||||||
"claude-opus-4-5",
|
|
||||||
"claude-sonnet-4-5",
|
|
||||||
"claude-haiku-4-5",
|
|
||||||
];
|
|
||||||
const OPENAI_MODELS = ["gpt-5.2", "gpt-5.0"];
|
const OPENAI_MODELS = ["gpt-5.2", "gpt-5.0"];
|
||||||
const CODEX_MODELS = [
|
const CODEX_MODELS = [
|
||||||
"gpt-5.2",
|
"gpt-5.2",
|
||||||
@@ -55,10 +51,7 @@ export function isModernModelRef(ref: ModelRef): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (provider === "google-antigravity") {
|
if (provider === "google-antigravity") {
|
||||||
return (
|
return matchesPrefix(id, GOOGLE_PREFIXES) || matchesPrefix(id, ANTHROPIC_PREFIXES);
|
||||||
matchesPrefix(id, GOOGLE_PREFIXES) ||
|
|
||||||
matchesPrefix(id, ANTHROPIC_PREFIXES)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider === "zai") {
|
if (provider === "zai") {
|
||||||
|
|||||||
@@ -52,9 +52,7 @@ function resolveStorePath(agentId: string, raw?: string): string {
|
|||||||
const stateDir = resolveStateDir(process.env, os.homedir);
|
const stateDir = resolveStateDir(process.env, os.homedir);
|
||||||
const fallback = path.join(stateDir, "memory", `${agentId}.sqlite`);
|
const fallback = path.join(stateDir, "memory", `${agentId}.sqlite`);
|
||||||
if (!raw) return fallback;
|
if (!raw) return fallback;
|
||||||
const withToken = raw.includes("{agentId}")
|
const withToken = raw.includes("{agentId}") ? raw.replaceAll("{agentId}", agentId) : raw;
|
||||||
? raw.replaceAll("{agentId}", agentId)
|
|
||||||
: raw;
|
|
||||||
return resolveUserPath(withToken);
|
return resolveUserPath(withToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,47 +75,29 @@ function mergeConfig(
|
|||||||
const model = overrides?.model ?? defaults?.model ?? DEFAULT_MODEL;
|
const model = overrides?.model ?? defaults?.model ?? DEFAULT_MODEL;
|
||||||
const local = {
|
const local = {
|
||||||
modelPath: overrides?.local?.modelPath ?? defaults?.local?.modelPath,
|
modelPath: overrides?.local?.modelPath ?? defaults?.local?.modelPath,
|
||||||
modelCacheDir:
|
modelCacheDir: overrides?.local?.modelCacheDir ?? defaults?.local?.modelCacheDir,
|
||||||
overrides?.local?.modelCacheDir ?? defaults?.local?.modelCacheDir,
|
|
||||||
};
|
};
|
||||||
const store = {
|
const store = {
|
||||||
driver: overrides?.store?.driver ?? defaults?.store?.driver ?? "sqlite",
|
driver: overrides?.store?.driver ?? defaults?.store?.driver ?? "sqlite",
|
||||||
path: resolveStorePath(
|
path: resolveStorePath(agentId, overrides?.store?.path ?? defaults?.store?.path),
|
||||||
agentId,
|
|
||||||
overrides?.store?.path ?? defaults?.store?.path,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
const chunking = {
|
const chunking = {
|
||||||
tokens:
|
tokens: overrides?.chunking?.tokens ?? defaults?.chunking?.tokens ?? DEFAULT_CHUNK_TOKENS,
|
||||||
overrides?.chunking?.tokens ??
|
overlap: overrides?.chunking?.overlap ?? defaults?.chunking?.overlap ?? DEFAULT_CHUNK_OVERLAP,
|
||||||
defaults?.chunking?.tokens ??
|
|
||||||
DEFAULT_CHUNK_TOKENS,
|
|
||||||
overlap:
|
|
||||||
overrides?.chunking?.overlap ??
|
|
||||||
defaults?.chunking?.overlap ??
|
|
||||||
DEFAULT_CHUNK_OVERLAP,
|
|
||||||
};
|
};
|
||||||
const sync = {
|
const sync = {
|
||||||
onSessionStart:
|
onSessionStart: overrides?.sync?.onSessionStart ?? defaults?.sync?.onSessionStart ?? true,
|
||||||
overrides?.sync?.onSessionStart ?? defaults?.sync?.onSessionStart ?? true,
|
|
||||||
onSearch: overrides?.sync?.onSearch ?? defaults?.sync?.onSearch ?? true,
|
onSearch: overrides?.sync?.onSearch ?? defaults?.sync?.onSearch ?? true,
|
||||||
watch: overrides?.sync?.watch ?? defaults?.sync?.watch ?? true,
|
watch: overrides?.sync?.watch ?? defaults?.sync?.watch ?? true,
|
||||||
watchDebounceMs:
|
watchDebounceMs:
|
||||||
overrides?.sync?.watchDebounceMs ??
|
overrides?.sync?.watchDebounceMs ??
|
||||||
defaults?.sync?.watchDebounceMs ??
|
defaults?.sync?.watchDebounceMs ??
|
||||||
DEFAULT_WATCH_DEBOUNCE_MS,
|
DEFAULT_WATCH_DEBOUNCE_MS,
|
||||||
intervalMinutes:
|
intervalMinutes: overrides?.sync?.intervalMinutes ?? defaults?.sync?.intervalMinutes ?? 0,
|
||||||
overrides?.sync?.intervalMinutes ?? defaults?.sync?.intervalMinutes ?? 0,
|
|
||||||
};
|
};
|
||||||
const query = {
|
const query = {
|
||||||
maxResults:
|
maxResults: overrides?.query?.maxResults ?? defaults?.query?.maxResults ?? DEFAULT_MAX_RESULTS,
|
||||||
overrides?.query?.maxResults ??
|
minScore: overrides?.query?.minScore ?? defaults?.query?.minScore ?? DEFAULT_MIN_SCORE,
|
||||||
defaults?.query?.maxResults ??
|
|
||||||
DEFAULT_MAX_RESULTS,
|
|
||||||
minScore:
|
|
||||||
overrides?.query?.minScore ??
|
|
||||||
defaults?.query?.minScore ??
|
|
||||||
DEFAULT_MIN_SCORE,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const overlap = Math.max(0, Math.min(chunking.overlap, chunking.tokens - 1));
|
const overlap = Math.max(0, Math.min(chunking.overlap, chunking.tokens - 1));
|
||||||
|
|||||||
@@ -51,9 +51,7 @@ export async function minimaxUnderstandImage(params: {
|
|||||||
const imageDataUrl = params.imageDataUrl.trim();
|
const imageDataUrl = params.imageDataUrl.trim();
|
||||||
if (!imageDataUrl) throw new Error("MiniMax VLM: imageDataUrl required");
|
if (!imageDataUrl) throw new Error("MiniMax VLM: imageDataUrl required");
|
||||||
if (!/^data:image\/(png|jpeg|webp);base64,/i.test(imageDataUrl)) {
|
if (!/^data:image\/(png|jpeg|webp);base64,/i.test(imageDataUrl)) {
|
||||||
throw new Error(
|
throw new Error("MiniMax VLM: imageDataUrl must be a base64 data:image/(png|jpeg|webp) URL");
|
||||||
"MiniMax VLM: imageDataUrl must be a base64 data:image/(png|jpeg|webp) URL",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = coerceApiHost({
|
const host = coerceApiHost({
|
||||||
@@ -92,17 +90,12 @@ export async function minimaxUnderstandImage(params: {
|
|||||||
throw new Error(`MiniMax VLM response was not JSON.${trace}`);
|
throw new Error(`MiniMax VLM response was not JSON.${trace}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseResp = isRecord(json.base_resp)
|
const baseResp = isRecord(json.base_resp) ? (json.base_resp as MinimaxBaseResp) : {};
|
||||||
? (json.base_resp as MinimaxBaseResp)
|
const code = typeof baseResp.status_code === "number" ? baseResp.status_code : -1;
|
||||||
: {};
|
|
||||||
const code =
|
|
||||||
typeof baseResp.status_code === "number" ? baseResp.status_code : -1;
|
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
const msg = (baseResp.status_msg ?? "").trim();
|
const msg = (baseResp.status_msg ?? "").trim();
|
||||||
const trace = traceId ? ` Trace-Id: ${traceId}` : "";
|
const trace = traceId ? ` Trace-Id: ${traceId}` : "";
|
||||||
throw new Error(
|
throw new Error(`MiniMax VLM API error (${code})${msg ? `: ${msg}` : ""}.${trace}`);
|
||||||
`MiniMax VLM API error (${code})${msg ? `: ${msg}` : ""}.${trace}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = pickString(json, "content").trim();
|
const content = pickString(json, "content").trim();
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { completeSimple, type Model } from "@mariozechner/pi-ai";
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const MINIMAX_KEY = process.env.MINIMAX_API_KEY ?? "";
|
const MINIMAX_KEY = process.env.MINIMAX_API_KEY ?? "";
|
||||||
const MINIMAX_BASE_URL =
|
const MINIMAX_BASE_URL = process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/anthropic";
|
||||||
process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/anthropic";
|
|
||||||
const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "MiniMax-M2.1";
|
const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "MiniMax-M2.1";
|
||||||
const LIVE = process.env.MINIMAX_LIVE_TEST === "1" || process.env.LIVE === "1";
|
const LIVE = process.env.MINIMAX_LIVE_TEST === "1" || process.env.LIVE === "1";
|
||||||
|
|
||||||
|
|||||||
@@ -107,11 +107,7 @@ describe("getApiKeyForModel", () => {
|
|||||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempDir, "agent");
|
process.env.CLAWDBOT_AGENT_DIR = path.join(tempDir, "agent");
|
||||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||||
|
|
||||||
const authProfilesPath = path.join(
|
const authProfilesPath = path.join(tempDir, "agent", "auth-profiles.json");
|
||||||
tempDir,
|
|
||||||
"agent",
|
|
||||||
"auth-profiles.json",
|
|
||||||
);
|
|
||||||
await fs.mkdir(path.dirname(authProfilesPath), {
|
await fs.mkdir(path.dirname(authProfilesPath), {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
mode: 0o700,
|
mode: 0o700,
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ import {
|
|||||||
} from "./auth-profiles.js";
|
} from "./auth-profiles.js";
|
||||||
import { normalizeProviderId } from "./model-selection.js";
|
import { normalizeProviderId } from "./model-selection.js";
|
||||||
|
|
||||||
export {
|
export { ensureAuthProfileStore, resolveAuthProfileOrder } from "./auth-profiles.js";
|
||||||
ensureAuthProfileStore,
|
|
||||||
resolveAuthProfileOrder,
|
|
||||||
} from "./auth-profiles.js";
|
|
||||||
|
|
||||||
export function getCustomProviderApiKey(
|
export function getCustomProviderApiKey(
|
||||||
cfg: ClawdbotConfig | undefined,
|
cfg: ClawdbotConfig | undefined,
|
||||||
@@ -109,16 +106,12 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
|||||||
const pick = (envVar: string): EnvApiKeyResult | null => {
|
const pick = (envVar: string): EnvApiKeyResult | null => {
|
||||||
const value = process.env[envVar]?.trim();
|
const value = process.env[envVar]?.trim();
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
const source = applied.has(envVar)
|
const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`;
|
||||||
? `shell env: ${envVar}`
|
|
||||||
: `env: ${envVar}`;
|
|
||||||
return { apiKey: value, source };
|
return { apiKey: value, source };
|
||||||
};
|
};
|
||||||
|
|
||||||
if (normalized === "github-copilot") {
|
if (normalized === "github-copilot") {
|
||||||
return (
|
return pick("COPILOT_GITHUB_TOKEN") ?? pick("GH_TOKEN") ?? pick("GITHUB_TOKEN");
|
||||||
pick("COPILOT_GITHUB_TOKEN") ?? pick("GH_TOKEN") ?? pick("GITHUB_TOKEN")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized === "anthropic") {
|
if (normalized === "anthropic") {
|
||||||
|
|||||||
@@ -58,8 +58,7 @@ export async function loadModelCatalog(params?: {
|
|||||||
typeof entry?.contextWindow === "number" && entry.contextWindow > 0
|
typeof entry?.contextWindow === "number" && entry.contextWindow > 0
|
||||||
? entry.contextWindow
|
? entry.contextWindow
|
||||||
: undefined;
|
: undefined;
|
||||||
const reasoning =
|
const reasoning = typeof entry?.reasoning === "boolean" ? entry.reasoning : undefined;
|
||||||
typeof entry?.reasoning === "boolean" ? entry.reasoning : undefined;
|
|
||||||
models.push({ id, name, provider, contextWindow, reasoning });
|
models.push({ id, name, provider, contextWindow, reasoning });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ function makeCfg(overrides: Partial<ClawdbotConfig> = {}): ClawdbotConfig {
|
|||||||
describe("runWithModelFallback", () => {
|
describe("runWithModelFallback", () => {
|
||||||
it("does not fall back on non-auth errors", async () => {
|
it("does not fall back on non-auth errors", async () => {
|
||||||
const cfg = makeCfg();
|
const cfg = makeCfg();
|
||||||
const run = vi
|
const run = vi.fn().mockRejectedValueOnce(new Error("bad request")).mockResolvedValueOnce("ok");
|
||||||
.fn()
|
|
||||||
.mockRejectedValueOnce(new Error("bad request"))
|
|
||||||
.mockResolvedValueOnce("ok");
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
runWithModelFallback({
|
runWithModelFallback({
|
||||||
@@ -60,9 +57,7 @@ describe("runWithModelFallback", () => {
|
|||||||
const cfg = makeCfg();
|
const cfg = makeCfg();
|
||||||
const run = vi
|
const run = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockRejectedValueOnce(
|
.mockRejectedValueOnce(Object.assign(new Error("payment required"), { status: 402 }))
|
||||||
Object.assign(new Error("payment required"), { status: 402 }),
|
|
||||||
)
|
|
||||||
.mockResolvedValueOnce("ok");
|
.mockResolvedValueOnce("ok");
|
||||||
|
|
||||||
const result = await runWithModelFallback({
|
const result = await runWithModelFallback({
|
||||||
@@ -106,9 +101,7 @@ describe("runWithModelFallback", () => {
|
|||||||
const cfg = makeCfg();
|
const cfg = makeCfg();
|
||||||
const run = vi
|
const run = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockRejectedValueOnce(
|
.mockRejectedValueOnce(new Error('No credentials found for profile "anthropic:claude-cli".'))
|
||||||
new Error('No credentials found for profile "anthropic:claude-cli".'),
|
|
||||||
)
|
|
||||||
.mockResolvedValueOnce("ok");
|
.mockResolvedValueOnce("ok");
|
||||||
|
|
||||||
const result = await runWithModelFallback({
|
const result = await runWithModelFallback({
|
||||||
@@ -136,9 +129,7 @@ describe("runWithModelFallback", () => {
|
|||||||
});
|
});
|
||||||
const run = vi
|
const run = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementation(() =>
|
.mockImplementation(() => Promise.reject(Object.assign(new Error("nope"), { status: 401 })));
|
||||||
Promise.reject(Object.assign(new Error("nope"), { status: 401 })),
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
runWithModelFallback({
|
runWithModelFallback({
|
||||||
@@ -219,9 +210,7 @@ describe("runWithModelFallback", () => {
|
|||||||
}),
|
}),
|
||||||
).rejects.toThrow("primary failed");
|
).rejects.toThrow("primary failed");
|
||||||
|
|
||||||
expect(calls).toEqual([
|
expect(calls).toEqual([{ provider: "anthropic", model: "claude-opus-4-5" }]);
|
||||||
{ provider: "anthropic", model: "claude-opus-4-5" },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back on missing API key errors", async () => {
|
it("falls back on missing API key errors", async () => {
|
||||||
@@ -277,9 +266,7 @@ describe("runWithModelFallback", () => {
|
|||||||
});
|
});
|
||||||
const run = vi
|
const run = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockRejectedValueOnce(
|
.mockRejectedValueOnce(Object.assign(new Error("timeout"), { code: "ETIMEDOUT" }))
|
||||||
Object.assign(new Error("timeout"), { code: "ETIMEDOUT" }),
|
|
||||||
)
|
|
||||||
.mockResolvedValueOnce("ok");
|
.mockResolvedValueOnce("ok");
|
||||||
|
|
||||||
const result = await runWithModelFallback({
|
const result = await runWithModelFallback({
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
||||||
import {
|
import { coerceToFailoverError, describeFailoverError, isFailoverError } from "./failover-error.js";
|
||||||
coerceToFailoverError,
|
|
||||||
describeFailoverError,
|
|
||||||
isFailoverError,
|
|
||||||
} from "./failover-error.js";
|
|
||||||
import {
|
import {
|
||||||
buildModelAliasIndex,
|
buildModelAliasIndex,
|
||||||
modelKey,
|
modelKey,
|
||||||
@@ -33,9 +29,7 @@ function isAbortError(err: unknown): boolean {
|
|||||||
const name = "name" in err ? String(err.name) : "";
|
const name = "name" in err ? String(err.name) : "";
|
||||||
if (name === "AbortError") return true;
|
if (name === "AbortError") return true;
|
||||||
const message =
|
const message =
|
||||||
"message" in err && typeof err.message === "string"
|
"message" in err && typeof err.message === "string" ? err.message.toLowerCase() : "";
|
||||||
? err.message.toLowerCase()
|
|
||||||
: "";
|
|
||||||
return message.includes("aborted");
|
return message.includes("aborted");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,10 +64,7 @@ function resolveImageFallbackCandidates(params: {
|
|||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const candidates: ModelCandidate[] = [];
|
const candidates: ModelCandidate[] = [];
|
||||||
|
|
||||||
const addCandidate = (
|
const addCandidate = (candidate: ModelCandidate, enforceAllowlist: boolean) => {
|
||||||
candidate: ModelCandidate,
|
|
||||||
enforceAllowlist: boolean,
|
|
||||||
) => {
|
|
||||||
if (!candidate.provider || !candidate.model) return;
|
if (!candidate.provider || !candidate.model) return;
|
||||||
const key = modelKey(candidate.provider, candidate.model);
|
const key = modelKey(candidate.provider, candidate.model);
|
||||||
if (seen.has(key)) return;
|
if (seen.has(key)) return;
|
||||||
@@ -99,8 +90,7 @@ function resolveImageFallbackCandidates(params: {
|
|||||||
| { primary?: string }
|
| { primary?: string }
|
||||||
| string
|
| string
|
||||||
| undefined;
|
| undefined;
|
||||||
const primary =
|
const primary = typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary;
|
||||||
typeof imageModel === "string" ? imageModel.trim() : imageModel?.primary;
|
|
||||||
if (primary?.trim()) addRaw(primary, false);
|
if (primary?.trim()) addRaw(primary, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,10 +136,7 @@ function resolveFallbackCandidates(params: {
|
|||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const candidates: ModelCandidate[] = [];
|
const candidates: ModelCandidate[] = [];
|
||||||
|
|
||||||
const addCandidate = (
|
const addCandidate = (candidate: ModelCandidate, enforceAllowlist: boolean) => {
|
||||||
candidate: ModelCandidate,
|
|
||||||
enforceAllowlist: boolean,
|
|
||||||
) => {
|
|
||||||
if (!candidate.provider || !candidate.model) return;
|
if (!candidate.provider || !candidate.model) return;
|
||||||
const key = modelKey(candidate.provider, candidate.model);
|
const key = modelKey(candidate.provider, candidate.model);
|
||||||
if (seen.has(key)) return;
|
if (seen.has(key)) return;
|
||||||
@@ -180,11 +167,7 @@ function resolveFallbackCandidates(params: {
|
|||||||
addCandidate(resolved.ref, true);
|
addCandidate(resolved.ref, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (params.fallbacksOverride === undefined && primary?.provider && primary.model) {
|
||||||
params.fallbacksOverride === undefined &&
|
|
||||||
primary?.provider &&
|
|
||||||
primary.model
|
|
||||||
) {
|
|
||||||
addCandidate({ provider: primary.provider, model: primary.model }, false);
|
addCandidate({ provider: primary.provider, model: primary.model }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,10 +254,9 @@ export async function runWithModelFallback<T>(params: {
|
|||||||
)
|
)
|
||||||
.join(" | ")
|
.join(" | ")
|
||||||
: "unknown";
|
: "unknown";
|
||||||
throw new Error(
|
throw new Error(`All models failed (${attempts.length || candidates.length}): ${summary}`, {
|
||||||
`All models failed (${attempts.length || candidates.length}): ${summary}`,
|
cause: lastError instanceof Error ? lastError : undefined,
|
||||||
{ cause: lastError instanceof Error ? lastError : undefined },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runWithImageModelFallback<T>(params: {
|
export async function runWithImageModelFallback<T>(params: {
|
||||||
@@ -340,14 +322,10 @@ export async function runWithImageModelFallback<T>(params: {
|
|||||||
const summary =
|
const summary =
|
||||||
attempts.length > 0
|
attempts.length > 0
|
||||||
? attempts
|
? attempts
|
||||||
.map(
|
.map((attempt) => `${attempt.provider}/${attempt.model}: ${attempt.error}`)
|
||||||
(attempt) =>
|
|
||||||
`${attempt.provider}/${attempt.model}: ${attempt.error}`,
|
|
||||||
)
|
|
||||||
.join(" | ")
|
.join(" | ")
|
||||||
: "unknown";
|
: "unknown";
|
||||||
throw new Error(
|
throw new Error(`All image models failed (${attempts.length || candidates.length}): ${summary}`, {
|
||||||
`All image models failed (${attempts.length || candidates.length}): ${summary}`,
|
cause: lastError instanceof Error ? lastError : undefined,
|
||||||
{ cause: lastError instanceof Error ? lastError : undefined },
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,11 +79,7 @@ export type OpenRouterScanOptions = {
|
|||||||
maxAgeDays?: number;
|
maxAgeDays?: number;
|
||||||
providerFilter?: string;
|
providerFilter?: string;
|
||||||
probe?: boolean;
|
probe?: boolean;
|
||||||
onProgress?: (update: {
|
onProgress?: (update: { phase: "catalog" | "probe"; completed: number; total: number }) => void;
|
||||||
phase: "catalog" | "probe";
|
|
||||||
completed: number;
|
|
||||||
total: number;
|
|
||||||
}) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type OpenAIModel = Model<"openai-completions">;
|
type OpenAIModel = Model<"openai-completions">;
|
||||||
@@ -97,9 +93,7 @@ function normalizeCreatedAtMs(value: unknown): number | null {
|
|||||||
|
|
||||||
function inferParamBFromIdOrName(text: string): number | null {
|
function inferParamBFromIdOrName(text: string): number | null {
|
||||||
const raw = text.toLowerCase();
|
const raw = text.toLowerCase();
|
||||||
const matches = raw.matchAll(
|
const matches = raw.matchAll(/(?:^|[^a-z0-9])[a-z]?(\d+(?:\.\d+)?)b(?:[^a-z0-9]|$)/g);
|
||||||
/(?:^|[^a-z0-9])[a-z]?(\d+(?:\.\d+)?)b(?:[^a-z0-9]|$)/g,
|
|
||||||
);
|
|
||||||
let best: number | null = null;
|
let best: number | null = null;
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const numRaw = match[1];
|
const numRaw = match[1];
|
||||||
@@ -169,9 +163,7 @@ async function withTimeout<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchOpenRouterModels(
|
async function fetchOpenRouterModels(fetchImpl: typeof fetch): Promise<OpenRouterModelMeta[]> {
|
||||||
fetchImpl: typeof fetch,
|
|
||||||
): Promise<OpenRouterModelMeta[]> {
|
|
||||||
const res = await fetchImpl(OPENROUTER_MODELS_URL, {
|
const res = await fetchImpl(OPENROUTER_MODELS_URL, {
|
||||||
headers: { Accept: "application/json" },
|
headers: { Accept: "application/json" },
|
||||||
});
|
});
|
||||||
@@ -187,21 +179,17 @@ async function fetchOpenRouterModels(
|
|||||||
const obj = entry as Record<string, unknown>;
|
const obj = entry as Record<string, unknown>;
|
||||||
const id = typeof obj.id === "string" ? obj.id.trim() : "";
|
const id = typeof obj.id === "string" ? obj.id.trim() : "";
|
||||||
if (!id) return null;
|
if (!id) return null;
|
||||||
const name =
|
const name = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id;
|
||||||
typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id;
|
|
||||||
|
|
||||||
const contextLength =
|
const contextLength =
|
||||||
typeof obj.context_length === "number" &&
|
typeof obj.context_length === "number" && Number.isFinite(obj.context_length)
|
||||||
Number.isFinite(obj.context_length)
|
|
||||||
? obj.context_length
|
? obj.context_length
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const maxCompletionTokens =
|
const maxCompletionTokens =
|
||||||
typeof obj.max_completion_tokens === "number" &&
|
typeof obj.max_completion_tokens === "number" && Number.isFinite(obj.max_completion_tokens)
|
||||||
Number.isFinite(obj.max_completion_tokens)
|
|
||||||
? obj.max_completion_tokens
|
? obj.max_completion_tokens
|
||||||
: typeof obj.max_output_tokens === "number" &&
|
: typeof obj.max_output_tokens === "number" && Number.isFinite(obj.max_output_tokens)
|
||||||
Number.isFinite(obj.max_output_tokens)
|
|
||||||
? obj.max_output_tokens
|
? obj.max_output_tokens
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -216,9 +204,7 @@ async function fetchOpenRouterModels(
|
|||||||
const supportsToolsMeta = supportedParameters.includes("tools");
|
const supportsToolsMeta = supportedParameters.includes("tools");
|
||||||
|
|
||||||
const modality =
|
const modality =
|
||||||
typeof obj.modality === "string" && obj.modality.trim()
|
typeof obj.modality === "string" && obj.modality.trim() ? obj.modality.trim() : null;
|
||||||
? obj.modality.trim()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const inferredParamB = inferParamBFromIdOrName(`${id} ${name}`);
|
const inferredParamB = inferParamBFromIdOrName(`${id} ${name}`);
|
||||||
const createdAtMs = normalizeCreatedAtMs(obj.created_at);
|
const createdAtMs = normalizeCreatedAtMs(obj.created_at);
|
||||||
@@ -268,9 +254,7 @@ async function probeTool(
|
|||||||
} satisfies OpenAICompletionsOptions),
|
} satisfies OpenAICompletionsOptions),
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasToolCall = message.content.some(
|
const hasToolCall = message.content.some((block) => block.type === "toolCall");
|
||||||
(block) => block.type === "toolCall",
|
|
||||||
);
|
|
||||||
if (!hasToolCall) {
|
if (!hasToolCall) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -361,9 +345,7 @@ async function mapWithConcurrency<T, R>(
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
||||||
Array.from({ length: Math.min(limit, items.length) }, () => worker()),
|
|
||||||
);
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,19 +356,11 @@ export async function scanOpenRouterModels(
|
|||||||
const probe = options.probe ?? true;
|
const probe = options.probe ?? true;
|
||||||
const apiKey = options.apiKey?.trim() || getEnvApiKey("openrouter") || "";
|
const apiKey = options.apiKey?.trim() || getEnvApiKey("openrouter") || "";
|
||||||
if (probe && !apiKey) {
|
if (probe && !apiKey) {
|
||||||
throw new Error(
|
throw new Error("Missing OpenRouter API key. Set OPENROUTER_API_KEY to run models scan.");
|
||||||
"Missing OpenRouter API key. Set OPENROUTER_API_KEY to run models scan.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeoutMs = Math.max(
|
const timeoutMs = Math.max(1, Math.floor(options.timeoutMs ?? DEFAULT_TIMEOUT_MS));
|
||||||
1,
|
const concurrency = Math.max(1, Math.floor(options.concurrency ?? DEFAULT_CONCURRENCY));
|
||||||
Math.floor(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
||||||
);
|
|
||||||
const concurrency = Math.max(
|
|
||||||
1,
|
|
||||||
Math.floor(options.concurrency ?? DEFAULT_CONCURRENCY),
|
|
||||||
);
|
|
||||||
const minParamB = Math.max(0, Math.floor(options.minParamB ?? 0));
|
const minParamB = Math.max(0, Math.floor(options.minParamB ?? 0));
|
||||||
const maxAgeDays = Math.max(0, Math.floor(options.maxAgeDays ?? 0));
|
const maxAgeDays = Math.max(0, Math.floor(options.maxAgeDays ?? 0));
|
||||||
const providerFilter = options.providerFilter?.trim().toLowerCase() ?? "";
|
const providerFilter = options.providerFilter?.trim().toLowerCase() ?? "";
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ describe("buildAllowedModelSet", () => {
|
|||||||
|
|
||||||
expect(allowed.allowAny).toBe(false);
|
expect(allowed.allowAny).toBe(false);
|
||||||
expect(allowed.allowedKeys.has(modelKey("openai", "gpt-4"))).toBe(true);
|
expect(allowed.allowedKeys.has(modelKey("openai", "gpt-4"))).toBe(true);
|
||||||
expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe(
|
expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe(true);
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes the default model when no allowlist is set", () => {
|
it("includes the default model when no allowlist is set", () => {
|
||||||
@@ -58,9 +56,7 @@ describe("buildAllowedModelSet", () => {
|
|||||||
|
|
||||||
expect(allowed.allowAny).toBe(true);
|
expect(allowed.allowAny).toBe(true);
|
||||||
expect(allowed.allowedKeys.has(modelKey("openai", "gpt-4"))).toBe(true);
|
expect(allowed.allowedKeys.has(modelKey("openai", "gpt-4"))).toBe(true);
|
||||||
expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe(
|
expect(allowed.allowedKeys.has(modelKey("claude-cli", "opus-4.5"))).toBe(true);
|
||||||
true,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows explicit custom providers from models.providers", () => {
|
it("allows explicit custom providers from models.providers", () => {
|
||||||
@@ -93,9 +89,7 @@ describe("buildAllowedModelSet", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(allowed.allowAny).toBe(false);
|
expect(allowed.allowAny).toBe(false);
|
||||||
expect(
|
expect(allowed.allowedKeys.has(modelKey("moonshot", "kimi-k2-0905-preview"))).toBe(true);
|
||||||
allowed.allowedKeys.has(modelKey("moonshot", "kimi-k2-0905-preview")),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,7 @@ export type ModelRef = {
|
|||||||
model: string;
|
model: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ThinkLevel =
|
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||||
| "off"
|
|
||||||
| "minimal"
|
|
||||||
| "low"
|
|
||||||
| "medium"
|
|
||||||
| "high"
|
|
||||||
| "xhigh";
|
|
||||||
|
|
||||||
export type ModelAliasIndex = {
|
export type ModelAliasIndex = {
|
||||||
byAlias: Map<string, { alias: string; ref: ModelRef }>;
|
byAlias: Map<string, { alias: string; ref: ModelRef }>;
|
||||||
@@ -40,9 +34,7 @@ export function isCliProvider(provider: string, cfg?: ClawdbotConfig): boolean {
|
|||||||
if (normalized === "claude-cli") return true;
|
if (normalized === "claude-cli") return true;
|
||||||
if (normalized === "codex-cli") return true;
|
if (normalized === "codex-cli") return true;
|
||||||
const backends = cfg?.agents?.defaults?.cliBackends ?? {};
|
const backends = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||||
return Object.keys(backends).some(
|
return Object.keys(backends).some((key) => normalizeProviderId(key) === normalized);
|
||||||
(key) => normalizeProviderId(key) === normalized,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeAnthropicModelId(model: string): string {
|
function normalizeAnthropicModelId(model: string): string {
|
||||||
@@ -60,10 +52,7 @@ function normalizeProviderModelId(provider: string, model: string): string {
|
|||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseModelRef(
|
export function parseModelRef(raw: string, defaultProvider: string): ModelRef | null {
|
||||||
raw: string,
|
|
||||||
defaultProvider: string,
|
|
||||||
): ModelRef | null {
|
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
const slash = trimmed.indexOf("/");
|
const slash = trimmed.indexOf("/");
|
||||||
@@ -91,9 +80,7 @@ export function buildModelAliasIndex(params: {
|
|||||||
for (const [keyRaw, entryRaw] of Object.entries(rawModels)) {
|
for (const [keyRaw, entryRaw] of Object.entries(rawModels)) {
|
||||||
const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider);
|
const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider);
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
const alias = String(
|
const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim();
|
||||||
(entryRaw as { alias?: string } | undefined)?.alias ?? "",
|
|
||||||
).trim();
|
|
||||||
if (!alias) continue;
|
if (!alias) continue;
|
||||||
const aliasKey = normalizeAliasKey(alias);
|
const aliasKey = normalizeAliasKey(alias);
|
||||||
byAlias.set(aliasKey, { alias, ref: parsed });
|
byAlias.set(aliasKey, { alias, ref: parsed });
|
||||||
@@ -131,10 +118,7 @@ export function resolveConfiguredModelRef(params: {
|
|||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
}): ModelRef {
|
}): ModelRef {
|
||||||
const rawModel = (() => {
|
const rawModel = (() => {
|
||||||
const raw = params.cfg.agents?.defaults?.model as
|
const raw = params.cfg.agents?.defaults?.model as { primary?: string } | string | undefined;
|
||||||
| { primary?: string }
|
|
||||||
| string
|
|
||||||
| undefined;
|
|
||||||
if (typeof raw === "string") return raw.trim();
|
if (typeof raw === "string") return raw.trim();
|
||||||
return raw?.primary?.trim() ?? "";
|
return raw?.primary?.trim() ?? "";
|
||||||
})();
|
})();
|
||||||
@@ -176,9 +160,7 @@ export function buildAllowedModelSet(params: {
|
|||||||
defaultModel && params.defaultProvider
|
defaultModel && params.defaultProvider
|
||||||
? modelKey(params.defaultProvider, defaultModel)
|
? modelKey(params.defaultProvider, defaultModel)
|
||||||
: undefined;
|
: undefined;
|
||||||
const catalogKeys = new Set(
|
const catalogKeys = new Set(params.catalog.map((entry) => modelKey(entry.provider, entry.id)));
|
||||||
params.catalog.map((entry) => modelKey(entry.provider, entry.id)),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (allowAny) {
|
if (allowAny) {
|
||||||
if (defaultKey) catalogKeys.add(defaultKey);
|
if (defaultKey) catalogKeys.add(defaultKey);
|
||||||
@@ -190,10 +172,7 @@ export function buildAllowedModelSet(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allowedKeys = new Set<string>();
|
const allowedKeys = new Set<string>();
|
||||||
const configuredProviders = (params.cfg.models?.providers ?? {}) as Record<
|
const configuredProviders = (params.cfg.models?.providers ?? {}) as Record<string, unknown>;
|
||||||
string,
|
|
||||||
unknown
|
|
||||||
>;
|
|
||||||
for (const raw of rawAllowlist) {
|
for (const raw of rawAllowlist) {
|
||||||
const parsed = parseModelRef(String(raw), params.defaultProvider);
|
const parsed = parseModelRef(String(raw), params.defaultProvider);
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
@@ -253,9 +232,7 @@ export function getModelRefStatus(params: {
|
|||||||
const key = modelKey(params.ref.provider, params.ref.model);
|
const key = modelKey(params.ref.provider, params.ref.model);
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
inCatalog: params.catalog.some(
|
inCatalog: params.catalog.some((entry) => modelKey(entry.provider, entry.id) === key),
|
||||||
(entry) => modelKey(entry.provider, entry.id) === key,
|
|
||||||
),
|
|
||||||
allowAny: allowed.allowAny,
|
allowAny: allowed.allowAny,
|
||||||
allowed: allowed.allowAny || allowed.allowedKeys.has(key),
|
allowed: allowed.allowAny || allowed.allowedKeys.has(key),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ describe("models-config", () => {
|
|||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
|
||||||
vi.doMock("../providers/github-copilot-token.js", () => ({
|
vi.doMock("../providers/github-copilot-token.js", () => ({
|
||||||
DEFAULT_COPILOT_API_BASE_URL:
|
DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com",
|
||||||
"https://api.individual.githubcopilot.com",
|
|
||||||
resolveCopilotApiToken: vi.fn().mockResolvedValue({
|
resolveCopilotApiToken: vi.fn().mockResolvedValue({
|
||||||
token: "copilot",
|
token: "copilot",
|
||||||
expiresAt: Date.now() + 60 * 60 * 1000,
|
expiresAt: Date.now() + 60 * 60 * 1000,
|
||||||
@@ -67,17 +66,12 @@ describe("models-config", () => {
|
|||||||
const agentDir = path.join(home, "agent-default-base-url");
|
const agentDir = path.join(home, "agent-default-base-url");
|
||||||
await ensureClawdbotModelsJson({ models: { providers: {} } }, agentDir);
|
await ensureClawdbotModelsJson({ models: { providers: {} } }, agentDir);
|
||||||
|
|
||||||
const raw = await fs.readFile(
|
const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8");
|
||||||
path.join(agentDir, "models.json"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
const parsed = JSON.parse(raw) as {
|
const parsed = JSON.parse(raw) as {
|
||||||
providers: Record<string, { baseUrl?: string; models?: unknown[] }>;
|
providers: Record<string, { baseUrl?: string; models?: unknown[] }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(parsed.providers["github-copilot"]?.baseUrl).toBe(
|
expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://api.copilot.example");
|
||||||
"https://api.copilot.example",
|
|
||||||
);
|
|
||||||
expect(parsed.providers["github-copilot"]?.models?.length ?? 0).toBe(0);
|
expect(parsed.providers["github-copilot"]?.models?.length ?? 0).toBe(0);
|
||||||
} finally {
|
} finally {
|
||||||
process.env.COPILOT_GITHUB_TOKEN = previous;
|
process.env.COPILOT_GITHUB_TOKEN = previous;
|
||||||
@@ -104,8 +98,7 @@ describe("models-config", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
vi.doMock("../providers/github-copilot-token.js", () => ({
|
vi.doMock("../providers/github-copilot-token.js", () => ({
|
||||||
DEFAULT_COPILOT_API_BASE_URL:
|
DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com",
|
||||||
"https://api.individual.githubcopilot.com",
|
|
||||||
resolveCopilotApiToken,
|
resolveCopilotApiToken,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -62,17 +62,12 @@ describe("models-config", () => {
|
|||||||
await ensureClawdbotModelsJson({ models: { providers: {} } });
|
await ensureClawdbotModelsJson({ models: { providers: {} } });
|
||||||
|
|
||||||
const agentDir = resolveClawdbotAgentDir();
|
const agentDir = resolveClawdbotAgentDir();
|
||||||
const raw = await fs.readFile(
|
const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8");
|
||||||
path.join(agentDir, "models.json"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
const parsed = JSON.parse(raw) as {
|
const parsed = JSON.parse(raw) as {
|
||||||
providers: Record<string, { baseUrl?: string }>;
|
providers: Record<string, { baseUrl?: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(parsed.providers["github-copilot"]?.baseUrl).toBe(
|
expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://api.default.test");
|
||||||
"https://api.default.test",
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
process.env.COPILOT_GITHUB_TOKEN = previous;
|
process.env.COPILOT_GITHUB_TOKEN = previous;
|
||||||
}
|
}
|
||||||
@@ -111,8 +106,7 @@ describe("models-config", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
vi.doMock("../providers/github-copilot-token.js", () => ({
|
vi.doMock("../providers/github-copilot-token.js", () => ({
|
||||||
DEFAULT_COPILOT_API_BASE_URL:
|
DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com",
|
||||||
"https://api.individual.githubcopilot.com",
|
|
||||||
resolveCopilotApiToken: vi.fn().mockResolvedValue({
|
resolveCopilotApiToken: vi.fn().mockResolvedValue({
|
||||||
token: "copilot",
|
token: "copilot",
|
||||||
expiresAt: Date.now() + 60 * 60 * 1000,
|
expiresAt: Date.now() + 60 * 60 * 1000,
|
||||||
@@ -125,17 +119,12 @@ describe("models-config", () => {
|
|||||||
|
|
||||||
await ensureClawdbotModelsJson({ models: { providers: {} } }, agentDir);
|
await ensureClawdbotModelsJson({ models: { providers: {} } }, agentDir);
|
||||||
|
|
||||||
const raw = await fs.readFile(
|
const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8");
|
||||||
path.join(agentDir, "models.json"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
const parsed = JSON.parse(raw) as {
|
const parsed = JSON.parse(raw) as {
|
||||||
providers: Record<string, { baseUrl?: string }>;
|
providers: Record<string, { baseUrl?: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(parsed.providers["github-copilot"]?.baseUrl).toBe(
|
expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://api.copilot.example");
|
||||||
"https://api.copilot.example",
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (previous === undefined) delete process.env.COPILOT_GITHUB_TOKEN;
|
if (previous === undefined) delete process.env.COPILOT_GITHUB_TOKEN;
|
||||||
else process.env.COPILOT_GITHUB_TOKEN = previous;
|
else process.env.COPILOT_GITHUB_TOKEN = previous;
|
||||||
|
|||||||
@@ -79,10 +79,7 @@ describe("models-config", () => {
|
|||||||
const modelPath = path.join(resolveClawdbotAgentDir(), "models.json");
|
const modelPath = path.join(resolveClawdbotAgentDir(), "models.json");
|
||||||
const raw = await fs.readFile(modelPath, "utf8");
|
const raw = await fs.readFile(modelPath, "utf8");
|
||||||
const parsed = JSON.parse(raw) as {
|
const parsed = JSON.parse(raw) as {
|
||||||
providers: Record<
|
providers: Record<string, { apiKey?: string; models?: Array<{ id: string }> }>;
|
||||||
string,
|
|
||||||
{ apiKey?: string; models?: Array<{ id: string }> }
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
expect(parsed.providers.minimax?.apiKey).toBe("MINIMAX_API_KEY");
|
expect(parsed.providers.minimax?.apiKey).toBe("MINIMAX_API_KEY");
|
||||||
const ids = parsed.providers.minimax?.models?.map((model) => model.id);
|
const ids = parsed.providers.minimax?.models?.map((model) => model.id);
|
||||||
@@ -138,12 +135,8 @@ describe("models-config", () => {
|
|||||||
providers: Record<string, { baseUrl?: string }>;
|
providers: Record<string, { baseUrl?: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(parsed.providers.existing?.baseUrl).toBe(
|
expect(parsed.providers.existing?.baseUrl).toBe("http://localhost:1234/v1");
|
||||||
"http://localhost:1234/v1",
|
expect(parsed.providers["custom-proxy"]?.baseUrl).toBe("http://localhost:4000/v1");
|
||||||
);
|
|
||||||
expect(parsed.providers["custom-proxy"]?.baseUrl).toBe(
|
|
||||||
"http://localhost:4000/v1",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import {
|
|||||||
DEFAULT_COPILOT_API_BASE_URL,
|
DEFAULT_COPILOT_API_BASE_URL,
|
||||||
resolveCopilotApiToken,
|
resolveCopilotApiToken,
|
||||||
} from "../providers/github-copilot-token.js";
|
} from "../providers/github-copilot-token.js";
|
||||||
import {
|
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
|
||||||
ensureAuthProfileStore,
|
|
||||||
listProfilesForProvider,
|
|
||||||
} from "./auth-profiles.js";
|
|
||||||
import { resolveEnvApiKey } from "./model-auth.js";
|
import { resolveEnvApiKey } from "./model-auth.js";
|
||||||
import {
|
import {
|
||||||
buildSyntheticModelDefinition,
|
buildSyntheticModelDefinition,
|
||||||
@@ -104,8 +101,7 @@ export function normalizeProviders(params: {
|
|||||||
// Fix common misconfig: apiKey set to "${ENV_VAR}" instead of "ENV_VAR".
|
// Fix common misconfig: apiKey set to "${ENV_VAR}" instead of "ENV_VAR".
|
||||||
if (
|
if (
|
||||||
normalizedProvider.apiKey &&
|
normalizedProvider.apiKey &&
|
||||||
normalizeApiKeyConfig(normalizedProvider.apiKey) !==
|
normalizeApiKeyConfig(normalizedProvider.apiKey) !== normalizedProvider.apiKey
|
||||||
normalizedProvider.apiKey
|
|
||||||
) {
|
) {
|
||||||
mutated = true;
|
mutated = true;
|
||||||
normalizedProvider = {
|
normalizedProvider = {
|
||||||
@@ -117,8 +113,7 @@ export function normalizeProviders(params: {
|
|||||||
// If a provider defines models, pi's ModelRegistry requires apiKey to be set.
|
// If a provider defines models, pi's ModelRegistry requires apiKey to be set.
|
||||||
// Fill it from the environment or auth profiles when possible.
|
// Fill it from the environment or auth profiles when possible.
|
||||||
const hasModels =
|
const hasModels =
|
||||||
Array.isArray(normalizedProvider.models) &&
|
Array.isArray(normalizedProvider.models) && normalizedProvider.models.length > 0;
|
||||||
normalizedProvider.models.length > 0;
|
|
||||||
if (hasModels && !normalizedProvider.apiKey?.trim()) {
|
if (hasModels && !normalizedProvider.apiKey?.trim()) {
|
||||||
const fromEnv = resolveEnvApiKeyVarName(normalizedKey);
|
const fromEnv = resolveEnvApiKeyVarName(normalizedKey);
|
||||||
const fromProfiles = resolveApiKeyFromProfiles({
|
const fromProfiles = resolveApiKeyFromProfiles({
|
||||||
@@ -197,9 +192,7 @@ function buildSyntheticProvider(): ProviderConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveImplicitProviders(params: {
|
export function resolveImplicitProviders(params: { agentDir: string }): ModelsConfig["providers"] {
|
||||||
agentDir: string;
|
|
||||||
}): ModelsConfig["providers"] {
|
|
||||||
const providers: Record<string, ProviderConfig> = {};
|
const providers: Record<string, ProviderConfig> = {};
|
||||||
const authStore = ensureAuthProfileStore(params.agentDir, {
|
const authStore = ensureAuthProfileStore(params.agentDir, {
|
||||||
allowKeychainPrompt: false,
|
allowKeychainPrompt: false,
|
||||||
@@ -235,8 +228,7 @@ export async function resolveImplicitCopilotProvider(params: {
|
|||||||
}): Promise<ProviderConfig | null> {
|
}): Promise<ProviderConfig | null> {
|
||||||
const env = params.env ?? process.env;
|
const env = params.env ?? process.env;
|
||||||
const authStore = ensureAuthProfileStore(params.agentDir);
|
const authStore = ensureAuthProfileStore(params.agentDir);
|
||||||
const hasProfile =
|
const hasProfile = listProfilesForProvider(authStore, "github-copilot").length > 0;
|
||||||
listProfilesForProvider(authStore, "github-copilot").length > 0;
|
|
||||||
const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN;
|
const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN;
|
||||||
const githubToken = (envToken ?? "").trim();
|
const githubToken = (envToken ?? "").trim();
|
||||||
|
|
||||||
|
|||||||
@@ -70,9 +70,7 @@ describe("models-config", () => {
|
|||||||
agentDir,
|
agentDir,
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(fs.stat(path.join(agentDir, "models.json"))).rejects.toThrow();
|
||||||
fs.stat(path.join(agentDir, "models.json")),
|
|
||||||
).rejects.toThrow();
|
|
||||||
expect(result.wrote).toBe(false);
|
expect(result.wrote).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
if (previous === undefined) delete process.env.COPILOT_GITHUB_TOKEN;
|
if (previous === undefined) delete process.env.COPILOT_GITHUB_TOKEN;
|
||||||
@@ -85,8 +83,7 @@ describe("models-config", () => {
|
|||||||
else process.env.MINIMAX_API_KEY = previousMinimax;
|
else process.env.MINIMAX_API_KEY = previousMinimax;
|
||||||
if (previousMoonshot === undefined) delete process.env.MOONSHOT_API_KEY;
|
if (previousMoonshot === undefined) delete process.env.MOONSHOT_API_KEY;
|
||||||
else process.env.MOONSHOT_API_KEY = previousMoonshot;
|
else process.env.MOONSHOT_API_KEY = previousMoonshot;
|
||||||
if (previousSynthetic === undefined)
|
if (previousSynthetic === undefined) delete process.env.SYNTHETIC_API_KEY;
|
||||||
delete process.env.SYNTHETIC_API_KEY;
|
|
||||||
else process.env.SYNTHETIC_API_KEY = previousSynthetic;
|
else process.env.SYNTHETIC_API_KEY = previousSynthetic;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -105,9 +102,7 @@ describe("models-config", () => {
|
|||||||
providers: Record<string, { baseUrl?: string }>;
|
providers: Record<string, { baseUrl?: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(parsed.providers["custom-proxy"]?.baseUrl).toBe(
|
expect(parsed.providers["custom-proxy"]?.baseUrl).toBe("http://localhost:4000/v1");
|
||||||
"http://localhost:4000/v1",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("adds minimax provider when MINIMAX_API_KEY is set", async () => {
|
it("adds minimax provider when MINIMAX_API_KEY is set", async () => {
|
||||||
@@ -133,9 +128,7 @@ describe("models-config", () => {
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
expect(parsed.providers.minimax?.baseUrl).toBe(
|
expect(parsed.providers.minimax?.baseUrl).toBe("https://api.minimax.io/anthropic");
|
||||||
"https://api.minimax.io/anthropic",
|
|
||||||
);
|
|
||||||
expect(parsed.providers.minimax?.apiKey).toBe("MINIMAX_API_KEY");
|
expect(parsed.providers.minimax?.apiKey).toBe("MINIMAX_API_KEY");
|
||||||
const ids = parsed.providers.minimax?.models?.map((model) => model.id);
|
const ids = parsed.providers.minimax?.models?.map((model) => model.id);
|
||||||
expect(ids).toContain("MiniMax-M2.1");
|
expect(ids).toContain("MiniMax-M2.1");
|
||||||
@@ -169,13 +162,9 @@ describe("models-config", () => {
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
expect(parsed.providers.synthetic?.baseUrl).toBe(
|
expect(parsed.providers.synthetic?.baseUrl).toBe("https://api.synthetic.new/anthropic");
|
||||||
"https://api.synthetic.new/anthropic",
|
|
||||||
);
|
|
||||||
expect(parsed.providers.synthetic?.apiKey).toBe("SYNTHETIC_API_KEY");
|
expect(parsed.providers.synthetic?.apiKey).toBe("SYNTHETIC_API_KEY");
|
||||||
const ids = parsed.providers.synthetic?.models?.map(
|
const ids = parsed.providers.synthetic?.models?.map((model) => model.id);
|
||||||
(model) => model.id,
|
|
||||||
);
|
|
||||||
expect(ids).toContain("hf:MiniMaxAI/MiniMax-M2.1");
|
expect(ids).toContain("hf:MiniMaxAI/MiniMax-M2.1");
|
||||||
} finally {
|
} finally {
|
||||||
if (prevKey === undefined) delete process.env.SYNTHETIC_API_KEY;
|
if (prevKey === undefined) delete process.env.SYNTHETIC_API_KEY;
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeProviderModels(
|
function mergeProviderModels(implicit: ProviderConfig, explicit: ProviderConfig): ProviderConfig {
|
||||||
implicit: ProviderConfig,
|
|
||||||
explicit: ProviderConfig,
|
|
||||||
): ProviderConfig {
|
|
||||||
const implicitModels = Array.isArray(implicit.models) ? implicit.models : [];
|
const implicitModels = Array.isArray(implicit.models) ? implicit.models : [];
|
||||||
const explicitModels = Array.isArray(explicit.models) ? explicit.models : [];
|
const explicitModels = Array.isArray(explicit.models) ? explicit.models : [];
|
||||||
if (implicitModels.length === 0) return { ...implicit, ...explicit };
|
if (implicitModels.length === 0) return { ...implicit, ...explicit };
|
||||||
@@ -55,16 +52,12 @@ function mergeProviders(params: {
|
|||||||
implicit?: Record<string, ProviderConfig> | null;
|
implicit?: Record<string, ProviderConfig> | null;
|
||||||
explicit?: Record<string, ProviderConfig> | null;
|
explicit?: Record<string, ProviderConfig> | null;
|
||||||
}): Record<string, ProviderConfig> {
|
}): Record<string, ProviderConfig> {
|
||||||
const out: Record<string, ProviderConfig> = params.implicit
|
const out: Record<string, ProviderConfig> = params.implicit ? { ...params.implicit } : {};
|
||||||
? { ...params.implicit }
|
|
||||||
: {};
|
|
||||||
for (const [key, explicit] of Object.entries(params.explicit ?? {})) {
|
for (const [key, explicit] of Object.entries(params.explicit ?? {})) {
|
||||||
const providerKey = key.trim();
|
const providerKey = key.trim();
|
||||||
if (!providerKey) continue;
|
if (!providerKey) continue;
|
||||||
const implicit = out[providerKey];
|
const implicit = out[providerKey];
|
||||||
out[providerKey] = implicit
|
out[providerKey] = implicit ? mergeProviderModels(implicit, explicit) : explicit;
|
||||||
? mergeProviderModels(implicit, explicit)
|
|
||||||
: explicit;
|
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -83,14 +76,9 @@ export async function ensureClawdbotModelsJson(
|
|||||||
agentDirOverride?: string,
|
agentDirOverride?: string,
|
||||||
): Promise<{ agentDir: string; wrote: boolean }> {
|
): Promise<{ agentDir: string; wrote: boolean }> {
|
||||||
const cfg = config ?? loadConfig();
|
const cfg = config ?? loadConfig();
|
||||||
const agentDir = agentDirOverride?.trim()
|
const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveClawdbotAgentDir();
|
||||||
? agentDirOverride.trim()
|
|
||||||
: resolveClawdbotAgentDir();
|
|
||||||
|
|
||||||
const explicitProviders = (cfg.models?.providers ?? {}) as Record<
|
const explicitProviders = (cfg.models?.providers ?? {}) as Record<string, ProviderConfig>;
|
||||||
string,
|
|
||||||
ProviderConfig
|
|
||||||
>;
|
|
||||||
const implicitProviders = resolveImplicitProviders({ agentDir });
|
const implicitProviders = resolveImplicitProviders({ agentDir });
|
||||||
const providers: Record<string, ProviderConfig> = mergeProviders({
|
const providers: Record<string, ProviderConfig> = mergeProviders({
|
||||||
implicit: implicitProviders,
|
implicit: implicitProviders,
|
||||||
|
|||||||
@@ -88,8 +88,7 @@ describe("models-config", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
vi.doMock("../providers/github-copilot-token.js", () => ({
|
vi.doMock("../providers/github-copilot-token.js", () => ({
|
||||||
DEFAULT_COPILOT_API_BASE_URL:
|
DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com",
|
||||||
"https://api.individual.githubcopilot.com",
|
|
||||||
resolveCopilotApiToken,
|
resolveCopilotApiToken,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -119,8 +118,7 @@ describe("models-config", () => {
|
|||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
|
||||||
vi.doMock("../providers/github-copilot-token.js", () => ({
|
vi.doMock("../providers/github-copilot-token.js", () => ({
|
||||||
DEFAULT_COPILOT_API_BASE_URL:
|
DEFAULT_COPILOT_API_BASE_URL: "https://api.individual.githubcopilot.com",
|
||||||
"https://api.individual.githubcopilot.com",
|
|
||||||
resolveCopilotApiToken: vi.fn().mockResolvedValue({
|
resolveCopilotApiToken: vi.fn().mockResolvedValue({
|
||||||
token: "copilot",
|
token: "copilot",
|
||||||
expiresAt: Date.now() + 60 * 60 * 1000,
|
expiresAt: Date.now() + 60 * 60 * 1000,
|
||||||
@@ -145,17 +143,12 @@ describe("models-config", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const agentDir = resolveClawdbotAgentDir();
|
const agentDir = resolveClawdbotAgentDir();
|
||||||
const raw = await fs.readFile(
|
const raw = await fs.readFile(path.join(agentDir, "models.json"), "utf8");
|
||||||
path.join(agentDir, "models.json"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
const parsed = JSON.parse(raw) as {
|
const parsed = JSON.parse(raw) as {
|
||||||
providers: Record<string, { baseUrl?: string }>;
|
providers: Record<string, { baseUrl?: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(parsed.providers["github-copilot"]?.baseUrl).toBe(
|
expect(parsed.providers["github-copilot"]?.baseUrl).toBe("https://copilot.local");
|
||||||
"https://copilot.local",
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
process.env.COPILOT_GITHUB_TOKEN = previous;
|
process.env.COPILOT_GITHUB_TOKEN = previous;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai";
|
import { type Api, completeSimple, type Model } from "@mariozechner/pi-ai";
|
||||||
import {
|
import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
|
||||||
discoverAuthStorage,
|
|
||||||
discoverModels,
|
|
||||||
} from "@mariozechner/pi-coding-agent";
|
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
@@ -19,8 +16,7 @@ import { isRateLimitErrorMessage } from "./pi-embedded-helpers/errors.js";
|
|||||||
|
|
||||||
const LIVE = process.env.LIVE === "1" || process.env.CLAWDBOT_LIVE_TEST === "1";
|
const LIVE = process.env.LIVE === "1" || process.env.CLAWDBOT_LIVE_TEST === "1";
|
||||||
const DIRECT_ENABLED = Boolean(process.env.CLAWDBOT_LIVE_MODELS?.trim());
|
const DIRECT_ENABLED = Boolean(process.env.CLAWDBOT_LIVE_MODELS?.trim());
|
||||||
const REQUIRE_PROFILE_KEYS =
|
const REQUIRE_PROFILE_KEYS = process.env.CLAWDBOT_LIVE_REQUIRE_PROFILE_KEYS === "1";
|
||||||
process.env.CLAWDBOT_LIVE_REQUIRE_PROFILE_KEYS === "1";
|
|
||||||
|
|
||||||
const describeLive = LIVE ? describe : describe.skip;
|
const describeLive = LIVE ? describe : describe.skip;
|
||||||
|
|
||||||
@@ -62,8 +58,7 @@ function isModelNotFoundErrorMessage(raw: string): boolean {
|
|||||||
if (!msg) return false;
|
if (!msg) return false;
|
||||||
if (/\b404\b/.test(msg) && /not[_-]?found/i.test(msg)) return true;
|
if (/\b404\b/.test(msg) && /not[_-]?found/i.test(msg)) return true;
|
||||||
if (/not_found_error/i.test(msg)) return true;
|
if (/not_found_error/i.test(msg)) return true;
|
||||||
if (/model:\s*[a-z0-9._-]+/i.test(msg) && /not[_-]?found/i.test(msg))
|
if (/model:\s*[a-z0-9._-]+/i.test(msg) && /not[_-]?found/i.test(msg)) return true;
|
||||||
return true;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,9 +151,7 @@ describeLive("live models (profile keys)", () => {
|
|||||||
const anthropicKeys = collectAnthropicApiKeys();
|
const anthropicKeys = collectAnthropicApiKeys();
|
||||||
if (anthropicKeys.length > 0) {
|
if (anthropicKeys.length > 0) {
|
||||||
process.env.ANTHROPIC_API_KEY = anthropicKeys[0];
|
process.env.ANTHROPIC_API_KEY = anthropicKeys[0];
|
||||||
logProgress(
|
logProgress(`[live-models] anthropic keys loaded: ${anthropicKeys.length}`);
|
||||||
`[live-models] anthropic keys loaded: ${anthropicKeys.length}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentDir = resolveClawdbotAgentDir();
|
const agentDir = resolveClawdbotAgentDir();
|
||||||
@@ -171,13 +164,8 @@ describeLive("live models (profile keys)", () => {
|
|||||||
const useExplicit = Boolean(rawModels) && !useModern;
|
const useExplicit = Boolean(rawModels) && !useModern;
|
||||||
const filter = useExplicit ? parseModelFilter(rawModels) : null;
|
const filter = useExplicit ? parseModelFilter(rawModels) : null;
|
||||||
const allowNotFoundSkip = useModern;
|
const allowNotFoundSkip = useModern;
|
||||||
const providers = parseProviderFilter(
|
const providers = parseProviderFilter(process.env.CLAWDBOT_LIVE_PROVIDERS);
|
||||||
process.env.CLAWDBOT_LIVE_PROVIDERS,
|
const perModelTimeoutMs = toInt(process.env.CLAWDBOT_LIVE_MODEL_TIMEOUT_MS, 30_000);
|
||||||
);
|
|
||||||
const perModelTimeoutMs = toInt(
|
|
||||||
process.env.CLAWDBOT_LIVE_MODEL_TIMEOUT_MS,
|
|
||||||
30_000,
|
|
||||||
);
|
|
||||||
|
|
||||||
const failures: Array<{ model: string; error: string }> = [];
|
const failures: Array<{ model: string; error: string }> = [];
|
||||||
const skipped: Array<{ model: string; reason: string }> = [];
|
const skipped: Array<{ model: string; reason: string }> = [];
|
||||||
@@ -197,10 +185,7 @@ describeLive("live models (profile keys)", () => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const apiKeyInfo = await getApiKeyForModel({ model, cfg });
|
const apiKeyInfo = await getApiKeyForModel({ model, cfg });
|
||||||
if (
|
if (REQUIRE_PROFILE_KEYS && !apiKeyInfo.source.startsWith("profile:")) {
|
||||||
REQUIRE_PROFILE_KEYS &&
|
|
||||||
!apiKeyInfo.source.startsWith("profile:")
|
|
||||||
) {
|
|
||||||
skipped.push({
|
skipped.push({
|
||||||
model: id,
|
model: id,
|
||||||
reason: `non-profile credential source: ${apiKeyInfo.source}`,
|
reason: `non-profile credential source: ${apiKeyInfo.source}`,
|
||||||
@@ -218,9 +203,7 @@ describeLive("live models (profile keys)", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logProgress(
|
logProgress(`[live-models] selection=${useExplicit ? "explicit" : "modern"}`);
|
||||||
`[live-models] selection=${useExplicit ? "explicit" : "modern"}`,
|
|
||||||
);
|
|
||||||
logProgress(`[live-models] running ${candidates.length} models`);
|
logProgress(`[live-models] running ${candidates.length} models`);
|
||||||
const total = candidates.length;
|
const total = candidates.length;
|
||||||
|
|
||||||
@@ -229,9 +212,7 @@ describeLive("live models (profile keys)", () => {
|
|||||||
const id = `${model.provider}/${model.id}`;
|
const id = `${model.provider}/${model.id}`;
|
||||||
const progressLabel = `[live-models] ${index + 1}/${total} ${id}`;
|
const progressLabel = `[live-models] ${index + 1}/${total} ${id}`;
|
||||||
const attemptMax =
|
const attemptMax =
|
||||||
model.provider === "anthropic" && anthropicKeys.length > 0
|
model.provider === "anthropic" && anthropicKeys.length > 0 ? anthropicKeys.length : 1;
|
||||||
? anthropicKeys.length
|
|
||||||
: 1;
|
|
||||||
for (let attempt = 0; attempt < attemptMax; attempt += 1) {
|
for (let attempt = 0; attempt < attemptMax; attempt += 1) {
|
||||||
if (model.provider === "anthropic" && anthropicKeys.length > 0) {
|
if (model.provider === "anthropic" && anthropicKeys.length > 0) {
|
||||||
process.env.ANTHROPIC_API_KEY = anthropicKeys[attempt];
|
process.env.ANTHROPIC_API_KEY = anthropicKeys[attempt];
|
||||||
@@ -254,8 +235,7 @@ describeLive("live models (profile keys)", () => {
|
|||||||
parameters: Type.Object({}, { additionalProperties: false }),
|
parameters: Type.Object({}, { additionalProperties: false }),
|
||||||
};
|
};
|
||||||
|
|
||||||
let firstUserContent =
|
let firstUserContent = "Call the tool `noop` with {}. Do not write any other text.";
|
||||||
"Call the tool `noop` with {}. Do not write any other text.";
|
|
||||||
let firstUser = {
|
let firstUser = {
|
||||||
role: "user" as const,
|
role: "user" as const,
|
||||||
content: firstUserContent,
|
content: firstUserContent,
|
||||||
@@ -282,11 +262,7 @@ describeLive("live models (profile keys)", () => {
|
|||||||
|
|
||||||
// Occasional flake: model answers in text instead of tool call (or adds text).
|
// Occasional flake: model answers in text instead of tool call (or adds text).
|
||||||
// Retry a couple times with a stronger instruction so we still exercise the tool-only replay path.
|
// Retry a couple times with a stronger instruction so we still exercise the tool-only replay path.
|
||||||
for (
|
for (let i = 0; i < 2 && (!toolCall || firstText.length > 0); i += 1) {
|
||||||
let i = 0;
|
|
||||||
i < 2 && (!toolCall || firstText.length > 0);
|
|
||||||
i += 1
|
|
||||||
) {
|
|
||||||
firstUserContent =
|
firstUserContent =
|
||||||
"Call the tool `noop` with {}. IMPORTANT: respond ONLY with the tool call; no other text.";
|
"Call the tool `noop` with {}. IMPORTANT: respond ONLY with the tool call; no other text.";
|
||||||
firstUser = {
|
firstUser = {
|
||||||
@@ -405,29 +381,19 @@ describeLive("live models (profile keys)", () => {
|
|||||||
isAnthropicRateLimitError(message) &&
|
isAnthropicRateLimitError(message) &&
|
||||||
attempt + 1 < attemptMax
|
attempt + 1 < attemptMax
|
||||||
) {
|
) {
|
||||||
logProgress(
|
logProgress(`${progressLabel}: rate limit, retrying with next key`);
|
||||||
`${progressLabel}: rate limit, retrying with next key`,
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (model.provider === "anthropic" && isAnthropicBillingError(message)) {
|
||||||
model.provider === "anthropic" &&
|
|
||||||
isAnthropicBillingError(message)
|
|
||||||
) {
|
|
||||||
if (attempt + 1 < attemptMax) {
|
if (attempt + 1 < attemptMax) {
|
||||||
logProgress(
|
logProgress(`${progressLabel}: billing issue, retrying with next key`);
|
||||||
`${progressLabel}: billing issue, retrying with next key`,
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
skipped.push({ model: id, reason: message });
|
skipped.push({ model: id, reason: message });
|
||||||
logProgress(`${progressLabel}: skip (anthropic billing)`);
|
logProgress(`${progressLabel}: skip (anthropic billing)`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (
|
if (model.provider === "google" && isGoogleModelNotFoundError(err)) {
|
||||||
model.provider === "google" &&
|
|
||||||
isGoogleModelNotFoundError(err)
|
|
||||||
) {
|
|
||||||
skipped.push({ model: id, reason: message });
|
skipped.push({ model: id, reason: message });
|
||||||
logProgress(`${progressLabel}: skip (google model not found)`);
|
logProgress(`${progressLabel}: skip (google model not found)`);
|
||||||
break;
|
break;
|
||||||
@@ -462,9 +428,7 @@ describeLive("live models (profile keys)", () => {
|
|||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
.map((f) => `- ${f.model}: ${f.error}`)
|
.map((f) => `- ${f.model}: ${f.error}`)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
throw new Error(
|
throw new Error(`live model failures (${failures.length}):\n${preview}`);
|
||||||
`live model failures (${failures.length}):\n${preview}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void skipped;
|
void skipped;
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import type {
|
import type { AssistantMessage, Model, ToolResultMessage } from "@mariozechner/pi-ai";
|
||||||
AssistantMessage,
|
|
||||||
Model,
|
|
||||||
ToolResultMessage,
|
|
||||||
} from "@mariozechner/pi-ai";
|
|
||||||
import { streamOpenAIResponses } from "@mariozechner/pi-ai";
|
import { streamOpenAIResponses } from "@mariozechner/pi-ai";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
@@ -31,8 +27,7 @@ function installFailingFetchCapture() {
|
|||||||
const bodyText = (() => {
|
const bodyText = (() => {
|
||||||
if (!rawBody) return "";
|
if (!rawBody) return "";
|
||||||
if (typeof rawBody === "string") return rawBody;
|
if (typeof rawBody === "string") return rawBody;
|
||||||
if (rawBody instanceof Uint8Array)
|
if (rawBody instanceof Uint8Array) return Buffer.from(rawBody).toString("utf8");
|
||||||
return Buffer.from(rawBody).toString("utf8");
|
|
||||||
if (rawBody instanceof ArrayBuffer)
|
if (rawBody instanceof ArrayBuffer)
|
||||||
return Buffer.from(new Uint8Array(rawBody)).toString("utf8");
|
return Buffer.from(new Uint8Array(rawBody)).toString("utf8");
|
||||||
return String(rawBody);
|
return String(rawBody);
|
||||||
@@ -135,17 +130,13 @@ describe("openai-responses reasoning replay", () => {
|
|||||||
const input = Array.isArray(body?.input) ? body?.input : [];
|
const input = Array.isArray(body?.input) ? body?.input : [];
|
||||||
const types = input
|
const types = input
|
||||||
.map((item) =>
|
.map((item) =>
|
||||||
item && typeof item === "object"
|
item && typeof item === "object" ? (item as Record<string, unknown>).type : undefined,
|
||||||
? (item as Record<string, unknown>).type
|
|
||||||
: undefined,
|
|
||||||
)
|
)
|
||||||
.filter((t): t is string => typeof t === "string");
|
.filter((t): t is string => typeof t === "string");
|
||||||
|
|
||||||
expect(types).toContain("reasoning");
|
expect(types).toContain("reasoning");
|
||||||
expect(types).toContain("function_call");
|
expect(types).toContain("function_call");
|
||||||
expect(types.indexOf("reasoning")).toBeLessThan(
|
expect(types.indexOf("reasoning")).toBeLessThan(types.indexOf("function_call"));
|
||||||
types.indexOf("function_call"),
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
cap.restore();
|
cap.restore();
|
||||||
}
|
}
|
||||||
@@ -204,9 +195,7 @@ describe("openai-responses reasoning replay", () => {
|
|||||||
const input = Array.isArray(body?.input) ? body?.input : [];
|
const input = Array.isArray(body?.input) ? body?.input : [];
|
||||||
const types = input
|
const types = input
|
||||||
.map((item) =>
|
.map((item) =>
|
||||||
item && typeof item === "object"
|
item && typeof item === "object" ? (item as Record<string, unknown>).type : undefined,
|
||||||
? (item as Record<string, unknown>).type
|
|
||||||
: undefined,
|
|
||||||
)
|
)
|
||||||
.filter((t): t is string => typeof t === "string");
|
.filter((t): t is string => typeof t === "string");
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ describe("resolveOpencodeZenAlias", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns input if no alias exists", () => {
|
it("returns input if no alias exists", () => {
|
||||||
expect(resolveOpencodeZenAlias("some-unknown-model")).toBe(
|
expect(resolveOpencodeZenAlias("some-unknown-model")).toBe("some-unknown-model");
|
||||||
"some-unknown-model",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is case-insensitive", () => {
|
it("is case-insensitive", () => {
|
||||||
@@ -42,22 +40,12 @@ describe("resolveOpencodeZenAlias", () => {
|
|||||||
|
|
||||||
describe("resolveOpencodeZenModelApi", () => {
|
describe("resolveOpencodeZenModelApi", () => {
|
||||||
it("maps APIs by model family", () => {
|
it("maps APIs by model family", () => {
|
||||||
expect(resolveOpencodeZenModelApi("claude-opus-4-5")).toBe(
|
expect(resolveOpencodeZenModelApi("claude-opus-4-5")).toBe("anthropic-messages");
|
||||||
"anthropic-messages",
|
expect(resolveOpencodeZenModelApi("minimax-m2.1-free")).toBe("anthropic-messages");
|
||||||
);
|
expect(resolveOpencodeZenModelApi("gemini-3-pro")).toBe("google-generative-ai");
|
||||||
expect(resolveOpencodeZenModelApi("minimax-m2.1-free")).toBe(
|
|
||||||
"anthropic-messages",
|
|
||||||
);
|
|
||||||
expect(resolveOpencodeZenModelApi("gemini-3-pro")).toBe(
|
|
||||||
"google-generative-ai",
|
|
||||||
);
|
|
||||||
expect(resolveOpencodeZenModelApi("gpt-5.2")).toBe("openai-responses");
|
expect(resolveOpencodeZenModelApi("gpt-5.2")).toBe("openai-responses");
|
||||||
expect(resolveOpencodeZenModelApi("glm-4.7-free")).toBe(
|
expect(resolveOpencodeZenModelApi("glm-4.7-free")).toBe("openai-completions");
|
||||||
"openai-completions",
|
expect(resolveOpencodeZenModelApi("some-unknown-model")).toBe("openai-completions");
|
||||||
);
|
|
||||||
expect(resolveOpencodeZenModelApi("some-unknown-model")).toBe(
|
|
||||||
"openai-completions",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -91,11 +91,7 @@ export function resolveOpencodeZenAlias(modelIdOrAlias: string): string {
|
|||||||
*/
|
*/
|
||||||
export function resolveOpencodeZenModelApi(modelId: string): ModelApi {
|
export function resolveOpencodeZenModelApi(modelId: string): ModelApi {
|
||||||
const lower = modelId.toLowerCase();
|
const lower = modelId.toLowerCase();
|
||||||
if (
|
if (lower.startsWith("claude-") || lower.startsWith("minimax") || lower.startsWith("alpha-gd4")) {
|
||||||
lower.startsWith("claude-") ||
|
|
||||||
lower.startsWith("minimax") ||
|
|
||||||
lower.startsWith("alpha-gd4")
|
|
||||||
) {
|
|
||||||
return "anthropic-messages";
|
return "anthropic-messages";
|
||||||
}
|
}
|
||||||
if (lower.startsWith("gemini-")) {
|
if (lower.startsWith("gemini-")) {
|
||||||
@@ -274,9 +270,7 @@ interface ZenModelsResponse {
|
|||||||
* @param apiKey - OpenCode Zen API key for authentication
|
* @param apiKey - OpenCode Zen API key for authentication
|
||||||
* @returns Array of model definitions, or static fallback on failure
|
* @returns Array of model definitions, or static fallback on failure
|
||||||
*/
|
*/
|
||||||
export async function fetchOpencodeZenModels(
|
export async function fetchOpencodeZenModels(apiKey?: string): Promise<ModelDefinitionConfig[]> {
|
||||||
apiKey?: string,
|
|
||||||
): Promise<ModelDefinitionConfig[]> {
|
|
||||||
// Return cached models if still valid
|
// Return cached models if still valid
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (cachedModels && now - cacheTimestamp < CACHE_TTL_MS) {
|
if (cachedModels && now - cacheTimestamp < CACHE_TTL_MS) {
|
||||||
@@ -298,9 +292,7 @@ export async function fetchOpencodeZenModels(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(`API returned ${response.status}: ${response.statusText}`);
|
||||||
`API returned ${response.status}: ${response.statusText}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = (await response.json()) as ZenModelsResponse;
|
const data = (await response.json()) as ZenModelsResponse;
|
||||||
@@ -316,9 +308,7 @@ export async function fetchOpencodeZenModels(
|
|||||||
|
|
||||||
return models;
|
return models;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(`[opencode-zen] Failed to fetch models, using static fallback: ${String(error)}`);
|
||||||
`[opencode-zen] Failed to fetch models, using static fallback: ${String(error)}`,
|
|
||||||
);
|
|
||||||
return getOpencodeZenStaticFallbackModels();
|
return getOpencodeZenStaticFallbackModels();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { findFenceSpanAt, isSafeFenceBreak, parseFenceSpans } from "../markdown/fences.js";
|
||||||
findFenceSpanAt,
|
|
||||||
isSafeFenceBreak,
|
|
||||||
parseFenceSpans,
|
|
||||||
} from "../markdown/fences.js";
|
|
||||||
|
|
||||||
export type BlockReplyChunking = {
|
export type BlockReplyChunking = {
|
||||||
minChars: number;
|
minChars: number;
|
||||||
@@ -61,10 +57,7 @@ export class EmbeddedBlockChunker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (
|
while (this.#buffer.length >= minChars || (force && this.#buffer.length > 0)) {
|
||||||
this.#buffer.length >= minChars ||
|
|
||||||
(force && this.#buffer.length > 0)
|
|
||||||
) {
|
|
||||||
const breakResult =
|
const breakResult =
|
||||||
force && this.#buffer.length <= maxChars
|
force && this.#buffer.length <= maxChars
|
||||||
? this.#pickSoftBreakIndex(this.#buffer, 1)
|
? this.#pickSoftBreakIndex(this.#buffer, 1)
|
||||||
@@ -80,9 +73,7 @@ export class EmbeddedBlockChunker {
|
|||||||
const breakIdx = breakResult.index;
|
const breakIdx = breakResult.index;
|
||||||
let rawChunk = this.#buffer.slice(0, breakIdx);
|
let rawChunk = this.#buffer.slice(0, breakIdx);
|
||||||
if (rawChunk.trim().length === 0) {
|
if (rawChunk.trim().length === 0) {
|
||||||
this.#buffer = stripLeadingNewlines(
|
this.#buffer = stripLeadingNewlines(this.#buffer.slice(breakIdx)).trimStart();
|
||||||
this.#buffer.slice(breakIdx),
|
|
||||||
).trimStart();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,10 +109,7 @@ export class EmbeddedBlockChunker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#pickSoftBreakIndex(buffer: string, minCharsOverride?: number): BreakResult {
|
#pickSoftBreakIndex(buffer: string, minCharsOverride?: number): BreakResult {
|
||||||
const minChars = Math.max(
|
const minChars = Math.max(1, Math.floor(minCharsOverride ?? this.#chunking.minChars));
|
||||||
1,
|
|
||||||
Math.floor(minCharsOverride ?? this.#chunking.minChars),
|
|
||||||
);
|
|
||||||
if (buffer.length < minChars) return { index: -1 };
|
if (buffer.length < minChars) return { index: -1 };
|
||||||
const fenceSpans = parseFenceSpans(buffer);
|
const fenceSpans = parseFenceSpans(buffer);
|
||||||
const preference = this.#chunking.breakPreference ?? "paragraph";
|
const preference = this.#chunking.breakPreference ?? "paragraph";
|
||||||
@@ -144,10 +132,7 @@ export class EmbeddedBlockChunker {
|
|||||||
if (preference === "paragraph" || preference === "newline") {
|
if (preference === "paragraph" || preference === "newline") {
|
||||||
let newlineIdx = buffer.indexOf("\n");
|
let newlineIdx = buffer.indexOf("\n");
|
||||||
while (newlineIdx !== -1) {
|
while (newlineIdx !== -1) {
|
||||||
if (
|
if (newlineIdx >= minChars && isSafeFenceBreak(fenceSpans, newlineIdx)) {
|
||||||
newlineIdx >= minChars &&
|
|
||||||
isSafeFenceBreak(fenceSpans, newlineIdx)
|
|
||||||
) {
|
|
||||||
return { index: newlineIdx };
|
return { index: newlineIdx };
|
||||||
}
|
}
|
||||||
newlineIdx = buffer.indexOf("\n", newlineIdx + 1);
|
newlineIdx = buffer.indexOf("\n", newlineIdx + 1);
|
||||||
@@ -172,10 +157,7 @@ export class EmbeddedBlockChunker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#pickBreakIndex(buffer: string, minCharsOverride?: number): BreakResult {
|
#pickBreakIndex(buffer: string, minCharsOverride?: number): BreakResult {
|
||||||
const minChars = Math.max(
|
const minChars = Math.max(1, Math.floor(minCharsOverride ?? this.#chunking.minChars));
|
||||||
1,
|
|
||||||
Math.floor(minCharsOverride ?? this.#chunking.minChars),
|
|
||||||
);
|
|
||||||
const maxChars = Math.max(minChars, Math.floor(this.#chunking.maxChars));
|
const maxChars = Math.max(minChars, Math.floor(this.#chunking.maxChars));
|
||||||
if (buffer.length < minChars) return { index: -1 };
|
if (buffer.length < minChars) return { index: -1 };
|
||||||
const window = buffer.slice(0, Math.min(maxChars, buffer.length));
|
const window = buffer.slice(0, Math.min(maxChars, buffer.length));
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import { buildBootstrapContextFiles, DEFAULT_BOOTSTRAP_MAX_CHARS } from "./pi-embedded-helpers.js";
|
||||||
buildBootstrapContextFiles,
|
|
||||||
DEFAULT_BOOTSTRAP_MAX_CHARS,
|
|
||||||
} from "./pi-embedded-helpers.js";
|
|
||||||
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
||||||
|
|
||||||
const makeFile = (
|
const makeFile = (overrides: Partial<WorkspaceBootstrapFile>): WorkspaceBootstrapFile => ({
|
||||||
overrides: Partial<WorkspaceBootstrapFile>,
|
|
||||||
): WorkspaceBootstrapFile => ({
|
|
||||||
name: DEFAULT_AGENTS_FILENAME,
|
name: DEFAULT_AGENTS_FILENAME,
|
||||||
path: "/tmp/AGENTS.md",
|
path: "/tmp/AGENTS.md",
|
||||||
content: "",
|
content: "",
|
||||||
@@ -40,9 +35,7 @@ describe("buildBootstrapContextFiles", () => {
|
|||||||
maxChars,
|
maxChars,
|
||||||
warn: (message) => warnings.push(message),
|
warn: (message) => warnings.push(message),
|
||||||
});
|
});
|
||||||
expect(result?.content).toContain(
|
expect(result?.content).toContain("[...truncated, read TOOLS.md for full content...]");
|
||||||
"[...truncated, read TOOLS.md for full content...]",
|
|
||||||
);
|
|
||||||
expect(result?.content.length).toBeLessThan(long.length);
|
expect(result?.content.length).toBeLessThan(long.length);
|
||||||
expect(result?.content.startsWith(long.slice(0, 120))).toBe(true);
|
expect(result?.content.startsWith(long.slice(0, 120))).toBe(true);
|
||||||
expect(result?.content.endsWith(long.slice(-expectedTailChars))).toBe(true);
|
expect(result?.content.endsWith(long.slice(-expectedTailChars))).toBe(true);
|
||||||
@@ -55,8 +48,6 @@ describe("buildBootstrapContextFiles", () => {
|
|||||||
const files = [makeFile({ content: long })];
|
const files = [makeFile({ content: long })];
|
||||||
const [result] = buildBootstrapContextFiles(files);
|
const [result] = buildBootstrapContextFiles(files);
|
||||||
expect(result?.content).toBe(long);
|
expect(result?.content).toBe(long);
|
||||||
expect(result?.content).not.toContain(
|
expect(result?.content).not.toContain("[...truncated, read AGENTS.md for full content...]");
|
||||||
"[...truncated, read AGENTS.md for full content...]",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { classifyFailoverReason } from "./pi-embedded-helpers.js";
|
import { classifyFailoverReason } from "./pi-embedded-helpers.js";
|
||||||
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
||||||
|
|
||||||
const _makeFile = (
|
const _makeFile = (overrides: Partial<WorkspaceBootstrapFile>): WorkspaceBootstrapFile => ({
|
||||||
overrides: Partial<WorkspaceBootstrapFile>,
|
|
||||||
): WorkspaceBootstrapFile => ({
|
|
||||||
name: DEFAULT_AGENTS_FILENAME,
|
name: DEFAULT_AGENTS_FILENAME,
|
||||||
path: "/tmp/AGENTS.md",
|
path: "/tmp/AGENTS.md",
|
||||||
content: "",
|
content: "",
|
||||||
@@ -17,9 +15,7 @@ describe("classifyFailoverReason", () => {
|
|||||||
expect(classifyFailoverReason("no credentials found")).toBe("auth");
|
expect(classifyFailoverReason("no credentials found")).toBe("auth");
|
||||||
expect(classifyFailoverReason("no api key found")).toBe("auth");
|
expect(classifyFailoverReason("no api key found")).toBe("auth");
|
||||||
expect(classifyFailoverReason("429 too many requests")).toBe("rate_limit");
|
expect(classifyFailoverReason("429 too many requests")).toBe("rate_limit");
|
||||||
expect(classifyFailoverReason("resource has been exhausted")).toBe(
|
expect(classifyFailoverReason("resource has been exhausted")).toBe("rate_limit");
|
||||||
"rate_limit",
|
|
||||||
);
|
|
||||||
expect(
|
expect(
|
||||||
classifyFailoverReason(
|
classifyFailoverReason(
|
||||||
'{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}',
|
'{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}',
|
||||||
@@ -28,16 +24,12 @@ describe("classifyFailoverReason", () => {
|
|||||||
expect(classifyFailoverReason("invalid request format")).toBe("format");
|
expect(classifyFailoverReason("invalid request format")).toBe("format");
|
||||||
expect(classifyFailoverReason("credit balance too low")).toBe("billing");
|
expect(classifyFailoverReason("credit balance too low")).toBe("billing");
|
||||||
expect(classifyFailoverReason("deadline exceeded")).toBe("timeout");
|
expect(classifyFailoverReason("deadline exceeded")).toBe("timeout");
|
||||||
expect(classifyFailoverReason("string should match pattern")).toBe(
|
expect(classifyFailoverReason("string should match pattern")).toBe("format");
|
||||||
"format",
|
|
||||||
);
|
|
||||||
expect(classifyFailoverReason("bad request")).toBeNull();
|
expect(classifyFailoverReason("bad request")).toBeNull();
|
||||||
});
|
});
|
||||||
it("classifies OpenAI usage limit errors as rate_limit", () => {
|
it("classifies OpenAI usage limit errors as rate_limit", () => {
|
||||||
expect(
|
expect(classifyFailoverReason("You have hit your ChatGPT usage limit (plus plan)")).toBe(
|
||||||
classifyFailoverReason(
|
"rate_limit",
|
||||||
"You have hit your ChatGPT usage limit (plus plan)",
|
);
|
||||||
),
|
|
||||||
).toBe("rate_limit");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { formatAssistantErrorText } from "./pi-embedded-helpers.js";
|
import { formatAssistantErrorText } from "./pi-embedded-helpers.js";
|
||||||
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
||||||
|
|
||||||
const _makeFile = (
|
const _makeFile = (overrides: Partial<WorkspaceBootstrapFile>): WorkspaceBootstrapFile => ({
|
||||||
overrides: Partial<WorkspaceBootstrapFile>,
|
|
||||||
): WorkspaceBootstrapFile => ({
|
|
||||||
name: DEFAULT_AGENTS_FILENAME,
|
name: DEFAULT_AGENTS_FILENAME,
|
||||||
path: "/tmp/AGENTS.md",
|
path: "/tmp/AGENTS.md",
|
||||||
content: "",
|
content: "",
|
||||||
@@ -24,12 +22,8 @@ describe("formatAssistantErrorText", () => {
|
|||||||
expect(formatAssistantErrorText(msg)).toContain("Context overflow");
|
expect(formatAssistantErrorText(msg)).toContain("Context overflow");
|
||||||
});
|
});
|
||||||
it("returns a friendly message for Anthropic role ordering", () => {
|
it("returns a friendly message for Anthropic role ordering", () => {
|
||||||
const msg = makeAssistantError(
|
const msg = makeAssistantError('messages: roles must alternate between "user" and "assistant"');
|
||||||
'messages: roles must alternate between "user" and "assistant"',
|
expect(formatAssistantErrorText(msg)).toContain("Message ordering conflict");
|
||||||
);
|
|
||||||
expect(formatAssistantErrorText(msg)).toContain(
|
|
||||||
"Message ordering conflict",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it("returns a friendly message for Anthropic overload errors", () => {
|
it("returns a friendly message for Anthropic overload errors", () => {
|
||||||
const msg = makeAssistantError(
|
const msg = makeAssistantError(
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { isAuthErrorMessage } from "./pi-embedded-helpers.js";
|
import { isAuthErrorMessage } from "./pi-embedded-helpers.js";
|
||||||
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
||||||
|
|
||||||
const _makeFile = (
|
const _makeFile = (overrides: Partial<WorkspaceBootstrapFile>): WorkspaceBootstrapFile => ({
|
||||||
overrides: Partial<WorkspaceBootstrapFile>,
|
|
||||||
): WorkspaceBootstrapFile => ({
|
|
||||||
name: DEFAULT_AGENTS_FILENAME,
|
name: DEFAULT_AGENTS_FILENAME,
|
||||||
path: "/tmp/AGENTS.md",
|
path: "/tmp/AGENTS.md",
|
||||||
content: "",
|
content: "",
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { isBillingErrorMessage } from "./pi-embedded-helpers.js";
|
import { isBillingErrorMessage } from "./pi-embedded-helpers.js";
|
||||||
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
||||||
|
|
||||||
const _makeFile = (
|
const _makeFile = (overrides: Partial<WorkspaceBootstrapFile>): WorkspaceBootstrapFile => ({
|
||||||
overrides: Partial<WorkspaceBootstrapFile>,
|
|
||||||
): WorkspaceBootstrapFile => ({
|
|
||||||
name: DEFAULT_AGENTS_FILENAME,
|
name: DEFAULT_AGENTS_FILENAME,
|
||||||
path: "/tmp/AGENTS.md",
|
path: "/tmp/AGENTS.md",
|
||||||
content: "",
|
content: "",
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { isCloudCodeAssistFormatError } from "./pi-embedded-helpers.js";
|
import { isCloudCodeAssistFormatError } from "./pi-embedded-helpers.js";
|
||||||
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
||||||
|
|
||||||
const _makeFile = (
|
const _makeFile = (overrides: Partial<WorkspaceBootstrapFile>): WorkspaceBootstrapFile => ({
|
||||||
overrides: Partial<WorkspaceBootstrapFile>,
|
|
||||||
): WorkspaceBootstrapFile => ({
|
|
||||||
name: DEFAULT_AGENTS_FILENAME,
|
name: DEFAULT_AGENTS_FILENAME,
|
||||||
path: "/tmp/AGENTS.md",
|
path: "/tmp/AGENTS.md",
|
||||||
content: "",
|
content: "",
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { isCompactionFailureError } from "./pi-embedded-helpers.js";
|
import { isCompactionFailureError } from "./pi-embedded-helpers.js";
|
||||||
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
import { DEFAULT_AGENTS_FILENAME } from "./workspace.js";
|
||||||
|
|
||||||
const _makeFile = (
|
const _makeFile = (overrides: Partial<WorkspaceBootstrapFile>): WorkspaceBootstrapFile => ({
|
||||||
overrides: Partial<WorkspaceBootstrapFile>,
|
|
||||||
): WorkspaceBootstrapFile => ({
|
|
||||||
name: DEFAULT_AGENTS_FILENAME,
|
name: DEFAULT_AGENTS_FILENAME,
|
||||||
path: "/tmp/AGENTS.md",
|
path: "/tmp/AGENTS.md",
|
||||||
content: "",
|
content: "",
|
||||||
@@ -23,9 +21,7 @@ describe("isCompactionFailureError", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
it("ignores non-compaction overflow errors", () => {
|
it("ignores non-compaction overflow errors", () => {
|
||||||
expect(isCompactionFailureError("Context overflow: prompt too large")).toBe(
|
expect(isCompactionFailureError("Context overflow: prompt too large")).toBe(false);
|
||||||
false,
|
|
||||||
);
|
|
||||||
expect(isCompactionFailureError("rate limit exceeded")).toBe(false);
|
expect(isCompactionFailureError("rate limit exceeded")).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user