mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-08 09:49:34 -10:00
Compare commits
476
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1663cc25f7 | ||
|
|
5434d6b1b6 | ||
|
|
f803e8473a | ||
|
|
0cb8a34075 | ||
|
|
8378c39e1d | ||
|
|
1340fa5ae1 | ||
|
|
efaeabdb55 | ||
|
|
22e79d6fbe | ||
|
|
b65d2f1177 | ||
|
|
eaa1617425 | ||
|
|
362253b58c | ||
|
|
e74ed3eaf9 | ||
|
|
cff3245568 | ||
|
|
973c8ca97a | ||
|
|
652690c1e5 | ||
|
|
ce2e5c79d0 | ||
|
|
738ab547cb | ||
|
|
bf76da2683 | ||
|
|
f0144b45fc | ||
|
|
9c5379adf5 | ||
|
|
d466e7a072 | ||
|
|
159d2fb2a9 | ||
|
|
f7f1f05b0e | ||
|
|
cb32fda724 | ||
|
|
b33b5f2aa8 | ||
|
|
6ce59e681e | ||
|
|
0c059f5048 | ||
|
|
f0ba49c66e | ||
|
|
2a0fa271f2 | ||
|
|
03079e7f95 | ||
|
|
ad63ce2de3 | ||
|
|
0f2f6955b1 | ||
|
|
122f98a07b | ||
|
|
797e3a26bc | ||
|
|
2840462c61 | ||
|
|
ff99a5f286 | ||
|
|
1b6e6a7856 | ||
|
|
089dbd2e26 | ||
|
|
13de9d7ce7 | ||
|
|
838d11d8c1 | ||
|
|
89483d3fff | ||
|
|
df1b7e007f | ||
|
|
fa72b59619 | ||
|
|
7b6c9510e3 | ||
|
|
c198560854 | ||
|
|
467992e17e | ||
|
|
2a211e9b67 | ||
|
|
98af8807c0 | ||
|
|
53a4eb2efc | ||
|
|
d22d485da8 | ||
|
|
3f96681782 | ||
|
|
26a07f8632 | ||
|
|
d00e3c87e1 | ||
|
|
68010f16ec | ||
|
|
1c74c9b316 | ||
|
|
e3ed3e95a7 | ||
|
|
d96c5a83b0 | ||
|
|
594aa56405 | ||
|
|
933c0060b0 | ||
|
|
21026086c3 | ||
|
|
df466c5f6a | ||
|
|
85b3c86d5b | ||
|
|
0e1f25baf4 | ||
|
|
d0f43ab309 | ||
|
|
93236dc0cb | ||
|
|
79dbdeed66 | ||
|
|
857e206443 | ||
|
|
ff077b9c20 | ||
|
|
5ea719f9a8 | ||
|
|
926cd7b4b2 | ||
|
|
957c71f480 | ||
|
|
6dcccf5425 | ||
|
|
8a3a5bc803 | ||
|
|
27e685d885 | ||
|
|
fd520cee66 | ||
|
|
7464a23b7a | ||
|
|
9809a062cf | ||
|
|
1a39474738 | ||
|
|
5855e326b2 | ||
|
|
ff20f914a1 | ||
|
|
299b012222 | ||
|
|
1237c5669a | ||
|
|
8e18f9f48d | ||
|
|
3c6056e81e | ||
|
|
83a9208edb | ||
|
|
6f230b3431 | ||
|
|
0987f5272a | ||
|
|
321ee665c1 | ||
|
|
446aca17f4 | ||
|
|
0f1b605300 | ||
|
|
dd2b6ba03f | ||
|
|
faba98ea7e | ||
|
|
0da0388d2f | ||
|
|
5f298c68f2 | ||
|
|
c08ba3cef7 | ||
|
|
19b52802c7 | ||
|
|
80a695b596 | ||
|
|
aa206fa5a4 | ||
|
|
0cbb45636f | ||
|
|
6ea8e420a4 | ||
|
|
a8609b12b9 | ||
|
|
b77e820225 | ||
|
|
e0e7c2396b | ||
|
|
969e959c5a | ||
|
|
f8ee037073 | ||
|
|
2ef7eb3cda | ||
|
|
5660f40de0 | ||
|
|
458f71fd1d | ||
|
|
679e7f8df4 | ||
|
|
4d324265a4 | ||
|
|
e01a594d5e | ||
|
|
2e8eaf7112 | ||
|
|
be0ef63b20 | ||
|
|
c968be278a | ||
|
|
ad1249a0e3 | ||
|
|
05ad939eab | ||
|
|
4166cfc03b | ||
|
|
97c3d357c6 | ||
|
|
798423ca95 | ||
|
|
7711eb96d5 | ||
|
|
a510789411 | ||
|
|
cf56ece043 | ||
|
|
56f2f3d4a5 | ||
|
|
9ef702c184 | ||
|
|
c8c94c69ea | ||
|
|
1ffbea6a6a | ||
|
|
e48f04a07d | ||
|
|
0c5b71e2e0 | ||
|
|
c6e48056e0 | ||
|
|
fd42b8b5b6 | ||
|
|
e80dabe34b | ||
|
|
4aac44961d | ||
|
|
e54d707013 | ||
|
|
e541690e7d | ||
|
|
3c97437f9b | ||
|
|
abc3d7a848 | ||
|
|
98ee70f7f4 | ||
|
|
94dad89b7c | ||
|
|
355eae789d | ||
|
|
7298d7d3b0 | ||
|
|
81dede85db | ||
|
|
ee30c1be2a | ||
|
|
03c06bf8a5 | ||
|
|
82ee2da78f | ||
|
|
295c543f5b | ||
|
|
0ff7f66c05 | ||
|
|
d5e1f3bb80 | ||
|
|
47bd0b5e5b | ||
|
|
8b598eb79f | ||
|
|
7ce0e9fafc | ||
|
|
27097c1bf5 | ||
|
|
5e8d94c068 | ||
|
|
2d790aaa4f | ||
|
|
8a6799967f | ||
|
|
ae62e4d7ca | ||
|
|
4bfa5b0984 | ||
|
|
a1d7ed18f9 | ||
|
|
1d2960cfd3 | ||
|
|
dceb83b438 | ||
|
|
4a4cc6c27e | ||
|
|
2d4a911086 | ||
|
|
aab95fec03 | ||
|
|
a0e3f90591 | ||
|
|
46afafc218 | ||
|
|
c59260680c | ||
|
|
0489541c78 | ||
|
|
0be8bfdadd | ||
|
|
fcd776c608 | ||
|
|
a9d7056f29 | ||
|
|
0c9c5b9bd1 | ||
|
|
1b18f88290 | ||
|
|
d7dd4aeb2d | ||
|
|
d8d11b4884 | ||
|
|
6a4058324f | ||
|
|
8f7c20b5d6 | ||
|
|
75ca43777f | ||
|
|
6c1aa0b46a | ||
|
|
c2b5c3b238 | ||
|
|
e7e2385522 | ||
|
|
ac4cd4c513 | ||
|
|
7d7ec95b47 | ||
|
|
a6698d837a | ||
|
|
02729f4161 | ||
|
|
976bac14be | ||
|
|
aaad061104 | ||
|
|
a98ef8786c | ||
|
|
8d3ccbc20a | ||
|
|
f3581f53dc | ||
|
|
5cfea35cdf | ||
|
|
482c8da43a | ||
|
|
8d6340bd8e | ||
|
|
e8b0fefad1 | ||
|
|
515b41a065 | ||
|
|
c65486c0d5 | ||
|
|
aa2ab465f3 | ||
|
|
255c3c4fe1 | ||
|
|
d590abc5c7 | ||
|
|
dbb050fb1d | ||
|
|
ed7d9653b7 | ||
|
|
64fe4ec6c7 | ||
|
|
d8933013bb | ||
|
|
676c9a18c0 | ||
|
|
4124870d70 | ||
|
|
3599bf95a3 | ||
|
|
d0634cc95c | ||
|
|
0564d9ecea | ||
|
|
1d8d5e8a4e | ||
|
|
2a9e121adf | ||
|
|
bc70710caa | ||
|
|
016e5f2c70 | ||
|
|
bd8b9dfbfe | ||
|
|
1d78b430cb | ||
|
|
f35e957627 | ||
|
|
e4eddf0097 | ||
|
|
d478ffe5e7 | ||
|
|
d14c0df801 | ||
|
|
2bbac5b7c6 | ||
|
|
c8828822e4 | ||
|
|
07ceda46b3 | ||
|
|
7e5e0ff5f9 | ||
|
|
4004ce09f3 | ||
|
|
ab986b8251 | ||
|
|
8319dfffe5 | ||
|
|
f41826b3fe | ||
|
|
5fab7bc01e | ||
|
|
bcb05c7aea | ||
|
|
cf1fb1a399 | ||
|
|
d2dea772ea | ||
|
|
ebbab956fa | ||
|
|
b3c00588c1 | ||
|
|
20d2c420ff | ||
|
|
4fa9e906dd | ||
|
|
44346dfbce | ||
|
|
6ec5276807 | ||
|
|
4ccdc3ac5f | ||
|
|
34d3c90a7e | ||
|
|
14ce7fe9e5 | ||
|
|
8d741c30c8 | ||
|
|
6e19bb8197 | ||
|
|
b42086c13a | ||
|
|
99c7c71590 | ||
|
|
431326de86 | ||
|
|
74519a5195 | ||
|
|
b8ef3202e6 | ||
|
|
e673da2399 | ||
|
|
dcf3fb103f | ||
|
|
4b291b649e | ||
|
|
7748d912e0 | ||
|
|
656fc27f3f | ||
|
|
f6054c2626 | ||
|
|
5d122996a2 | ||
|
|
452cfaa3e0 | ||
|
|
8540cb58f3 | ||
|
|
528a6cacc2 | ||
|
|
5a7aff38e3 | ||
|
|
26c3b74d9a | ||
|
|
9d2aa78984 | ||
|
|
ca2e1f8f73 | ||
|
|
027b9ed70a | ||
|
|
f17b2f35f3 | ||
|
|
30f589ed44 | ||
|
|
869ce1f84d | ||
|
|
eda0f569e5 | ||
|
|
ee053e4fa6 | ||
|
|
4a235fe241 | ||
|
|
eef969b30d | ||
|
|
f72bd5d773 | ||
|
|
498fc58a52 | ||
|
|
62dfcd0cbf | ||
|
|
343607d0b8 | ||
|
|
5828a71b1e | ||
|
|
5781f232d7 | ||
|
|
1ba9d47f78 | ||
|
|
346a5bc9cb | ||
|
|
b5513522c7 | ||
|
|
a666d8f3a0 | ||
|
|
56c5104df9 | ||
|
|
dc36ae0be7 | ||
|
|
be626a96ee | ||
|
|
5cba0e200a | ||
|
|
f575231d2c | ||
|
|
f6c032614a | ||
|
|
d53903b7c7 | ||
|
|
0dfecaf0b3 | ||
|
|
10f8adb735 | ||
|
|
7292d24738 | ||
|
|
6a58992570 | ||
|
|
9c8d1073ed | ||
|
|
ef15310379 | ||
|
|
19a38a06fb | ||
|
|
f09f1b69af | ||
|
|
a5bc6229bd | ||
|
|
700b8c3fc6 | ||
|
|
39bd7a27d8 | ||
|
|
7a7144cf62 | ||
|
|
3e154dab26 | ||
|
|
51db9fcc99 | ||
|
|
ce05474725 | ||
|
|
fb0fd62b74 | ||
|
|
798802b9e5 | ||
|
|
0178f4df8e | ||
|
|
ac4cf93772 | ||
|
|
1f51067e34 | ||
|
|
6699ea0987 | ||
|
|
d799d546db | ||
|
|
cc0b3934d2 | ||
|
|
116f064ee5 | ||
|
|
d4f13d3547 | ||
|
|
22670503db | ||
|
|
cf66e5f471 | ||
|
|
77e41f958e | ||
|
|
299955d98d | ||
|
|
55728e1335 | ||
|
|
2c0d49c5cc | ||
|
|
ee3913ce0d | ||
|
|
84b60292f1 | ||
|
|
dba56ebe4f | ||
|
|
bad458ced9 | ||
|
|
d3140c986c | ||
|
|
b893d00cd2 | ||
|
|
b68b423565 | ||
|
|
bd158253a2 | ||
|
|
feb9b1c56b | ||
|
|
f81ee9b2af | ||
|
|
4390205b7f | ||
|
|
93e1b026b2 | ||
|
|
c6e8e5bd8a | ||
|
|
22485db130 | ||
|
|
e2b74afcdb | ||
|
|
d43c368830 | ||
|
|
4fc8aca8e0 | ||
|
|
cb0391bbeb | ||
|
|
d11714e1db | ||
|
|
a8f08f421a | ||
|
|
177c029a88 | ||
|
|
f04d3c0cd8 | ||
|
|
f5da40fdb5 | ||
|
|
790bac5eb2 | ||
|
|
9625b3439c | ||
|
|
861458e0d4 | ||
|
|
5733e3c530 | ||
|
|
b2d8fd1bee | ||
|
|
7eb8c16b98 | ||
|
|
a784253c19 | ||
|
|
efeb385ceb | ||
|
|
fecb51f069 | ||
|
|
0a3c35e686 | ||
|
|
59e6cea925 | ||
|
|
7c7d6b2d5b | ||
|
|
8ae00c0d78 | ||
|
|
d3e7517e19 | ||
|
|
13a1996b01 | ||
|
|
9aef72da30 | ||
|
|
c3fb330be8 | ||
|
|
7fe058d3a0 | ||
|
|
c359c4040d | ||
|
|
9f60c961db | ||
|
|
70c84b91fd | ||
|
|
db0de38120 | ||
|
|
af0705ff77 | ||
|
|
3a4856155d | ||
|
|
b403dd9215 | ||
|
|
4408a95654 | ||
|
|
5065046072 | ||
|
|
f4af0e9a7c | ||
|
|
211fc2d741 | ||
|
|
444fe4c1be | ||
|
|
97457467ee | ||
|
|
64076d935e | ||
|
|
a1aeaba4d4 | ||
|
|
435804ec80 | ||
|
|
dbc88b08c7 | ||
|
|
f3e3502877 | ||
|
|
fbdf800830 | ||
|
|
18a67c8435 | ||
|
|
2876a0a98f | ||
|
|
3b63dbc0cb | ||
|
|
5635cde896 | ||
|
|
2a52fb745b | ||
|
|
26e0b018fa | ||
|
|
7961ddcebb | ||
|
|
1c328c4346 | ||
|
|
a4e4c51684 | ||
|
|
1918a9c6af | ||
|
|
8e4652dc76 | ||
|
|
dfe2976483 | ||
|
|
c0c8787e3c | ||
|
|
59c2ba7c62 | ||
|
|
a410311b6c | ||
|
|
6471947588 | ||
|
|
7680479db4 | ||
|
|
9dbb46c230 | ||
|
|
411c255f0d | ||
|
|
920410dcab | ||
|
|
4ae71af794 | ||
|
|
0036fc6384 | ||
|
|
a637568d79 | ||
|
|
d3bd1c3317 | ||
|
|
5a48380d04 | ||
|
|
67a765141c | ||
|
|
927f4c1093 | ||
|
|
14c8f54a6e | ||
|
|
ac17900017 | ||
|
|
e3625e6183 | ||
|
|
83c9c838c7 | ||
|
|
125790404c | ||
|
|
9e66c3c976 | ||
|
|
5b3bcbd15f | ||
|
|
e54478cf73 | ||
|
|
50db365c2f | ||
|
|
82d7e4a57b | ||
|
|
7bcbffe8e3 | ||
|
|
eb91ea0ec1 | ||
|
|
9c9ace9314 | ||
|
|
348f5dc131 | ||
|
|
0b7d5f02b1 | ||
|
|
31a68f296c | ||
|
|
b22895e4b0 | ||
|
|
e95097f6a6 | ||
|
|
f52eaebf34 | ||
|
|
ae0d6eab31 | ||
|
|
598c3dabb4 | ||
|
|
59559e8f3c | ||
|
|
a60563edf7 | ||
|
|
54f58ed7f5 | ||
|
|
974e215d62 | ||
|
|
4f5ad96c7d | ||
|
|
914bce1c40 | ||
|
|
b0a90944ea | ||
|
|
2d243908ed | ||
|
|
093eadc229 | ||
|
|
ded539d4b9 | ||
|
|
ffbb63676e | ||
|
|
62d6dc001d | ||
|
|
426355f4d7 | ||
|
|
30d98ff488 | ||
|
|
b57625c535 | ||
|
|
4ec13bb043 | ||
|
|
2e4458a9a0 | ||
|
|
cc5eddab8a | ||
|
|
b97e533735 | ||
|
|
d5aeec742e | ||
|
|
47dac1af84 | ||
|
|
0ab01dc034 | ||
|
|
4e41294bd6 | ||
|
|
b8a11a190d | ||
|
|
d18846f7b9 | ||
|
|
8f0762eb42 | ||
|
|
29fcdfe9b9 | ||
|
|
1822034212 | ||
|
|
e8fa03109c | ||
|
|
33e15f37c2 | ||
|
|
24f4bd5e21 | ||
|
|
417da3d5f3 | ||
|
|
ce64f1ac44 | ||
|
|
9cc32f11d9 | ||
|
|
1e7b3f0a68 | ||
|
|
5e63e6c8fe | ||
|
|
8d124c6a03 | ||
|
|
7d44025331 | ||
|
|
8986a874ee | ||
|
|
492cb3e185 | ||
|
|
fbee46279a | ||
|
|
bccd4d6875 | ||
|
|
79003e9de2 | ||
|
|
82bd37476d | ||
|
|
2b062b8986 | ||
|
|
3de1ca66a3 | ||
|
|
e6e25e97e0 | ||
|
|
086d77ff62 | ||
|
|
cfabbdba1e | ||
|
|
2a4c92ce1d | ||
|
|
b7b5f5b4fa | ||
|
|
69bd85b376 | ||
|
|
f869315e82 | ||
|
|
cb71ad1c5d |
No files matched your search
-42
@@ -1,42 +0,0 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
/.zero-build
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
.zero-test-cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
.cursor
|
||||
|
||||
zero
|
||||
zero.exe
|
||||
.*.bun-build
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
||||
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
Default to using Bun instead of Node.js.
|
||||
|
||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||
- Use `bun test` instead of `jest` or `vitest`
|
||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
||||
- Bun automatically loads .env, so don't use dotenv.
|
||||
|
||||
## APIs
|
||||
|
||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||
- `WebSocket` is built-in. Don't use `ws`.
|
||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||
- Bun.$`ls` instead of execa.
|
||||
|
||||
## Testing
|
||||
|
||||
Use `bun test` to run tests.
|
||||
|
||||
```ts#index.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("hello world", () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||
|
||||
Server:
|
||||
|
||||
```ts#index.ts
|
||||
import index from "./index.html"
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/users/:id": {
|
||||
GET: (req) => {
|
||||
return new Response(JSON.stringify({ id: req.params.id }));
|
||||
},
|
||||
},
|
||||
},
|
||||
// optional websocket support
|
||||
websocket: {
|
||||
open: (ws) => {
|
||||
ws.send("Hello, world!");
|
||||
},
|
||||
message: (ws, message) => {
|
||||
ws.send(message);
|
||||
},
|
||||
close: (ws) => {
|
||||
// handle close
|
||||
}
|
||||
},
|
||||
development: {
|
||||
hmr: true,
|
||||
console: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||
|
||||
```html#index.html
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
<script type="module" src="./frontend.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
With the following `frontend.tsx`:
|
||||
|
||||
```tsx#frontend.tsx
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
// import .css files directly and it works
|
||||
import './index.css';
|
||||
|
||||
const root = createRoot(document.body);
|
||||
|
||||
export default function Frontend() {
|
||||
return <h1>Hello, world!</h1>;
|
||||
}
|
||||
|
||||
root.render(<Frontend />);
|
||||
```
|
||||
|
||||
Then, run index.ts
|
||||
|
||||
```sh
|
||||
bun --hot ./index.ts
|
||||
```
|
||||
|
||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for your interest in improving this project.
|
||||
|
||||
This project is currently in a controlled development phase. During this period,
|
||||
most implementation work will be completed by the core team while the application
|
||||
stabilizes. Community participation is still welcome, especially through bug
|
||||
reports, feature requests, reproduction details, logs, and discussion on existing
|
||||
issues.
|
||||
|
||||
Please read this policy before opening a pull request. Pull requests that do not
|
||||
follow it may be closed without review.
|
||||
|
||||
## Current Contribution Policy
|
||||
|
||||
The project is not accepting unsolicited community pull requests by default while
|
||||
the application is still reaching a stable baseline.
|
||||
|
||||
Community pull requests must be tied to an existing issue that has already been
|
||||
reviewed and approved by the core team. Approval is shown by the `issue-approved`
|
||||
label on the related issue.
|
||||
|
||||
If a pull request is opened before the related issue has the `issue-approved`
|
||||
label, the pull request will be closed without review. Maintainers may ask the
|
||||
author to continue the discussion on the issue instead.
|
||||
|
||||
Team members may open pull requests as part of the internal development cycle.
|
||||
This does not indicate that the same issue is open for community implementation
|
||||
unless the issue has been explicitly approved for outside contribution.
|
||||
|
||||
## How to Contribute
|
||||
|
||||
There are three primary ways to contribute:
|
||||
|
||||
1. Open a bug report with clear reproduction steps.
|
||||
2. Open a feature request describing the problem, use case, and expected behavior.
|
||||
3. Add useful context to an existing issue, such as logs, screenshots, examples,
|
||||
affected versions, or reproduction cases.
|
||||
|
||||
Please search existing issues before opening a new one. Duplicate or low-signal
|
||||
issues may be closed so maintainers can focus on actionable reports.
|
||||
|
||||
## Issue First Requirement
|
||||
|
||||
All community pull requests require an approved parent issue.
|
||||
|
||||
Before starting implementation work:
|
||||
|
||||
1. Open an issue using the appropriate issue template.
|
||||
2. Describe the bug, feature request, or functional change clearly.
|
||||
3. Wait for the core team to review the issue.
|
||||
4. Do not open a pull request unless the issue has the `issue-approved` label.
|
||||
|
||||
An approved issue means only that the specific issue has been accepted for work.
|
||||
It does not approve unrelated future work, future pull requests, or future issues
|
||||
from the same author. Each issue is reviewed case by case.
|
||||
|
||||
## Pull Request Requirements
|
||||
|
||||
Before opening a community pull request, make sure all of the following are true:
|
||||
|
||||
- The pull request links to an existing issue.
|
||||
- The linked issue already has the `issue-approved` label.
|
||||
- The pull request is focused on only that approved issue.
|
||||
- The pull request explains what changed and why.
|
||||
- The pull request includes tests or verification notes when appropriate.
|
||||
- UI changes include screenshots or video when possible.
|
||||
- The pull request does not include fixes, features, refactors, formatting
|
||||
changes, or other work outside the approved issue scope unless that expansion
|
||||
was discussed with and approved by project team members.
|
||||
|
||||
Use `Fixes #123`, `Closes #123`, or another clear link in the pull request
|
||||
description so maintainers can confirm the approved parent issue.
|
||||
|
||||
Pull requests that include unrelated changes, broad rewrites, formatting-only
|
||||
changes, or unapproved feature work may be closed without review. Scope drift
|
||||
will be rejected or called out for correction before a pull request can be
|
||||
approved.
|
||||
|
||||
## Project Consistency
|
||||
|
||||
Pull requests should stay within the project's existing technical direction.
|
||||
Changes that introduce a new implementation language, shift existing code away
|
||||
from the core language used by the project, or add a new language/runtime to the
|
||||
codebase are unlikely to be accepted unless they were discussed and approved by
|
||||
maintainers before implementation.
|
||||
|
||||
The same applies to dependency changes. Pull requests that replace, rebase, or
|
||||
significantly restructure the project's dependencies without prior discussion are
|
||||
likely to be rejected. A preference-based explanation such as "this is better" is
|
||||
not enough. Dependency changes need a clear project benefit, such as fixing a
|
||||
specific bug, addressing a security issue, improving compatibility, or supporting
|
||||
an approved feature.
|
||||
|
||||
## What We Prioritize
|
||||
|
||||
The core team will prioritize:
|
||||
|
||||
- Bug reports with clear reproduction steps.
|
||||
- Functional defects that affect current users.
|
||||
- Feature requests that align with the project roadmap.
|
||||
- Requests that explain the user problem and expected behavior.
|
||||
- Reports with logs, screenshots, test cases, or other concrete evidence.
|
||||
|
||||
Most approved implementation work will be completed by team members until the
|
||||
project reaches a more stable status.
|
||||
|
||||
## What We Are Not Prioritizing
|
||||
|
||||
The project is not currently prioritizing:
|
||||
|
||||
- Unsolicited implementation pull requests.
|
||||
- Pull requests without an approved parent issue.
|
||||
- Typo-only changes.
|
||||
- Formatting-only changes.
|
||||
- Refactors without a clear functional benefit.
|
||||
- Large rewrites that were not discussed with maintainers first.
|
||||
- New features that have not been approved through an issue.
|
||||
|
||||
## Submitting a Bug Report
|
||||
|
||||
When filing a bug report, please include:
|
||||
|
||||
- The version, branch, or commit you are using.
|
||||
- Your operating system and relevant environment details.
|
||||
- Steps to reproduce the issue.
|
||||
- Expected behavior.
|
||||
- Actual behavior.
|
||||
- Relevant logs, screenshots, or error messages.
|
||||
- A minimal reproduction case when possible.
|
||||
|
||||
Clear bug reports are the most useful way to help the project during this phase.
|
||||
|
||||
## Submitting a Feature Request
|
||||
|
||||
When filing a feature request, please include:
|
||||
|
||||
- The problem you are trying to solve.
|
||||
- The behavior or functionality you are requesting.
|
||||
- Why the request belongs in this project.
|
||||
- Any alternatives or workarounds you have considered.
|
||||
- Examples, screenshots, or prior discussion if relevant.
|
||||
|
||||
Please do not begin implementation until the issue has been reviewed and marked
|
||||
with the `issue-approved` label.
|
||||
|
||||
## Review Expectations
|
||||
|
||||
Maintainers may close issues or pull requests that do not follow these guidelines.
|
||||
This is not a judgment on the contributor; it is part of keeping the development
|
||||
cycle focused while the project stabilizes.
|
||||
|
||||
If an issue or pull request is closed, the best next step is usually to clarify
|
||||
the issue, provide more evidence, or wait for the project to reach a more open
|
||||
contribution phase.
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you are unsure whether a change is appropriate, open an issue first and ask.
|
||||
Please keep discussion focused, specific, and respectful of maintainer time.
|
||||
@@ -1,90 +1,45 @@
|
||||
# zero
|
||||
# Origin session history
|
||||
|
||||
To install dependencies:
|
||||
This branch is written automatically by [Origin](https://getorigin.io). It is
|
||||
**not part of your codebase** — nothing here is built, imported, or deployed. It
|
||||
records which AI agent wrote the code on the other branches, and the prompt
|
||||
behind each change.
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
You don't need Origin installed to read any of it. Plain `git` is enough.
|
||||
|
||||
To run:
|
||||
## Layout
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
sessions/<session-id>/metadata.json model, cost, tokens, duration, commits
|
||||
sessions/<session-id>/prompts.md the prompts, in order
|
||||
sessions/<session-id>/changes.json per-prompt diffs and files touched
|
||||
|
||||
To list built-in provider catalog entries:
|
||||
## Reading it
|
||||
|
||||
```bash
|
||||
bun run src/index.ts providers catalog
|
||||
```
|
||||
List the sessions:
|
||||
|
||||
## Provider catalog
|
||||
git ls-tree --name-only origin/origin-sessions:sessions/
|
||||
|
||||
Provider, gateway, and local endpoint catalog entries live in
|
||||
`src/providers/catalog/definitions`. Global model metadata lives in
|
||||
`src/providers/catalog/models`.
|
||||
Read the prompts behind one:
|
||||
|
||||
Top-level providers are services that self-publish model families, such as
|
||||
OpenAI with GPT/o-series, Anthropic with Claude/Opus, Google with Gemini, and
|
||||
DeepSeek with DeepSeek models. Gateways should usually reference those global
|
||||
models instead of redefining them.
|
||||
git show origin/origin-sessions:sessions/<session-id>/prompts.md
|
||||
|
||||
Minimal gateway example:
|
||||
## Per-commit attribution (one extra step)
|
||||
|
||||
```toml
|
||||
id = "my-gateway"
|
||||
name = "My Gateway"
|
||||
kind = "gateway"
|
||||
description = "OpenAI-compatible gateway"
|
||||
category = "aggregating"
|
||||
baseURL = "https://api.example.com/v1"
|
||||
defaultModel = "vendor/model-name"
|
||||
apiKeyRequired = true
|
||||
credentialEnvVars = ["MY_GATEWAY_API_KEY"]
|
||||
costCurrency = "USD"
|
||||
Origin also writes a git note per commit, so `git log` can show you which agent
|
||||
wrote it and why. Notes live in `refs/notes/origin`, and **`git clone` does not
|
||||
fetch them** — that's a git default, not an Origin choice. Bring them down once:
|
||||
|
||||
[setup]
|
||||
requiresAuth = true
|
||||
authMode = "api-key"
|
||||
git fetch origin refs/notes/origin:refs/notes/origin
|
||||
|
||||
[transportConfig]
|
||||
kind = "openai-compatible"
|
||||
maxTokensField = "max_completion_tokens"
|
||||
removeBodyFields = ["store", "stream_options"]
|
||||
authHeader.name = "authorization"
|
||||
authHeader.scheme = "bearer"
|
||||
headers.X-Trace-Source = "zero"
|
||||
headers.X-Tenant = "$MY_GATEWAY_TENANT"
|
||||
Then, with plain git:
|
||||
|
||||
[catalog]
|
||||
source = "hybrid"
|
||||
discoveryCacheTtl = "1h"
|
||||
discoveryRefreshMode = "manual"
|
||||
discovery.kind = "openai-compatible"
|
||||
discovery.requiresAuth = true
|
||||
discovery.path = "/models"
|
||||
git log --show-notes=origin
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "gpt-4o"
|
||||
apiName = "openai/gpt-4o"
|
||||
contextWindow = 32000
|
||||
maxOutputTokens = 4096
|
||||
cost.inputPerMillion = 2.50
|
||||
cost.outputPerMillion = 10.00
|
||||
To keep them arriving on every ordinary `git pull`, add the refspec:
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "claude-sonnet-4"
|
||||
apiName = "anthropic/claude-sonnet-4"
|
||||
contextWindow = 200000
|
||||
maxOutputTokens = 64000
|
||||
cost.inputPerMillion = 3.00
|
||||
cost.outputPerMillion = 15.00
|
||||
```
|
||||
git config --add remote.origin.fetch '+refs/notes/origin:refs/notes/origin-remote'
|
||||
|
||||
Detailed docs:
|
||||
|
||||
- [Provider definitions](docs/provider-catalog/providers.md)
|
||||
- [Model catalog entries](docs/provider-catalog/models.md)
|
||||
- [Examples](docs/provider-catalog/examples.md)
|
||||
|
||||
This project was created using `bun init` in bun v1.3.11. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
(That stages them into `refs/notes/origin-remote`. Origin merges the staging ref
|
||||
into `refs/notes/origin` so your own local notes are never overwritten — mapping
|
||||
the remote straight onto `refs/notes/origin` with a forced refspec would silently
|
||||
destroy any note you hadn't pushed yet.)
|
||||
@@ -1,280 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "zero",
|
||||
"dependencies": {
|
||||
"commander": "^15.0.0",
|
||||
"execa": "^9.6.1",
|
||||
"ink": "^7.0.4",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"openai": "^6.39.1",
|
||||
"react": "^19.2.6",
|
||||
"react-devtools-core": "^7.0.1",
|
||||
"shiki": "^4.1.0",
|
||||
"smol-toml": "^1.6.1",
|
||||
"zod": "^4.4.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/react": "^19.2.15",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@alcalzone/ansi-tokenize": ["@alcalzone/[email protected]", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="],
|
||||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/[email protected]", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ=="],
|
||||
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg=="],
|
||||
|
||||
"@shikijs/langs": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg=="],
|
||||
|
||||
"@shikijs/primitive": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="],
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/[email protected]", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/[email protected]", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
"@types/hast": ["@types/[email protected]", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/mdast": ["@types/[email protected]", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
|
||||
|
||||
"@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="],
|
||||
|
||||
"@types/unist": ["@types/[email protected]", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/[email protected]", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="],
|
||||
|
||||
"ansi-escapes": ["[email protected]", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
|
||||
|
||||
"ansi-regex": ["[email protected]", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"ansi-styles": ["[email protected]", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"auto-bind": ["[email protected]", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
||||
|
||||
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||
|
||||
"ccount": ["[email protected]", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"chalk": ["[email protected]", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"character-entities-html4": ["[email protected]", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
||||
"character-entities-legacy": ["[email protected]", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
|
||||
"cli-boxes": ["[email protected]", "", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="],
|
||||
|
||||
"cli-cursor": ["[email protected]", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
|
||||
|
||||
"cli-spinners": ["[email protected]", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
|
||||
|
||||
"cli-truncate": ["[email protected]", "", { "dependencies": { "slice-ansi": "^9.0.0", "string-width": "^8.2.0" } }, "sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA=="],
|
||||
|
||||
"code-excerpt": ["[email protected]", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
|
||||
|
||||
"comma-separated-tokens": ["[email protected]", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["[email protected]", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
|
||||
|
||||
"convert-to-spaces": ["[email protected]", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
|
||||
|
||||
"cross-spawn": ["[email protected]", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"csstype": ["[email protected]", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"dequal": ["[email protected]", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"devlop": ["[email protected]", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"environment": ["[email protected]", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
|
||||
|
||||
"es-toolkit": ["[email protected]", "", {}, "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw=="],
|
||||
|
||||
"escape-string-regexp": ["[email protected]", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
|
||||
|
||||
"execa": ["[email protected]", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
|
||||
|
||||
"figures": ["[email protected]", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
||||
|
||||
"get-east-asian-width": ["[email protected]", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||
|
||||
"get-stream": ["[email protected]", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
|
||||
|
||||
"hast-util-to-html": ["[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||
|
||||
"hast-util-whitespace": ["[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"html-void-elements": ["[email protected]", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"human-signals": ["[email protected]", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
||||
|
||||
"indent-string": ["[email protected]", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
|
||||
|
||||
"ink": ["[email protected]", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.3", "auto-bind": "^5.0.1", "chalk": "^5.6.2", "cli-boxes": "^4.0.1", "cli-cursor": "^4.0.0", "cli-truncate": "^6.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.45.1", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^9.0.0", "stack-utils": "^2.0.6", "string-width": "^8.2.0", "terminal-size": "^4.0.1", "type-fest": "^5.5.0", "widest-line": "^6.0.0", "wrap-ansi": "^10.0.0", "ws": "^8.20.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.2.0", "react": ">=19.2.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-4wsM/gMKOT2ZANNTJibI6I9IcwBfobqv/CgaDcwvOaCREZIQxo3iGQS7qPHa2hmA67NYltZWCMtBDELB/mcbJQ=="],
|
||||
|
||||
"ink-spinner": ["[email protected]", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="],
|
||||
|
||||
"ink-text-input": ["[email protected]", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="],
|
||||
|
||||
"is-fullwidth-code-point": ["[email protected]", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
|
||||
|
||||
"is-in-ci": ["[email protected]", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="],
|
||||
|
||||
"is-plain-obj": ["[email protected]", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-stream": ["[email protected]", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||
|
||||
"is-unicode-supported": ["[email protected]", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"isexe": ["[email protected]", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"mdast-util-to-hast": ["[email protected]", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||
|
||||
"micromark-util-character": ["[email protected]", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-encode": ["[email protected]", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["[email protected]", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-symbol": ["[email protected]", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["[email protected]", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"mimic-fn": ["[email protected]", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"npm-run-path": ["[email protected]", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
|
||||
|
||||
"onetime": ["[email protected]", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"oniguruma-parser": ["[email protected]", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
|
||||
|
||||
"oniguruma-to-es": ["[email protected]", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
|
||||
|
||||
"openai": ["[email protected]", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-z3dO9fEWOXBzlXynVb/xZ/tujzUjFWQWn3C0n0mw6Vo0zJTbEkaN4b2cLWjhJ6haJQx8LlREoafHRl+Gu/Hl+A=="],
|
||||
|
||||
"parse-ms": ["[email protected]", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
|
||||
|
||||
"patch-console": ["[email protected]", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
|
||||
|
||||
"path-key": ["[email protected]", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"pretty-ms": ["[email protected]", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
|
||||
|
||||
"property-information": ["[email protected]", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
"react": ["[email protected]", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
|
||||
|
||||
"react-devtools-core": ["[email protected]", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw=="],
|
||||
|
||||
"react-reconciler": ["[email protected]", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="],
|
||||
|
||||
"regex": ["[email protected]", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||
|
||||
"regex-recursion": ["[email protected]", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||
|
||||
"regex-utilities": ["[email protected]", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||
|
||||
"restore-cursor": ["[email protected]", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
||||
|
||||
"scheduler": ["[email protected]", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"shebang-command": ["[email protected]", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["[email protected]", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"shell-quote": ["[email protected]", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="],
|
||||
|
||||
"shiki": ["[email protected]", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/engine-javascript": "4.1.0", "@shikijs/engine-oniguruma": "4.1.0", "@shikijs/langs": "4.1.0", "@shikijs/themes": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q=="],
|
||||
|
||||
"signal-exit": ["[email protected]", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"slice-ansi": ["[email protected]", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA=="],
|
||||
|
||||
"smol-toml": ["[email protected]", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="],
|
||||
|
||||
"space-separated-tokens": ["[email protected]", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"stack-utils": ["[email protected]", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
|
||||
|
||||
"string-width": ["[email protected]", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
|
||||
|
||||
"stringify-entities": ["[email protected]", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||
|
||||
"strip-final-newline": ["[email protected]", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
||||
|
||||
"tagged-tag": ["[email protected]", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
||||
|
||||
"terminal-size": ["[email protected]", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="],
|
||||
|
||||
"trim-lines": ["[email protected]", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"type-fest": ["[email protected]", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="],
|
||||
|
||||
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["[email protected]", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||
|
||||
"unicorn-magic": ["[email protected]", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
"unist-util-is": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-position": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-stringify-position": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
|
||||
"unist-util-visit-parents": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||
|
||||
"vfile": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-message": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"which": ["[email protected]", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"widest-line": ["[email protected]", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="],
|
||||
|
||||
"wrap-ansi": ["[email protected]", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="],
|
||||
|
||||
"ws": ["[email protected]", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||
|
||||
"yoctocolors": ["[email protected]", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
||||
|
||||
"yoga-layout": ["[email protected]", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
|
||||
|
||||
"zod": ["[email protected]", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||
|
||||
"zwitch": ["[email protected]", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"ink/signal-exit": ["[email protected]", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"ink-text-input/type-fest": ["[email protected]", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"npm-run-path/path-key": ["[email protected]", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"react-devtools-core/ws": ["[email protected]", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA=="],
|
||||
|
||||
"restore-cursor/signal-exit": ["[email protected]", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
# Provider catalog examples
|
||||
|
||||
Top-level providers self-publish model families, such as OpenAI with
|
||||
GPT/o-series, Anthropic with Claude/Opus, Google with Gemini, and DeepSeek with
|
||||
DeepSeek models. Gateways and local endpoints normally reference those global
|
||||
models with `globalModelId`.
|
||||
|
||||
## Gateway with discovery and curated models
|
||||
|
||||
```toml
|
||||
id = "my-gateway"
|
||||
name = "My Gateway"
|
||||
kind = "gateway"
|
||||
description = "OpenAI-compatible model gateway"
|
||||
category = "aggregating"
|
||||
baseURL = "https://api.example.com/v1"
|
||||
defaultModel = "vendor/model-name"
|
||||
supportsModelRouting = true
|
||||
apiKeyLabel = "My Gateway API key"
|
||||
apiKeyPlaceholder = "gw_live_..."
|
||||
apiKeyRequired = true
|
||||
credentialEnvVars = ["MY_GATEWAY_API_KEY"]
|
||||
costCurrency = "USD"
|
||||
|
||||
[setup]
|
||||
requiresAuth = true
|
||||
authMode = "api-key"
|
||||
|
||||
[validation]
|
||||
kind = "credential-env"
|
||||
missingCredentialMessage = "An API key is required."
|
||||
matchBaseUrlHosts = ["api.example.com"]
|
||||
|
||||
[transportConfig]
|
||||
kind = "openai-compatible"
|
||||
maxTokensField = "max_completion_tokens"
|
||||
removeBodyFields = ["store", "stream_options"]
|
||||
authHeader.name = "authorization"
|
||||
authHeader.scheme = "bearer"
|
||||
headers.X-Trace-Source = "zero"
|
||||
headers.X-Tenant = "$MY_GATEWAY_TENANT"
|
||||
|
||||
[catalog]
|
||||
source = "hybrid"
|
||||
discoveryCacheTtl = "1h"
|
||||
discoveryRefreshMode = "manual"
|
||||
allowManualRefresh = true
|
||||
discovery.kind = "openai-compatible"
|
||||
discovery.requiresAuth = true
|
||||
discovery.path = "/models"
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "gpt-4o"
|
||||
apiName = "openai/gpt-4o"
|
||||
contextWindow = 32000
|
||||
maxOutputTokens = 4096
|
||||
cost.inputPerMillion = 2.50
|
||||
cost.outputPerMillion = 10.00
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "claude-sonnet-4"
|
||||
apiName = "anthropic/claude-sonnet-4"
|
||||
contextWindow = 200000
|
||||
maxOutputTokens = 64000
|
||||
cost.inputPerMillion = 3.00
|
||||
cost.outputPerMillion = 15.00
|
||||
|
||||
[preset]
|
||||
id = "my-gateway"
|
||||
description = "My Gateway hosted models"
|
||||
fallbackBaseUrl = "https://api.example.com/v1"
|
||||
fallbackModel = "vendor/model-name"
|
||||
badge.text = "HOSTED"
|
||||
badge.color = "success"
|
||||
```
|
||||
|
||||
## Local Ollama-style provider
|
||||
|
||||
```toml
|
||||
id = "ollama"
|
||||
name = "Ollama"
|
||||
kind = "localhost"
|
||||
description = "Local models via Ollama"
|
||||
category = "local"
|
||||
baseURL = "http://localhost:11434/v1"
|
||||
defaultModel = "llama3.2"
|
||||
apiKeyRequired = false
|
||||
|
||||
[setup]
|
||||
requiresAuth = false
|
||||
authMode = "none"
|
||||
|
||||
[startup]
|
||||
autoDetectable = true
|
||||
probeReadiness = "openai-compatible-models"
|
||||
|
||||
[transportConfig]
|
||||
kind = "openai-compatible"
|
||||
|
||||
[catalog]
|
||||
source = "dynamic"
|
||||
discoveryCacheTtl = "5m"
|
||||
discoveryRefreshMode = "on-open"
|
||||
allowManualRefresh = true
|
||||
discovery.kind = "ollama"
|
||||
discovery.requiresAuth = false
|
||||
|
||||
[[catalog.models]]
|
||||
id = "llama3.2"
|
||||
name = "Llama 3.2"
|
||||
tier = "local"
|
||||
|
||||
[[catalog.models]]
|
||||
id = "mistral"
|
||||
name = "Mistral"
|
||||
tier = "local"
|
||||
```
|
||||
|
||||
## First-party provider
|
||||
|
||||
```toml
|
||||
id = "openai"
|
||||
name = "OpenAI"
|
||||
kind = "provider"
|
||||
description = "OpenAI direct API"
|
||||
category = "hosted"
|
||||
baseURL = "https://api.openai.com/v1"
|
||||
defaultModel = "gpt-4o"
|
||||
isFirstParty = true
|
||||
apiKeyRequired = true
|
||||
credentialEnvVars = ["OPENAI_API_KEY"]
|
||||
costCurrency = "USD"
|
||||
|
||||
[setup]
|
||||
requiresAuth = true
|
||||
authMode = "api-key"
|
||||
|
||||
[transportConfig]
|
||||
kind = "openai-compatible"
|
||||
|
||||
[catalog]
|
||||
source = "hybrid"
|
||||
discoveryCacheTtl = "1h"
|
||||
discoveryRefreshMode = "manual"
|
||||
allowManualRefresh = true
|
||||
discovery.kind = "openai-compatible"
|
||||
discovery.requiresAuth = true
|
||||
discovery.path = "/models"
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "gpt-4o"
|
||||
cost.inputPerMillion = 2.50
|
||||
cost.outputPerMillion = 10.00
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "gpt-4o-mini"
|
||||
cost.inputPerMillion = 0.15
|
||||
cost.outputPerMillion = 0.60
|
||||
```
|
||||
@@ -1,129 +0,0 @@
|
||||
# Provider model catalog
|
||||
|
||||
Global model metadata lives in `src/providers/catalog/models`. Files use TOML
|
||||
`[[model]]` entries. Global models describe identity, ownership, limits, and
|
||||
capabilities only; all pricing belongs on provider `[[catalog.models]]` route
|
||||
entries.
|
||||
|
||||
Only top-level providers should add global model metadata. A top-level provider
|
||||
self-publishes the model family, such as OpenAI with GPT/o-series, Anthropic
|
||||
with Claude/Opus, Google with Gemini, or DeepSeek with DeepSeek models.
|
||||
|
||||
Each model entry must stay as one TOML block. Nested model subtables such as
|
||||
`[model.cost]`, `[model.capabilities]`, or `[catalog.models.cost]` are not
|
||||
allowed; use dotted keys inside the `[[model]]` or `[[catalog.models]]` block.
|
||||
|
||||
## Global model example
|
||||
|
||||
Use one `[[model]]` block per global model owned by a top-level provider. Add
|
||||
additional models by adding additional `[[model]]` blocks in the same family
|
||||
file.
|
||||
|
||||
```toml
|
||||
[[model]]
|
||||
id = "gpt-4o"
|
||||
name = "GPT-4o"
|
||||
apiName = "gpt-4o"
|
||||
ownerProviderId = "openai"
|
||||
tier = "first-party"
|
||||
description = "OpenAI flagship multimodal model"
|
||||
classification = ["chat", "vision", "coding"]
|
||||
contextWindow = 128000
|
||||
maxOutputTokens = 16384
|
||||
defaultTemperature = 0.7
|
||||
temperatureRange.min = 0
|
||||
temperatureRange.max = 2
|
||||
capabilities.supportsVision = true
|
||||
capabilities.supportsStreaming = true
|
||||
capabilities.supportsFunctionCalling = true
|
||||
capabilities.supportsJsonMode = true
|
||||
capabilities.supportsReasoning = false
|
||||
capabilities.supportsPreciseTokenCount = true
|
||||
capabilities.supportsEmbeddings = false
|
||||
capabilities.supportsTemperature = true
|
||||
notes = "Optional maintainer note."
|
||||
|
||||
[[model]]
|
||||
id = "gpt-4o-mini"
|
||||
name = "GPT-4o Mini"
|
||||
apiName = "gpt-4o-mini"
|
||||
ownerProviderId = "openai"
|
||||
tier = "first-party"
|
||||
description = "OpenAI small multimodal model"
|
||||
classification = ["chat", "vision"]
|
||||
contextWindow = 128000
|
||||
maxOutputTokens = 16384
|
||||
capabilities.supportsVision = true
|
||||
capabilities.supportsStreaming = true
|
||||
capabilities.supportsFunctionCalling = true
|
||||
capabilities.supportsJsonMode = true
|
||||
capabilities.supportsReasoning = false
|
||||
```
|
||||
|
||||
## Model fields
|
||||
|
||||
Global `[[model]]` entries and provider `[[catalog.models]]` entries share the
|
||||
identity, capability, and limit fields below. Provider entries may also define
|
||||
route-specific pricing with `cost.*`.
|
||||
|
||||
| Field | Required | Accepted values or behavior |
|
||||
|-------|----------|-----------------------------|
|
||||
| `id` | Yes for global models; no for provider entries with `globalModelId` | Stable catalog ID. |
|
||||
| `apiName` | No | Exact model name sent to the API. Defaults to `id`; with `globalModelId`, defaults to the global model ID unless overridden. |
|
||||
| `globalModelId` | No | Inherits metadata from a global model, then applies provider-entry overrides. |
|
||||
| `ownerProviderId` | No | First-party provider that owns a global model. Used by `isFirstParty` providers. |
|
||||
| `name` / `label` | No | Display metadata. |
|
||||
| `default` / `hidden` | No | Optional selection metadata. |
|
||||
| `classification` | No | Any of `chat`, `reasoning`, `vision`, and `coding`. |
|
||||
| `tier` | No | `first-party`, `hosted`, `local`, or `community`. |
|
||||
| `contextWindow` | No | Input/context token limit for this model or route. |
|
||||
| `maxOutputTokens` | No | Output token limit for this model or route. |
|
||||
| `defaultTemperature` | No | Suggested temperature default. |
|
||||
| `temperatureRange.min` / `temperatureRange.max` | No | Supported temperature bounds. |
|
||||
| `capabilities.*` | No | Feature flags listed below. |
|
||||
| `transportOverrides.*` | No | Per-model transport overrides merged over provider `transportConfig`. |
|
||||
| `notes` | No | Free-form maintainer note. |
|
||||
|
||||
## Provider model pricing and overrides
|
||||
|
||||
When a provider or gateway exposes a global model under a provider-specific
|
||||
name, limit, or price, keep those route-specific values in that provider's
|
||||
single `[[catalog.models]]` block. Put the shared pricing currency once at the
|
||||
provider top level with `costCurrency`.
|
||||
|
||||
```toml
|
||||
costCurrency = "USD"
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "claude-sonnet-4"
|
||||
apiName = "anthropic/claude-sonnet-4"
|
||||
contextWindow = 200000
|
||||
maxOutputTokens = 64000
|
||||
capabilities.supportsReasoning = true
|
||||
cost.inputPerMillion = 3.00
|
||||
cost.outputPerMillion = 15.00
|
||||
cost.cachePerMillion = 0.30
|
||||
transportOverrides.maxTokensField = "max_tokens"
|
||||
transportOverrides.removeBodyFields = ["store"]
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "deepseek-v3"
|
||||
apiName = "deepseek/deepseek-chat"
|
||||
contextWindow = 65536
|
||||
maxOutputTokens = 8192
|
||||
cost.inputPerMillion = 0.14
|
||||
cost.outputPerMillion = 0.28
|
||||
```
|
||||
|
||||
`capabilities` flags are `supportsVision`, `supportsStreaming`,
|
||||
`supportsFunctionCalling`, `supportsJsonMode`, `supportsReasoning`,
|
||||
`supportsPreciseTokenCount`, `supportsEmbeddings`, and
|
||||
`supportsTemperature`.
|
||||
|
||||
Provider model `cost` requires `inputPerMillion` and `outputPerMillion` when
|
||||
present. `cachePerMillion` is optional. Do not put `cost` or `costCurrency` in
|
||||
global model files.
|
||||
|
||||
`transportOverrides` accepts the same fields as `transportConfig`. Header
|
||||
overrides are merged with provider headers, and `removeBodyFields` is unioned
|
||||
with provider-level removals.
|
||||
@@ -1,213 +0,0 @@
|
||||
# Provider catalog definitions
|
||||
|
||||
Provider catalog entries are TOML files in `src/providers/catalog/definitions`.
|
||||
Use one definition file for each API provider, gateway, or local endpoint.
|
||||
|
||||
A top-level provider is the company or service that self-publishes a model
|
||||
family. Examples: OpenAI publishes GPT and o-series models, Anthropic publishes
|
||||
Claude and Opus/Sonnet models, Google publishes Gemini models, and DeepSeek
|
||||
publishes DeepSeek models.
|
||||
|
||||
Gateways and hosted proxies do not own those global model families. They expose
|
||||
routes to models owned by top-level providers, so their catalog entries should
|
||||
usually reference global models with `globalModelId`.
|
||||
|
||||
## Pick The Right File
|
||||
|
||||
| What you're adding | Files to touch | Brand/provider examples |
|
||||
|--------------------|----------------|-------------------------|
|
||||
| Top-level provider with no new global models | `definitions/<provider>.toml` | OpenAI after GPT models already exist, Anthropic after Claude models already exist |
|
||||
| Top-level provider with owned global models | `definitions/<provider>.toml` and `models/<family>.toml` | OpenAI + GPT/o-series, Anthropic + Claude/Opus, Google + Gemini, DeepSeek + DeepSeek |
|
||||
| Gateway or hosted proxy | `definitions/<gateway>.toml` | OpenRouter, OpenGateway, Groq |
|
||||
| Local endpoint | `definitions/<local>.toml` | Ollama, LM Studio |
|
||||
|
||||
## Recommended Shape
|
||||
|
||||
Keep credential env vars at the provider top level:
|
||||
|
||||
```toml
|
||||
credentialEnvVars = ["OPENAI_API_KEY"]
|
||||
```
|
||||
|
||||
The catalog normalizer copies that value into setup, validation, and preset
|
||||
metadata for runtime consumers. Do not repeat the same env var in every section.
|
||||
|
||||
Keep these nested objects inside one TOML block with dotted keys:
|
||||
|
||||
```toml
|
||||
[transportConfig]
|
||||
kind = "openai-compatible"
|
||||
authHeader.name = "authorization"
|
||||
headers.X-Trace-Source = "zero"
|
||||
|
||||
[catalog]
|
||||
source = "hybrid"
|
||||
discovery.kind = "openai-compatible"
|
||||
|
||||
[preset]
|
||||
id = "my-provider"
|
||||
badge.text = "HOSTED"
|
||||
```
|
||||
|
||||
Use provider-level `costCurrency` for pricing currency. Individual provider
|
||||
model routes can define numeric `cost.*` fields, but global model files should
|
||||
not define pricing.
|
||||
|
||||
## Required Provider Fields
|
||||
|
||||
Every definition needs these top-level fields:
|
||||
|
||||
| Field | Purpose |
|
||||
|-------|---------|
|
||||
| `id` | Stable provider ID. |
|
||||
| `name` | Human-readable provider name. |
|
||||
| `kind` | `provider`, `gateway`, or `localhost`. |
|
||||
| `description` | Short provider description. |
|
||||
| `baseURL` | Provider API base URL. |
|
||||
| `defaultModel` | Model used when the user does not choose one. |
|
||||
|
||||
## Common Optional Fields
|
||||
|
||||
| Field | Purpose |
|
||||
|-------|---------|
|
||||
| `category` | `local`, `hosted`, or `aggregating`. |
|
||||
| `vendorId` | Inherit another provider's `transportConfig` when this definition omits one. |
|
||||
| `isFirstParty` | Auto-add owned global models from `models/*.toml`; use this for top-level providers. |
|
||||
| `supportsModelRouting` | Marks providers that can route multiple model families. |
|
||||
| `apiKeyLabel` / `apiKeyPlaceholder` | UI labels for credential entry. |
|
||||
| `apiKeyRequired` | `false` for local/no-auth providers; otherwise defaults from `setup.requiresAuth`. |
|
||||
| `credentialEnvVars` | Canonical env vars that may contain provider credentials. |
|
||||
| `costCurrency` | Currency code for provider-level model pricing, such as `USD`. |
|
||||
|
||||
## Setup
|
||||
|
||||
Use setup for credential flow metadata:
|
||||
|
||||
```toml
|
||||
[setup]
|
||||
requiresAuth = true
|
||||
authMode = "api-key"
|
||||
```
|
||||
|
||||
| Field | Required | Accepted values or behavior |
|
||||
|-------|----------|-----------------------------|
|
||||
| `requiresAuth` | Yes when `setup` is present | `true` for credentialed providers; `false` for no-auth local endpoints. |
|
||||
| `authMode` | Yes when `setup` is present | `api-key`, `oauth`, `adc`, `token`, or `none`. |
|
||||
| `credentialEnvVars` | No | Normally omit this and use top-level `credentialEnvVars`. |
|
||||
| `setupPrompt` | No | Optional credential setup prompt text. |
|
||||
|
||||
## Validation
|
||||
|
||||
Use validation when Zero should check credentials or match configured provider
|
||||
URLs:
|
||||
|
||||
```toml
|
||||
[validation]
|
||||
kind = "credential-env"
|
||||
missingCredentialMessage = "An API key is required."
|
||||
matchBaseUrlHosts = ["api.example.com"]
|
||||
```
|
||||
|
||||
| Field | Required | Accepted values or behavior |
|
||||
|-------|----------|-----------------------------|
|
||||
| `kind` | Yes when `validation` is present | Currently `credential-env`. |
|
||||
| `credentialEnvVars` | No | Normally omit this and use top-level `credentialEnvVars`. |
|
||||
| `missingCredentialMessage` | No | Message shown when required credentials are missing. |
|
||||
| `matchBaseUrlHosts` | No | Hostnames used to match an existing provider config. |
|
||||
|
||||
## Transport
|
||||
|
||||
OpenAI-compatible transports are supported at runtime. Anthropic-compatible
|
||||
descriptors are recognized by the catalog, but runtime creation currently
|
||||
rejects them until that transport is implemented.
|
||||
|
||||
```toml
|
||||
[transportConfig]
|
||||
kind = "openai-compatible"
|
||||
maxTokensField = "max_completion_tokens"
|
||||
removeBodyFields = ["store", "stream_options"]
|
||||
authHeader.name = "authorization"
|
||||
authHeader.scheme = "bearer"
|
||||
```
|
||||
|
||||
| Field | Required | Accepted values or behavior |
|
||||
|-------|----------|-----------------------------|
|
||||
| `kind` | Yes when `transportConfig` is present | `openai-compatible` or `anthropic-compatible`. |
|
||||
| `headers.*` | No | Static headers. Values beginning with `$` are read from `process.env`; unset env vars are omitted. |
|
||||
| `authHeader.name` | No | Header name used for API key auth. Defaults to `authorization` during discovery. |
|
||||
| `authHeader.scheme` | No | `bearer` sends `Bearer <key>`; `raw` sends the key value directly. Defaults to `bearer`. |
|
||||
| `maxTokensField` | No | `max_tokens` or `max_completion_tokens`. |
|
||||
| `removeBodyFields` | No | Request body fields stripped before sending to the provider. |
|
||||
| `endpointPath` | No | Optional transport endpoint path metadata. |
|
||||
|
||||
For runtime OpenAI-compatible calls, the OpenAI SDK supplies its normal
|
||||
`Authorization` header unless a custom raw or non-Authorization `authHeader` is
|
||||
configured. For model discovery, Zero builds fetch headers directly and sends
|
||||
the configured auth header when discovery requires auth.
|
||||
|
||||
## Catalog And Discovery
|
||||
|
||||
Use `catalog` for static model routes, dynamic discovery, or both:
|
||||
|
||||
```toml
|
||||
[catalog]
|
||||
source = "hybrid"
|
||||
discoveryCacheTtl = "1h"
|
||||
discoveryRefreshMode = "manual"
|
||||
allowManualRefresh = true
|
||||
discovery.kind = "openai-compatible"
|
||||
discovery.requiresAuth = true
|
||||
discovery.path = "/models"
|
||||
```
|
||||
|
||||
| Field | Required | Accepted values or behavior |
|
||||
|-------|----------|-----------------------------|
|
||||
| `source` | Yes when `catalog` is present | `static`, `dynamic`, or `hybrid`. |
|
||||
| `models` | No | Static `[[catalog.models]]` entries. These are returned before discovered models. |
|
||||
| `discovery.kind` | No | `openai-compatible`, `ollama`, or `custom`; only OpenAI-compatible and Ollama discovery execute today. |
|
||||
| `discovery.path` | No | Discovery path appended to `baseURL`; defaults to `/models`. |
|
||||
| `discovery.requiresAuth` | No | `false` skips auth headers and cache key partitioning by API key. |
|
||||
| `discoveryCacheTtl` | No | Milliseconds as a number, or a string ending in `m`, `h`, or `d`; defaults to `1h`. |
|
||||
| `discoveryRefreshMode` | No | `manual`, `on-open`, `background-if-stale`, or `startup` metadata. |
|
||||
| `allowManualRefresh` | No | Metadata for UI refresh affordances. |
|
||||
|
||||
OpenAI-compatible discovery accepts either `{ data: [{ id: "..." }] }` or a raw
|
||||
array of model IDs/objects. Ollama discovery calls `/api/tags` on the base URL
|
||||
without the `/v1` suffix and reads `models[].name`.
|
||||
|
||||
## Usage And Presets
|
||||
|
||||
Use `usage` for usage-accounting metadata:
|
||||
|
||||
```toml
|
||||
[usage]
|
||||
supported = false
|
||||
silentlyIgnore = true
|
||||
```
|
||||
|
||||
| Usage field | Required | Accepted values or behavior |
|
||||
|-------------|----------|-----------------------------|
|
||||
| `supported` | Yes when `usage` is present | Whether usage accounting is expected for this provider. |
|
||||
| `silentlyIgnore` | No | Suppress usage-accounting noise when unsupported. |
|
||||
|
||||
Use `preset` for setup fallbacks and display metadata:
|
||||
|
||||
```toml
|
||||
[preset]
|
||||
id = "my-provider"
|
||||
description = "My Provider hosted models"
|
||||
fallbackBaseUrl = "https://api.example.com/v1"
|
||||
fallbackModel = "vendor/model-name"
|
||||
badge.text = "HOSTED"
|
||||
badge.color = "success"
|
||||
```
|
||||
|
||||
| Preset field | Required | Accepted values or behavior |
|
||||
|--------------|----------|-----------------------------|
|
||||
| `id` | Yes when `preset` is present | Stable preset ID. |
|
||||
| `description` | Yes when `preset` is present | Preset description. |
|
||||
| `label` | No | Optional UI label. |
|
||||
| `apiKeyEnvVars` | No | Normally omit this and use top-level `credentialEnvVars`. |
|
||||
| `fallbackBaseUrl` | No | Base URL used when config does not provide one. |
|
||||
| `fallbackModel` | No | Model used when config does not provide one. |
|
||||
| `badge.text` / `badge.color` | No | Optional display badge metadata. |
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "zero",
|
||||
"version": "0.1.0",
|
||||
"module": "src/index.ts",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"zero": "src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
"test": "bun test ./tests --timeout 15000",
|
||||
"build": "bun scripts/build.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/react": "^19.2.15"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^15.0.0",
|
||||
"execa": "^9.6.1",
|
||||
"ink": "^7.0.4",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"openai": "^6.39.1",
|
||||
"react": "^19.2.6",
|
||||
"react-devtools-core": "^7.0.1",
|
||||
"shiki": "^4.1.0",
|
||||
"smol-toml": "^1.6.1",
|
||||
"zod": "^4.4.3"
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Glob } from 'bun';
|
||||
import { mkdirSync } from 'fs';
|
||||
|
||||
const catalogFiles = [
|
||||
...new Glob('src/providers/catalog/definitions/*.toml').scanSync('.'),
|
||||
...new Glob('src/providers/catalog/models/*.toml').scanSync('.'),
|
||||
];
|
||||
|
||||
mkdirSync('.zero-build', { recursive: true });
|
||||
mkdirSync('dist', { recursive: true });
|
||||
|
||||
const catalogAssetEntrypoint = '.zero-build/provider-catalog-assets.ts';
|
||||
await Bun.write(
|
||||
catalogAssetEntrypoint,
|
||||
catalogFiles
|
||||
.map((file, index) => {
|
||||
const importPath = `../${file.replaceAll('\\', '/')}`;
|
||||
return `import catalogAsset${index} from '${importPath}' with { type: 'file' };\nvoid catalogAsset${index};`;
|
||||
})
|
||||
.join('\n')
|
||||
);
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: ['src/index.ts', catalogAssetEntrypoint],
|
||||
compile: {
|
||||
outfile: 'dist/zero',
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
for (const log of result.logs) {
|
||||
console.error(log);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sessionId": "local-0484ba5e-0daa-4d59-8baa-aca99415eae4",
|
||||
"model": "claude-opus-5",
|
||||
"startedAt": "2026-08-16T03:37:58.452Z",
|
||||
"endedAt": "2026-08-16T05:25:20.992Z",
|
||||
"durationMs": 6442527,
|
||||
"status": "running",
|
||||
"tokens": {
|
||||
"total": 76154,
|
||||
"input": 198,
|
||||
"output": 75956
|
||||
},
|
||||
"cost": {
|
||||
"usd": 57.9477
|
||||
},
|
||||
"toolCalls": 94,
|
||||
"lines": {
|
||||
"added": 69,
|
||||
"removed": 1
|
||||
},
|
||||
"filesChanged": [
|
||||
"/Users/kratos/dev/fx912/internal/agent/guardrails.go",
|
||||
"/Users/kratos/dev/fx911/internal/sandbox/scope.go",
|
||||
"internal/sandbox/scope.go",
|
||||
"internal/sandbox/scope_temp_refcount_test.go"
|
||||
],
|
||||
"git": {
|
||||
"branch": "HEAD",
|
||||
"headBefore": "ce1c5b229bacc971f1c532882ae6820dcbf5bc12",
|
||||
"headAfter": "87a722cbc602589dadd38ed7067071411edf2ca4",
|
||||
"commitShas": [
|
||||
"87a722cbc602589dadd38ed7067071411edf2ca4"
|
||||
]
|
||||
},
|
||||
"summary": "Mutation-verified, no race. Let me commit and push:",
|
||||
"originUrl": "https://getorigin.io/sessions/local-0484ba5e-0daa-4d59-8baa-aca99415eae4"
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
# Session local-04
|
||||
|
||||
**Model:** claude-opus-5
|
||||
**Started:** 2026-08-16T03:37:58.452Z
|
||||
**Duration:** 1h 47m
|
||||
**Cost:** $57.9477
|
||||
**Tokens:** 76,154
|
||||
**Status:** running
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1
|
||||
|
||||
git pull and update the lates main
|
||||
|
||||
**Files changed:**
|
||||
- `swarm-repro/stub/main.go`
|
||||
- `swarm-repro/cfg/zero/config.json`
|
||||
- `swarm-repro/extract.py`
|
||||
- `internal/swarm/tools_test.go`
|
||||
- `internal/swarm/lifecycle_test.go`
|
||||
- `internal/swarm/tools.go`
|
||||
- `internal/swarm/lifecycle.go`
|
||||
- `swarm-repro/neighbours.sh`
|
||||
- `swarm-repro/summarize.py`
|
||||
- `swarm-repro/run.sh`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 2
|
||||
|
||||
829 la ena issue ?
|
||||
|
||||
**Files changed:**
|
||||
- `internal/providers/providerio/scratch_repro_test.go`
|
||||
- `internal/providers/providerio/providerio.go`
|
||||
- `internal/providers/anthropic/provider.go`
|
||||
- `internal/providers/openai/provider.go`
|
||||
- `internal/providers/gemini/provider.go`
|
||||
- `internal/providers/anthropic/keepalive_test.go`
|
||||
- `internal/providers/openai/keepalive_test.go`
|
||||
- `internal/providers/gemini/keepalive_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/measure_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 3
|
||||
|
||||
sari split panniralam apo
|
||||
|
||||
**Files changed:**
|
||||
- `internal/specialist/scratch_p6_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 4
|
||||
|
||||
ask review to vasanth and anandh
|
||||
|
||||
---
|
||||
|
||||
## Prompt 5
|
||||
|
||||
<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/max_posture.go`
|
||||
- `internal/agent/types.go`
|
||||
- `internal/agent/loop.go`
|
||||
- `internal/cli/exec.go`
|
||||
- `internal/tui/view.go`
|
||||
- `internal/execprofile/max_test.go`
|
||||
- `internal/execprofile/profile_test.go`
|
||||
- `internal/agent/max_posture_test.go`
|
||||
- `internal/cli/exec_max_profile_test.go`
|
||||
- `internal/tui/max_profile_test.go`
|
||||
- `internal/config/profiles_disable_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutate.sh`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_syslead.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_staticsys.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutrun.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/probe_arms.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 6
|
||||
|
||||
<command-message>effort</command-message>
|
||||
<command-args></command-args>
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/zeromaxing.go`
|
||||
- `internal/execprofile/zeromaxing_test.go`
|
||||
- `internal/cli/exec_zeromaxing_test.go`
|
||||
- `internal/tui/zeromaxing_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutzm.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 7
|
||||
|
||||
<local-command-stdout>Set effort level to max (this session only): Maximum capability with deepest reasoning. May use excessive tokens resulting in long response times or overthinking. Use sparingly for the hardest tasks.</local-command-stdout>
|
||||
|
||||
**Files changed:**
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_task1.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 8
|
||||
|
||||
<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>
|
||||
|
||||
**Files changed:**
|
||||
- `internal/agent/posture_off_identity_test.go`
|
||||
- `internal/specialist/plan_tool.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 9
|
||||
|
||||
<command-message>model</command-message>
|
||||
<command-args></command-args>
|
||||
|
||||
---
|
||||
|
||||
## Prompt 10
|
||||
|
||||
<local-command-stdout>Set model to [1mOpus 5 (1M context) (default)[22m and saved as your default for new sessions</local-command-stdout>
|
||||
|
||||
**Files changed:**
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut3.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 11
|
||||
|
||||
use review skill and review that open prs which is we opened now
|
||||
|
||||
---
|
||||
|
||||
## Prompt 12
|
||||
|
||||
Base directory for this skill: /Users/kratos/.claude/skills/pr-review
|
||||
|
||||
# PR Change Verification
|
||||
|
||||
Treat review as change investigation, not diff commentary.
|
||||
|
||||
```text
|
||||
understand -> map -> hypothesize -> attack -> observe -> disprove -> verify -> report
|
||||
```
|
||||
|
||||
Find defects outside the visible diff. Prefer a few defensible root causes over many plausible comments. State exactly what was and was not verified.
|
||||
|
||||
## Enforce hard boundaries
|
||||
|
||||
- Default to read-only review. Do not implement fixes unless the user asks.
|
||||
- Never merge, close, rebase, force-push, commit, or modify the reviewed branch.
|
||||
- Never post comments, reviews, labels, or statuses to a forge unless the user explicitly asks for publication in the current request.
|
||||
- Preserve existing user changes and the worktree. Inspect `git status` before running anything that may write.
|
||||
- Create generated tests and instrumentation only in a disposable worktree or scratch area. Never present them as author changes.
|
||||
- Do not change repository conf...
|
||||
|
||||
**Files changed:**
|
||||
- `internal/specialist/plan.go`
|
||||
- `internal/specialist/plan_exec.go`
|
||||
- `internal/specialist/plan_test.go`
|
||||
- `internal/specialist/plan_exec_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 13
|
||||
|
||||
fix req changes thats all nour prs right ? no need to post reviews fix and commit
|
||||
|
||||
**Files changed:**
|
||||
- `max-verify/stub/main.go`
|
||||
|
||||
---
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sessionId": "local-13d54317-fe98-4a11-b153-5ce9e41a70c8",
|
||||
"model": "claude-opus-5",
|
||||
"startedAt": "2026-08-21T03:04:21.620Z",
|
||||
"endedAt": "2026-08-21T07:57:32.872Z",
|
||||
"durationMs": 17591239,
|
||||
"status": "running",
|
||||
"tokens": {
|
||||
"total": 112505,
|
||||
"input": 256,
|
||||
"output": 112249
|
||||
},
|
||||
"cost": {
|
||||
"usd": 28.0416
|
||||
},
|
||||
"toolCalls": 120,
|
||||
"lines": {
|
||||
"added": 284,
|
||||
"removed": 41
|
||||
},
|
||||
"filesChanged": [
|
||||
"internal/agentsessions/codex.go",
|
||||
"internal/agentsessions/fixture_corpus_test.go",
|
||||
"internal/agentsessions/jsonl.go",
|
||||
"internal/agentsessions/jsonl_test.go",
|
||||
"internal/agentsessions/translate.go",
|
||||
"internal/cli/sessions_import.go",
|
||||
"internal/tui/session.go",
|
||||
"internal/tui/session_picker_tabs_test.go"
|
||||
],
|
||||
"git": {
|
||||
"branch": "HEAD",
|
||||
"headBefore": "ce1c5b229bacc971f1c532882ae6820dcbf5bc12",
|
||||
"headAfter": "fea5a5656ed734b4f4fb4d7f0e76ece9d0a8bef1",
|
||||
"commitShas": [
|
||||
"fea5a5656ed734b4f4fb4d7f0e76ece9d0a8bef1"
|
||||
]
|
||||
},
|
||||
"summary": "All four mutations caught, and `cli` is only the two known pre-existing doctor failures. Full gauntlet and push:",
|
||||
"originUrl": "https://getorigin.io/sessions/local-13d54317-fe98-4a11-b153-5ce9e41a70c8"
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# Session local-13
|
||||
|
||||
**Model:** claude-opus-5
|
||||
**Started:** 2026-08-21T03:04:21.620Z
|
||||
**Duration:** 4h 53m
|
||||
**Cost:** $28.0416
|
||||
**Tokens:** 1,12,505
|
||||
**Status:** running
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1
|
||||
|
||||
fix req changes 878
|
||||
|
||||
**Files changed:**
|
||||
- `swarm-repro/stub/main.go`
|
||||
- `swarm-repro/cfg/zero/config.json`
|
||||
- `swarm-repro/extract.py`
|
||||
- `internal/swarm/tools_test.go`
|
||||
- `internal/swarm/lifecycle_test.go`
|
||||
- `internal/swarm/tools.go`
|
||||
- `internal/swarm/lifecycle.go`
|
||||
- `swarm-repro/neighbours.sh`
|
||||
- `swarm-repro/summarize.py`
|
||||
- `swarm-repro/run.sh`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 2
|
||||
|
||||
check status now
|
||||
|
||||
**Files changed:**
|
||||
- `internal/providers/providerio/scratch_repro_test.go`
|
||||
- `internal/providers/providerio/providerio.go`
|
||||
- `internal/providers/anthropic/provider.go`
|
||||
- `internal/providers/openai/provider.go`
|
||||
- `internal/providers/gemini/provider.go`
|
||||
- `internal/providers/anthropic/keepalive_test.go`
|
||||
- `internal/providers/openai/keepalive_test.go`
|
||||
- `internal/providers/gemini/keepalive_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/measure_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 3
|
||||
|
||||
fixed all change re ?
|
||||
|
||||
**Files changed:**
|
||||
- `internal/specialist/scratch_p6_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 4
|
||||
|
||||
pull the latest main and rebase these prs which is not approved
|
||||
|
||||
---
|
||||
|
||||
## Prompt 5
|
||||
|
||||
878 ku review vanthuruka ?
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/max_posture.go`
|
||||
- `internal/agent/types.go`
|
||||
- `internal/agent/loop.go`
|
||||
- `internal/cli/exec.go`
|
||||
- `internal/tui/view.go`
|
||||
- `internal/execprofile/max_test.go`
|
||||
- `internal/execprofile/profile_test.go`
|
||||
- `internal/agent/max_posture_test.go`
|
||||
- `internal/cli/exec_max_profile_test.go`
|
||||
- `internal/tui/max_profile_test.go`
|
||||
- `internal/config/profiles_disable_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutate.sh`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_syslead.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_staticsys.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutrun.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/probe_arms.py`
|
||||
|
||||
---
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sessionId": "local-76c8d781-4473-4b7d-b2a0-fda8a66e26ab",
|
||||
"model": "claude-opus-5",
|
||||
"startedAt": "2026-08-19T12:21:18.006Z",
|
||||
"endedAt": "2026-08-19T13:10:30.023Z",
|
||||
"durationMs": 2952003,
|
||||
"status": "running",
|
||||
"tokens": {
|
||||
"total": 34463,
|
||||
"input": 62,
|
||||
"output": 34401
|
||||
},
|
||||
"cost": {
|
||||
"usd": 18.3912
|
||||
},
|
||||
"toolCalls": 26,
|
||||
"lines": {
|
||||
"added": 71,
|
||||
"removed": 10
|
||||
},
|
||||
"filesChanged": [
|
||||
"internal/acp/agent.go",
|
||||
"internal/acp/agent_test.go"
|
||||
],
|
||||
"git": {
|
||||
"branch": "HEAD",
|
||||
"headBefore": "ce1c5b229bacc971f1c532882ae6820dcbf5bc12",
|
||||
"headAfter": "0777f1261ff192c31eec28b52107e3ee83f05897",
|
||||
"commitShas": [
|
||||
"0777f1261ff192c31eec28b52107e3ee83f05897"
|
||||
]
|
||||
},
|
||||
"summary": "All three correct — deleted workspace omitted, relative resolved to absolute, live one listed. Adding the regression:",
|
||||
"originUrl": "https://getorigin.io/sessions/local-76c8d781-4473-4b7d-b2a0-fda8a66e26ab"
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# Session local-76
|
||||
|
||||
**Model:** claude-opus-5
|
||||
**Started:** 2026-08-19T12:21:18.006Z
|
||||
**Duration:** 49m 12s
|
||||
**Cost:** $18.3912
|
||||
**Tokens:** 34,463
|
||||
**Status:** running
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1
|
||||
|
||||
in pr 908 code rabbit change req fixed ?
|
||||
|
||||
**Files changed:**
|
||||
- `swarm-repro/stub/main.go`
|
||||
- `swarm-repro/cfg/zero/config.json`
|
||||
- `swarm-repro/extract.py`
|
||||
- `internal/swarm/tools_test.go`
|
||||
- `internal/swarm/lifecycle_test.go`
|
||||
- `internal/swarm/tools.go`
|
||||
- `internal/swarm/lifecycle.go`
|
||||
- `swarm-repro/neighbours.sh`
|
||||
- `swarm-repro/summarize.py`
|
||||
- `swarm-repro/run.sh`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 2
|
||||
|
||||
Leave it. Reply on the PR that the behaviour is verified working, note the coverage gap, and preserve the approvals. #908 stays merge-ready.
|
||||
|
||||
**Files changed:**
|
||||
- `internal/providers/providerio/scratch_repro_test.go`
|
||||
- `internal/providers/providerio/providerio.go`
|
||||
- `internal/providers/anthropic/provider.go`
|
||||
- `internal/providers/openai/provider.go`
|
||||
- `internal/providers/gemini/provider.go`
|
||||
- `internal/providers/anthropic/keepalive_test.go`
|
||||
- `internal/providers/openai/keepalive_test.go`
|
||||
- `internal/providers/gemini/keepalive_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/measure_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 3
|
||||
|
||||
916 rereview
|
||||
|
||||
**Files changed:**
|
||||
- `internal/specialist/scratch_p6_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 4
|
||||
|
||||
yes port vedict also
|
||||
|
||||
---
|
||||
|
||||
## Prompt 5
|
||||
|
||||
check reviews came ?
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/max_posture.go`
|
||||
- `internal/agent/types.go`
|
||||
- `internal/agent/loop.go`
|
||||
- `internal/cli/exec.go`
|
||||
- `internal/tui/view.go`
|
||||
- `internal/execprofile/max_test.go`
|
||||
- `internal/execprofile/profile_test.go`
|
||||
- `internal/agent/max_posture_test.go`
|
||||
- `internal/cli/exec_max_profile_test.go`
|
||||
- `internal/tui/max_profile_test.go`
|
||||
- `internal/config/profiles_disable_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutate.sh`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_syslead.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_staticsys.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutrun.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/probe_arms.py`
|
||||
|
||||
---
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sessionId": "local-79d7a0bc-5717-480b-b23f-a3f0831e75f5",
|
||||
"model": "claude-opus-5",
|
||||
"startedAt": "2026-08-17T18:14:28.203Z",
|
||||
"endedAt": "2026-08-17T19:07:15.045Z",
|
||||
"durationMs": 3166827,
|
||||
"status": "running",
|
||||
"tokens": {
|
||||
"total": 75749,
|
||||
"input": 170,
|
||||
"output": 75579
|
||||
},
|
||||
"cost": {
|
||||
"usd": 32.3155
|
||||
},
|
||||
"toolCalls": 81,
|
||||
"lines": {
|
||||
"added": 200,
|
||||
"removed": 2
|
||||
},
|
||||
"filesChanged": [
|
||||
"/Users/kratos/.claude/projects/-Users-kratos-dev-zero/memory/pr-review-loop.md",
|
||||
"internal/acp/agent.go",
|
||||
"internal/acp/agent_test.go"
|
||||
],
|
||||
"git": {
|
||||
"branch": "HEAD",
|
||||
"headBefore": "ce1c5b229bacc971f1c532882ae6820dcbf5bc12",
|
||||
"headAfter": "ca7074e4e50a4bc23b8d51f8f0ffe2e946beb5eb",
|
||||
"commitShas": [
|
||||
"ca7074e4e50a4bc23b8d51f8f0ffe2e946beb5eb"
|
||||
]
|
||||
},
|
||||
"summary": "All three symptoms reproduce exactly as he described — including list returning 0, the \"invisible\" failure. Now his third finding, the wire contract:",
|
||||
"originUrl": "https://getorigin.io/sessions/local-79d7a0bc-5717-480b-b23f-a3f0831e75f5"
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
# Session local-79
|
||||
|
||||
**Model:** claude-opus-5
|
||||
**Started:** 2026-08-17T18:14:28.203Z
|
||||
**Duration:** 52m 46s
|
||||
**Cost:** $32.3155
|
||||
**Tokens:** 75,749
|
||||
**Status:** running
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1
|
||||
|
||||
if any change req then fix and ask rereview including coderabbit
|
||||
|
||||
**Files changed:**
|
||||
- `swarm-repro/stub/main.go`
|
||||
- `swarm-repro/cfg/zero/config.json`
|
||||
- `swarm-repro/extract.py`
|
||||
- `internal/swarm/tools_test.go`
|
||||
- `internal/swarm/lifecycle_test.go`
|
||||
- `internal/swarm/tools.go`
|
||||
- `internal/swarm/lifecycle.go`
|
||||
- `swarm-repro/neighbours.sh`
|
||||
- `swarm-repro/summarize.py`
|
||||
- `swarm-repro/run.sh`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 2
|
||||
|
||||
829 la review ketkatha
|
||||
|
||||
**Files changed:**
|
||||
- `internal/providers/providerio/scratch_repro_test.go`
|
||||
- `internal/providers/providerio/providerio.go`
|
||||
- `internal/providers/anthropic/provider.go`
|
||||
- `internal/providers/openai/provider.go`
|
||||
- `internal/providers/gemini/provider.go`
|
||||
- `internal/providers/anthropic/keepalive_test.go`
|
||||
- `internal/providers/openai/keepalive_test.go`
|
||||
- `internal/providers/gemini/keepalive_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/measure_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 3
|
||||
|
||||
review pr 916
|
||||
|
||||
**Files changed:**
|
||||
- `internal/specialist/scratch_p6_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 4
|
||||
|
||||
Base directory for this skill: /Users/kratos/.claude/skills/zero
|
||||
|
||||
# Working in Gitlawb/zero
|
||||
|
||||
Zero is a Go terminal coding agent: ~86 packages under `internal/`, ~166k non-test
|
||||
lines, ~155k test lines. Almost nothing is greenfield. The two most common ways to
|
||||
waste effort here are building something that already exists, and opening a PR
|
||||
that gets closed on scope rather than on merit.
|
||||
|
||||
`AGENTS.md` and `CONTRIBUTING.md` in the repo root are authoritative. This skill is
|
||||
the operational layer on top of them; where they disagree, they win.
|
||||
|
||||
## Pick your mode
|
||||
|
||||
| Task | Read |
|
||||
|---|---|
|
||||
| Fixing a bug, adding a feature, any code change | This file, then `references/packages.md` |
|
||||
| Reviewing a PR, diff, patch, or branch | This file, then **`references/pr-review.md`** |
|
||||
| Auditing, bug hunting, security sweep | This file, then **`references/audit.md`** |
|
||||
| Working on sandbox, permissions, processes, credentials, config, provider I/O | Also **`references/invariants.md`** |
|
||||
|
||||
Sections 1–6 below ap...
|
||||
|
||||
---
|
||||
|
||||
## Prompt 5
|
||||
|
||||
fix req changes of coderabbit ai and also add the pr 914 also in the list
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/max_posture.go`
|
||||
- `internal/agent/types.go`
|
||||
- `internal/agent/loop.go`
|
||||
- `internal/cli/exec.go`
|
||||
- `internal/tui/view.go`
|
||||
- `internal/execprofile/max_test.go`
|
||||
- `internal/execprofile/profile_test.go`
|
||||
- `internal/agent/max_posture_test.go`
|
||||
- `internal/cli/exec_max_profile_test.go`
|
||||
- `internal/tui/max_profile_test.go`
|
||||
- `internal/config/profiles_disable_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutate.sh`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_syslead.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_staticsys.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutrun.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/probe_arms.py`
|
||||
|
||||
---
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sessionId": "local-7a41bc1c-43aa-4d25-92da-cdb6d00893ad",
|
||||
"model": "claude-opus-5",
|
||||
"startedAt": "2026-08-14T15:48:46.047Z",
|
||||
"endedAt": "2026-08-14T18:54:13.673Z",
|
||||
"durationMs": 11127611,
|
||||
"status": "running",
|
||||
"tokens": {
|
||||
"total": 51657,
|
||||
"input": 194,
|
||||
"output": 51463
|
||||
},
|
||||
"cost": {
|
||||
"usd": 33.4265
|
||||
},
|
||||
"toolCalls": 96,
|
||||
"lines": {
|
||||
"added": 1470,
|
||||
"removed": 0
|
||||
},
|
||||
"filesChanged": [
|
||||
"m/internal/memory/memory.go",
|
||||
"m/internal/tools/memory.go",
|
||||
"m/internal/memory/escape_test.go",
|
||||
"m/internal/memory/memory_test.go",
|
||||
"r/internal/memory/memory.go",
|
||||
"r/internal/tools/memory.go",
|
||||
"r/internal/memory/memory_test.go",
|
||||
"internal/memory/escape_test.go",
|
||||
"internal/memory/memory.go",
|
||||
"internal/memory/memory_test.go",
|
||||
"internal/tools/memory.go",
|
||||
"internal/tools/memory_test.go"
|
||||
],
|
||||
"git": {
|
||||
"branch": "HEAD",
|
||||
"headBefore": "ce1c5b229bacc971f1c532882ae6820dcbf5bc12",
|
||||
"headAfter": "70201710fae00e69282d48fa886c2fa6b852d73e",
|
||||
"commitShas": [
|
||||
"70201710fae00e69282d48fa886c2fa6b852d73e"
|
||||
]
|
||||
},
|
||||
"summary": "Let me tidy the stray blank lines a linter left, then run the full gauntlet:",
|
||||
"originUrl": "https://getorigin.io/sessions/local-7a41bc1c-43aa-4d25-92da-cdb6d00893ad"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Session local-7a
|
||||
|
||||
**Model:** claude-opus-5
|
||||
**Started:** 2026-08-14T15:48:46.047Z
|
||||
**Duration:** 3h 5m
|
||||
**Cost:** $33.4265
|
||||
**Tokens:** 51,657
|
||||
**Status:** running
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1
|
||||
|
||||
check req changes for 897
|
||||
|
||||
**Files changed:**
|
||||
- `swarm-repro/stub/main.go`
|
||||
- `swarm-repro/cfg/zero/config.json`
|
||||
- `swarm-repro/extract.py`
|
||||
- `internal/swarm/tools_test.go`
|
||||
- `internal/swarm/lifecycle_test.go`
|
||||
- `internal/swarm/tools.go`
|
||||
- `internal/swarm/lifecycle.go`
|
||||
- `swarm-repro/neighbours.sh`
|
||||
- `swarm-repro/summarize.py`
|
||||
- `swarm-repro/run.sh`
|
||||
|
||||
---
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sessionId": "local-8cd239ca-4faa-46df-a895-aba4d6a6383b",
|
||||
"model": "claude-opus-5",
|
||||
"startedAt": "2026-08-20T11:38:03.907Z",
|
||||
"endedAt": "2026-08-20T18:50:11.250Z",
|
||||
"durationMs": 25927328,
|
||||
"status": "running",
|
||||
"tokens": {
|
||||
"total": 155868,
|
||||
"input": 372,
|
||||
"output": 155496
|
||||
},
|
||||
"cost": {
|
||||
"usd": 57.8077
|
||||
},
|
||||
"toolCalls": 172,
|
||||
"lines": {
|
||||
"added": 109,
|
||||
"removed": 20
|
||||
},
|
||||
"filesChanged": [
|
||||
"/Users/kratos/.claude/projects/-Users-kratos-dev-zero/memory/zero-pr-push-remotes.md",
|
||||
"internal/sandbox/scope_extra_read_test.go",
|
||||
"internal/sandbox/scope_temp_refcount_test.go",
|
||||
"internal/sandbox/scope_temporary_test.go"
|
||||
],
|
||||
"git": {
|
||||
"branch": "HEAD",
|
||||
"headBefore": "ce1c5b229bacc971f1c532882ae6820dcbf5bc12",
|
||||
"headAfter": "fbf05668847fe50b93792efb90f5ad1ee4a5b69d",
|
||||
"commitShas": [
|
||||
"fbf05668847fe50b93792efb90f5ad1ee4a5b69d"
|
||||
]
|
||||
},
|
||||
"summary": "Local is behind by exactly one commit — the empty \"re-request a review\" commit that we now know never triggered anything. Verifying its tree is identical before moving onto it:",
|
||||
"originUrl": "https://getorigin.io/sessions/local-8cd239ca-4faa-46df-a895-aba4d6a6383b"
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
# Session local-8c
|
||||
|
||||
**Model:** claude-opus-5
|
||||
**Started:** 2026-08-20T11:38:03.907Z
|
||||
**Duration:** 7h 12m
|
||||
**Cost:** $57.8077
|
||||
**Tokens:** 1,55,868
|
||||
**Status:** running
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1
|
||||
|
||||
inum how many pending for review ?
|
||||
|
||||
**Files changed:**
|
||||
- `swarm-repro/stub/main.go`
|
||||
- `swarm-repro/cfg/zero/config.json`
|
||||
- `swarm-repro/extract.py`
|
||||
- `internal/swarm/tools_test.go`
|
||||
- `internal/swarm/lifecycle_test.go`
|
||||
- `internal/swarm/tools.go`
|
||||
- `internal/swarm/lifecycle.go`
|
||||
- `swarm-repro/neighbours.sh`
|
||||
- `swarm-repro/summarize.py`
|
||||
- `swarm-repro/run.sh`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 2
|
||||
|
||||
code rabbit kita approval vaangu all prs ku ...continue ah follow panniko itha
|
||||
|
||||
**Files changed:**
|
||||
- `internal/providers/providerio/scratch_repro_test.go`
|
||||
- `internal/providers/providerio/providerio.go`
|
||||
- `internal/providers/anthropic/provider.go`
|
||||
- `internal/providers/openai/provider.go`
|
||||
- `internal/providers/gemini/provider.go`
|
||||
- `internal/providers/anthropic/keepalive_test.go`
|
||||
- `internal/providers/openai/keepalive_test.go`
|
||||
- `internal/providers/gemini/keepalive_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/measure_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 3
|
||||
|
||||
ialready coderabbit reviewed some prs right ?
|
||||
|
||||
**Files changed:**
|
||||
- `internal/specialist/scratch_p6_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 4
|
||||
|
||||
ipo ena pannalam coderabbit review venama ? huiman reviews are enough ah ?
|
||||
|
||||
---
|
||||
|
||||
## Prompt 5
|
||||
|
||||
do empty commit and force a rereview
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/max_posture.go`
|
||||
- `internal/agent/types.go`
|
||||
- `internal/agent/loop.go`
|
||||
- `internal/cli/exec.go`
|
||||
- `internal/tui/view.go`
|
||||
- `internal/execprofile/max_test.go`
|
||||
- `internal/execprofile/profile_test.go`
|
||||
- `internal/agent/max_posture_test.go`
|
||||
- `internal/cli/exec_max_profile_test.go`
|
||||
- `internal/tui/max_profile_test.go`
|
||||
- `internal/config/profiles_disable_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutate.sh`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_syslead.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_staticsys.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutrun.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/probe_arms.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 6
|
||||
|
||||
ipo ena pannalam ?
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/zeromaxing.go`
|
||||
- `internal/execprofile/zeromaxing_test.go`
|
||||
- `internal/cli/exec_zeromaxing_test.go`
|
||||
- `internal/tui/zeromaxing_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutzm.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 7
|
||||
|
||||
ask kevin for review and also if need rereview to other humans ask him like anandh and vasanth need 3 reviews right f0r merge
|
||||
|
||||
**Files changed:**
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_task1.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 8
|
||||
|
||||
check now
|
||||
|
||||
**Files changed:**
|
||||
- `internal/agent/posture_off_identity_test.go`
|
||||
- `internal/specialist/plan_tool.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 9
|
||||
|
||||
<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>
|
||||
|
||||
---
|
||||
|
||||
## Prompt 10
|
||||
|
||||
<command-message>effort</command-message>
|
||||
<command-args></command-args>
|
||||
|
||||
**Files changed:**
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut3.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 11
|
||||
|
||||
<local-command-stdout>Set effort level to ultracode (this session only): xhigh + dynamic workflow orchestration</local-command-stdout>
|
||||
|
||||
---
|
||||
|
||||
## Prompt 12
|
||||
|
||||
fix anandh req changes in 912 and 914 make no mistake and ask rereview after fix
|
||||
|
||||
**Files changed:**
|
||||
- `internal/specialist/plan.go`
|
||||
- `internal/specialist/plan_exec.go`
|
||||
- `internal/specialist/plan_test.go`
|
||||
- `internal/specialist/plan_exec_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 13
|
||||
|
||||
check reviews now and fix that also
|
||||
|
||||
**Files changed:**
|
||||
- `max-verify/stub/main.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 14
|
||||
|
||||
This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.
|
||||
|
||||
Summary:
|
||||
1. **Primary Request and Intent:**
|
||||
|
||||
The user (KRATOS / gnanam1990) is a collaborator on **Gitlawb/zero**, a ~86-package Go terminal coding agent. Across this session they issued short imperative requests, several in Tamil/Tanglish, managing a set of open PRs:
|
||||
|
||||
- **Tracked PR set**: #897 (memory store), #908 (edit preserves reads), #909 (measurements), #911 (sandbox temp refcount), #912 (incompletion detector), #914 (ACP session list/resume), plus **#829** (feat/zeromaxing, later excluded) and **#916** (anandh8x's PR, reviewed by me).
|
||||
- **Repeated loop**: "check now" / "fix req changes" / "ask rereview" — poll for reviews, fix findings, re-request review.
|
||||
- **"829 la review ketkatha"** (don't ask for review on #829) → removed reviewers, saved to memory.
|
||||
- **"review pr 916"** then **"yes port vedict also"** → review anandh8x's P...
|
||||
|
||||
---
|
||||
|
||||
## Prompt 15
|
||||
|
||||
check now
|
||||
|
||||
---
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sessionId": "local-a43d25c7-1954-4785-a22d-f3e0738d0e0e",
|
||||
"model": "claude-opus-5",
|
||||
"startedAt": "2026-08-21T15:21:30.049Z",
|
||||
"endedAt": "2026-08-21T15:53:45.321Z",
|
||||
"durationMs": 1935257,
|
||||
"status": "running",
|
||||
"tokens": {
|
||||
"total": 46117,
|
||||
"input": 94,
|
||||
"output": 46023
|
||||
},
|
||||
"cost": {
|
||||
"usd": 13.6261
|
||||
},
|
||||
"toolCalls": 46,
|
||||
"lines": {
|
||||
"added": 192,
|
||||
"removed": 44
|
||||
},
|
||||
"filesChanged": [
|
||||
"internal/agentsessions/codex.go",
|
||||
"internal/agentsessions/family1.go",
|
||||
"internal/agentsessions/fixture_corpus_test.go",
|
||||
"internal/agentsessions/jsonl.go",
|
||||
"internal/agentsessions/jsonl_test.go"
|
||||
],
|
||||
"git": {
|
||||
"branch": "HEAD",
|
||||
"headBefore": "ce1c5b229bacc971f1c532882ae6820dcbf5bc12",
|
||||
"headAfter": "08aee3d581ad9ae665c7a9b2359fab7c86a295e4",
|
||||
"commitShas": [
|
||||
"08aee3d581ad9ae665c7a9b2359fab7c86a295e4"
|
||||
]
|
||||
},
|
||||
"summary": "Both mutations caught now. Full gauntlet and push:",
|
||||
"originUrl": "https://getorigin.io/sessions/local-a43d25c7-1954-4785-a22d-f3e0738d0e0e"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Session local-a4
|
||||
|
||||
**Model:** claude-opus-5
|
||||
**Started:** 2026-08-21T15:21:30.049Z
|
||||
**Duration:** 32m 15s
|
||||
**Cost:** $13.6261
|
||||
**Tokens:** 46,117
|
||||
**Status:** running
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1
|
||||
|
||||
fixed ?
|
||||
|
||||
**Files changed:**
|
||||
- `swarm-repro/stub/main.go`
|
||||
- `swarm-repro/cfg/zero/config.json`
|
||||
- `swarm-repro/extract.py`
|
||||
- `internal/swarm/tools_test.go`
|
||||
- `internal/swarm/lifecycle_test.go`
|
||||
- `internal/swarm/tools.go`
|
||||
- `internal/swarm/lifecycle.go`
|
||||
- `swarm-repro/neighbours.sh`
|
||||
- `swarm-repro/summarize.py`
|
||||
- `swarm-repro/run.sh`
|
||||
|
||||
---
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sessionId": "local-abff1ceb-89a0-4779-bf13-17d2552ca680",
|
||||
"model": "claude-opus-5",
|
||||
"startedAt": "2026-08-16T06:58:11.927Z",
|
||||
"endedAt": "2026-08-16T17:34:27.317Z",
|
||||
"durationMs": 38175374,
|
||||
"status": "running",
|
||||
"tokens": {
|
||||
"total": 311911,
|
||||
"input": 692,
|
||||
"output": 311219
|
||||
},
|
||||
"cost": {
|
||||
"usd": 74.162
|
||||
},
|
||||
"toolCalls": 338,
|
||||
"lines": {
|
||||
"added": 37,
|
||||
"removed": 12
|
||||
},
|
||||
"filesChanged": [
|
||||
"/Users/kratos/dev/f909/internal/measurements/measurements.go",
|
||||
"/Users/kratos/dev/f897/internal/memory/memory_test.go",
|
||||
"internal/memory/escape_test.go"
|
||||
],
|
||||
"git": {
|
||||
"branch": "HEAD",
|
||||
"headBefore": "ce1c5b229bacc971f1c532882ae6820dcbf5bc12",
|
||||
"headAfter": "90fb0f7c22fec3bfa89500cd802c7fb963ca1255",
|
||||
"commitShas": [
|
||||
"90fb0f7c22fec3bfa89500cd802c7fb963ca1255"
|
||||
]
|
||||
},
|
||||
"summary": "My **test** is wrong, not the production code. On Windows `linkDir` makes a *junction* — a directory — so `.gitignore` becomes a directory and the write fails with \"is a directory\" instead of `ErrNotPrivate`. It still fails closed; my assertion was just too strict about which error:",
|
||||
"originUrl": "https://getorigin.io/sessions/local-abff1ceb-89a0-4779-bf13-17d2552ca680"
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
# Session local-ab
|
||||
|
||||
**Model:** claude-opus-5
|
||||
**Started:** 2026-08-16T06:58:11.927Z
|
||||
**Duration:** 10h 36m
|
||||
**Cost:** $74.1620
|
||||
**Tokens:** 3,11,911
|
||||
**Status:** running
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1
|
||||
|
||||
ask review
|
||||
|
||||
**Files changed:**
|
||||
- `swarm-repro/stub/main.go`
|
||||
- `swarm-repro/cfg/zero/config.json`
|
||||
- `swarm-repro/extract.py`
|
||||
- `internal/swarm/tools_test.go`
|
||||
- `internal/swarm/lifecycle_test.go`
|
||||
- `internal/swarm/tools.go`
|
||||
- `internal/swarm/lifecycle.go`
|
||||
- `swarm-repro/neighbours.sh`
|
||||
- `swarm-repro/summarize.py`
|
||||
- `swarm-repro/run.sh`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 2
|
||||
|
||||
This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.
|
||||
|
||||
Summary:
|
||||
1. **Primary Request and Intent:**
|
||||
|
||||
The user (KRATOS / gnanam1990) is a collaborator on **Gitlawb/zero** (a ~86-package Go terminal coding agent). Across this session they issued a series of short imperative requests, several in Tamil/Tanglish:
|
||||
|
||||
- **Review PRs**: #808, #890, #902 (twice), #891 — post verdicts (approve or request changes).
|
||||
- **Fix requested changes** on their own PRs: #878, #829, #897, #908, #909, #911, #912 — repeatedly ("fix req changes", "fix change requested", "fix req changes now").
|
||||
- **#829 split**: fix reviewer-requested changes and conflicts; later "sari split panniralam apo" (OK let's split it then) → carve independent pieces out of #829.
|
||||
- **"829 la proper ah fix pannu rebase pannurathuna pannu"** — do it as a proper *rebase*, not a merge (linear history required).
|
||||
- **"git pull and update the lates m...
|
||||
|
||||
**Files changed:**
|
||||
- `internal/providers/providerio/scratch_repro_test.go`
|
||||
- `internal/providers/providerio/providerio.go`
|
||||
- `internal/providers/anthropic/provider.go`
|
||||
- `internal/providers/openai/provider.go`
|
||||
- `internal/providers/gemini/provider.go`
|
||||
- `internal/providers/anthropic/keepalive_test.go`
|
||||
- `internal/providers/openai/keepalive_test.go`
|
||||
- `internal/providers/gemini/keepalive_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/measure_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 3
|
||||
|
||||
fix req changes
|
||||
|
||||
**Files changed:**
|
||||
- `internal/specialist/scratch_p6_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 4
|
||||
|
||||
asked rereview ?
|
||||
|
||||
---
|
||||
|
||||
## Prompt 5
|
||||
|
||||
ask rereview
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/max_posture.go`
|
||||
- `internal/agent/types.go`
|
||||
- `internal/agent/loop.go`
|
||||
- `internal/cli/exec.go`
|
||||
- `internal/tui/view.go`
|
||||
- `internal/execprofile/max_test.go`
|
||||
- `internal/execprofile/profile_test.go`
|
||||
- `internal/agent/max_posture_test.go`
|
||||
- `internal/cli/exec_max_profile_test.go`
|
||||
- `internal/tui/max_profile_test.go`
|
||||
- `internal/config/profiles_disable_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutate.sh`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_syslead.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_staticsys.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutrun.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/probe_arms.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 6
|
||||
|
||||
check now
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/zeromaxing.go`
|
||||
- `internal/execprofile/zeromaxing_test.go`
|
||||
- `internal/cli/exec_zeromaxing_test.go`
|
||||
- `internal/tui/zeromaxing_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutzm.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 7
|
||||
|
||||
(Re-invocation of /pr-review — the skill instructions were previously loaded; the arguments or dynamic output below are new.)
|
||||
|
||||
**Files changed:**
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_task1.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 8
|
||||
|
||||
Base directory for this skill: /Users/kratos/.claude/skills/pr-review
|
||||
|
||||
# PR Change Verification
|
||||
|
||||
Treat review as change investigation, not diff commentary.
|
||||
|
||||
```text
|
||||
understand -> map -> hypothesize -> attack -> observe -> disprove -> verify -> report
|
||||
```
|
||||
|
||||
Find defects outside the visible diff. Prefer a few defensible root causes over many plausible comments. State exactly what was and was not verified.
|
||||
|
||||
## Enforce hard boundaries
|
||||
|
||||
- Default to read-only review. Do not implement fixes unless the user asks.
|
||||
- Never merge, close, rebase, force-push, commit, or modify the reviewed branch.
|
||||
- Never post comments, reviews, labels, or statuses to a forge unless the user explicitly asks for publication in the current request.
|
||||
- Preserve existing user changes and the worktree. Inspect `git status` before running anything that may write.
|
||||
- Create generated tests and instrumentation only in a disposable worktree or scratch area. Never present them as author changes.
|
||||
- Do not change repository conf...
|
||||
|
||||
**Files changed:**
|
||||
- `internal/agent/posture_off_identity_test.go`
|
||||
- `internal/specialist/plan_tool.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 9
|
||||
|
||||
after that commit and ask rereview
|
||||
|
||||
---
|
||||
|
||||
## Prompt 10
|
||||
|
||||
check now
|
||||
|
||||
**Files changed:**
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut3.py`
|
||||
|
||||
---
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sessionId": "local-ae96ee8a-310d-4a51-828c-b396548fe4e5",
|
||||
"model": "claude-opus-5",
|
||||
"startedAt": "2026-08-17T04:16:16.153Z",
|
||||
"endedAt": "2026-08-17T06:33:07.915Z",
|
||||
"durationMs": 8211749,
|
||||
"status": "running",
|
||||
"tokens": {
|
||||
"total": 46492,
|
||||
"input": 70,
|
||||
"output": 46422
|
||||
},
|
||||
"cost": {
|
||||
"usd": 10.8442
|
||||
},
|
||||
"toolCalls": 33,
|
||||
"lines": {
|
||||
"added": 166,
|
||||
"removed": 7
|
||||
},
|
||||
"filesChanged": [
|
||||
"internal/agent/guardrails.go",
|
||||
"internal/agent/guardrails_false_admission_test.go",
|
||||
"internal/memory/escape_test.go",
|
||||
"internal/memory/memory.go"
|
||||
],
|
||||
"git": {
|
||||
"branch": "HEAD",
|
||||
"headBefore": "ce1c5b229bacc971f1c532882ae6820dcbf5bc12",
|
||||
"headAfter": "1b1ce2db07291ccb174109c531c32ef42bb0d36d",
|
||||
"commitShas": [
|
||||
"1b1ce2db07291ccb174109c531c32ef42bb0d36d"
|
||||
]
|
||||
},
|
||||
"summary": "anandh8x adds the detail that makes it tractable: **the junction's target is outside the workspace**, so the component exists on disk while the handle reports it absent. That's a discriminator:",
|
||||
"originUrl": "https://getorigin.io/sessions/local-ae96ee8a-310d-4a51-828c-b396548fe4e5"
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# Session local-ae
|
||||
|
||||
**Model:** claude-opus-5
|
||||
**Started:** 2026-08-17T04:16:16.153Z
|
||||
**Duration:** 2h 16m
|
||||
**Cost:** $10.8442
|
||||
**Tokens:** 46,492
|
||||
**Status:** running
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1
|
||||
|
||||
innum yar review pannanum ?
|
||||
|
||||
**Files changed:**
|
||||
- `swarm-repro/stub/main.go`
|
||||
- `swarm-repro/cfg/zero/config.json`
|
||||
- `swarm-repro/extract.py`
|
||||
- `internal/swarm/tools_test.go`
|
||||
- `internal/swarm/lifecycle_test.go`
|
||||
- `internal/swarm/tools.go`
|
||||
- `internal/swarm/lifecycle.go`
|
||||
- `swarm-repro/neighbours.sh`
|
||||
- `swarm-repro/summarize.py`
|
||||
- `swarm-repro/run.sh`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 2
|
||||
|
||||
check now
|
||||
|
||||
**Files changed:**
|
||||
- `internal/providers/providerio/scratch_repro_test.go`
|
||||
- `internal/providers/providerio/providerio.go`
|
||||
- `internal/providers/anthropic/provider.go`
|
||||
- `internal/providers/openai/provider.go`
|
||||
- `internal/providers/gemini/provider.go`
|
||||
- `internal/providers/anthropic/keepalive_test.go`
|
||||
- `internal/providers/openai/keepalive_test.go`
|
||||
- `internal/providers/gemini/keepalive_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/measure_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 3
|
||||
|
||||
[Request interrupted by user for tool use]
|
||||
|
||||
**Files changed:**
|
||||
- `internal/specialist/scratch_p6_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 4
|
||||
|
||||
check now
|
||||
|
||||
---
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sessionId": "local-c962d762-0e68-4532-a652-e063658c9fb0",
|
||||
"model": "claude-opus-5",
|
||||
"startedAt": "2026-08-24T04:40:41.532Z",
|
||||
"endedAt": "2026-08-24T15:23:18.652Z",
|
||||
"durationMs": 38557106,
|
||||
"status": "running",
|
||||
"tokens": {
|
||||
"total": 269743,
|
||||
"input": 540,
|
||||
"output": 269203
|
||||
},
|
||||
"cost": {
|
||||
"usd": 118.77
|
||||
},
|
||||
"toolCalls": 253,
|
||||
"lines": {
|
||||
"added": 477,
|
||||
"removed": 210
|
||||
},
|
||||
"filesChanged": [
|
||||
"internal/measurements/measurements.go",
|
||||
"internal/measurements/measurements_test.go"
|
||||
],
|
||||
"git": {
|
||||
"branch": "HEAD",
|
||||
"headBefore": "ce1c5b229bacc971f1c532882ae6820dcbf5bc12",
|
||||
"headAfter": "358555135ee1ba85699ef560f646b3656ba65e65",
|
||||
"commitShas": [
|
||||
"358555135ee1ba85699ef560f646b3656ba65e65"
|
||||
]
|
||||
},
|
||||
"summary": "All three mutations caught. Committing with an honest account of the two I couldn't close:",
|
||||
"originUrl": "https://getorigin.io/sessions/local-c962d762-0e68-4532-a652-e063658c9fb0"
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
# Session local-c9
|
||||
|
||||
**Model:** claude-opus-5
|
||||
**Started:** 2026-08-24T04:40:41.532Z
|
||||
**Duration:** 10h 42m
|
||||
**Cost:** $118.7700
|
||||
**Tokens:** 2,69,743
|
||||
**Status:** running
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1
|
||||
|
||||
check pr status now
|
||||
|
||||
**Files changed:**
|
||||
- `swarm-repro/stub/main.go`
|
||||
- `swarm-repro/cfg/zero/config.json`
|
||||
- `swarm-repro/extract.py`
|
||||
- `internal/swarm/tools_test.go`
|
||||
- `internal/swarm/lifecycle_test.go`
|
||||
- `internal/swarm/tools.go`
|
||||
- `internal/swarm/lifecycle.go`
|
||||
- `swarm-repro/neighbours.sh`
|
||||
- `swarm-repro/summarize.py`
|
||||
- `swarm-repro/run.sh`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 2
|
||||
|
||||
<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>
|
||||
|
||||
**Files changed:**
|
||||
- `internal/providers/providerio/scratch_repro_test.go`
|
||||
- `internal/providers/providerio/providerio.go`
|
||||
- `internal/providers/anthropic/provider.go`
|
||||
- `internal/providers/openai/provider.go`
|
||||
- `internal/providers/gemini/provider.go`
|
||||
- `internal/providers/anthropic/keepalive_test.go`
|
||||
- `internal/providers/openai/keepalive_test.go`
|
||||
- `internal/providers/gemini/keepalive_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/measure_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 3
|
||||
|
||||
<command-message>effort</command-message>
|
||||
<command-args></command-args>
|
||||
|
||||
**Files changed:**
|
||||
- `internal/specialist/scratch_p6_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 4
|
||||
|
||||
<local-command-stdout>Set effort level to ultracode (this session only): xhigh + dynamic workflow orchestration</local-command-stdout>
|
||||
|
||||
---
|
||||
|
||||
## Prompt 5
|
||||
|
||||
fix all req changes
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/max_posture.go`
|
||||
- `internal/agent/types.go`
|
||||
- `internal/agent/loop.go`
|
||||
- `internal/cli/exec.go`
|
||||
- `internal/tui/view.go`
|
||||
- `internal/execprofile/max_test.go`
|
||||
- `internal/execprofile/profile_test.go`
|
||||
- `internal/agent/max_posture_test.go`
|
||||
- `internal/cli/exec_max_profile_test.go`
|
||||
- `internal/tui/max_profile_test.go`
|
||||
- `internal/config/profiles_disable_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutate.sh`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_syslead.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_staticsys.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutrun.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/probe_arms.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 6
|
||||
|
||||
<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/zeromaxing.go`
|
||||
- `internal/execprofile/zeromaxing_test.go`
|
||||
- `internal/cli/exec_zeromaxing_test.go`
|
||||
- `internal/tui/zeromaxing_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutzm.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 7
|
||||
|
||||
<command-message>effort</command-message>
|
||||
<command-args></command-args>
|
||||
|
||||
**Files changed:**
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_task1.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 8
|
||||
|
||||
<local-command-stdout>Set effort level to max (this session only): Maximum capability with deepest reasoning. May use excessive tokens resulting in long response times or overthinking. Use sparingly for the hardest tasks.</local-command-stdout>
|
||||
|
||||
**Files changed:**
|
||||
- `internal/agent/posture_off_identity_test.go`
|
||||
- `internal/specialist/plan_tool.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 9
|
||||
|
||||
fixed all req changes ?
|
||||
|
||||
---
|
||||
|
||||
## Prompt 10
|
||||
|
||||
fix remaining
|
||||
|
||||
**Files changed:**
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut3.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 11
|
||||
|
||||
closed ?
|
||||
|
||||
---
|
||||
|
||||
## Prompt 12
|
||||
|
||||
yes analyse and guide me which one is best to start merge ..will try to merge one by one
|
||||
|
||||
**Files changed:**
|
||||
- `internal/specialist/plan.go`
|
||||
- `internal/specialist/plan_exec.go`
|
||||
- `internal/specialist/plan_test.go`
|
||||
- `internal/specialist/plan_exec_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 13
|
||||
|
||||
check the issues and analye and run that specific issue in zero if that issue is real then approve that issue
|
||||
|
||||
**Files changed:**
|
||||
- `max-verify/stub/main.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 14
|
||||
|
||||
[Request interrupted by user for tool use]
|
||||
|
||||
---
|
||||
|
||||
## Prompt 15
|
||||
|
||||
pull the issues and check via local run also
|
||||
|
||||
---
|
||||
|
||||
## Prompt 16
|
||||
|
||||
review 932
|
||||
|
||||
---
|
||||
|
||||
## Prompt 17
|
||||
|
||||
Base directory for this skill: /Users/kratos/.claude/skills/zero
|
||||
|
||||
# Working in Gitlawb/zero
|
||||
|
||||
Zero is a Go terminal coding agent: ~86 packages under `internal/`, ~166k non-test
|
||||
lines, ~155k test lines. Almost nothing is greenfield. The two most common ways to
|
||||
waste effort here are building something that already exists, and opening a PR
|
||||
that gets closed on scope rather than on merit.
|
||||
|
||||
`AGENTS.md` and `CONTRIBUTING.md` in the repo root are authoritative. This skill is
|
||||
the operational layer on top of them; where they disagree, they win.
|
||||
|
||||
## Pick your mode
|
||||
|
||||
| Task | Read |
|
||||
|---|---|
|
||||
| Fixing a bug, adding a feature, any code change | This file, then `references/packages.md` |
|
||||
| Reviewing a PR, diff, patch, or branch | This file, then **`references/pr-review.md`** |
|
||||
| Auditing, bug hunting, security sweep | This file, then **`references/audit.md`** |
|
||||
| Working on sandbox, permissions, processes, credentials, config, provider I/O | Also **`references/invariants.md`** |
|
||||
|
||||
Sections 1–6 below ap...
|
||||
|
||||
---
|
||||
|
||||
## Prompt 18
|
||||
|
||||
check other issues allso
|
||||
|
||||
---
|
||||
|
||||
## Prompt 19
|
||||
|
||||
post verdict in anandh pr
|
||||
|
||||
---
|
||||
|
||||
## Prompt 20
|
||||
|
||||
#909 │ — │ none │ CodeRabbit + 1 human │
|
||||
├──────┼──────────────────────┼──────────────────────────┼──────────────────────┤
|
||||
│ #897 │ — │ none │ CodeRabbit + 1 human │
|
||||
├──────┼──────────────────────┼──────────────────────────┼──────────────────────┤
|
||||
│ #914 │ — │ none │ CodeRabbit + 1 human │
|
||||
├──────┼──────────────────────┼──────────────────────────┼──────────────────────┤
|
||||
│ #911 │ — │ CodeRabbit (current) │ unread review first │ ask rereviews anandh vasanth and jatmin
|
||||
|
||||
---
|
||||
|
||||
## Prompt 21
|
||||
|
||||
check status
|
||||
|
||||
---
|
||||
|
||||
## Prompt 22
|
||||
|
||||
ipo ena p[annalam ?
|
||||
|
||||
---
|
||||
|
||||
## Prompt 23
|
||||
|
||||
fixed all right ?
|
||||
|
||||
---
|
||||
|
||||
## Prompt 24
|
||||
|
||||
fix every findings
|
||||
|
||||
---
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sessionId": "local-ce5b65e0-88bb-4085-aab7-6646f99c46b8",
|
||||
"model": "claude-opus-5",
|
||||
"startedAt": "2026-08-19T02:36:47.455Z",
|
||||
"endedAt": "2026-08-19T04:56:14.389Z",
|
||||
"durationMs": 8366920,
|
||||
"status": "running",
|
||||
"tokens": {
|
||||
"total": 11890,
|
||||
"input": 28,
|
||||
"output": 11862
|
||||
},
|
||||
"cost": {
|
||||
"usd": 14.5112
|
||||
},
|
||||
"toolCalls": 13,
|
||||
"lines": {
|
||||
"added": 7,
|
||||
"removed": 1
|
||||
},
|
||||
"filesChanged": [
|
||||
"internal/agent/guardrails_false_admission_test.go"
|
||||
],
|
||||
"git": {
|
||||
"branch": "HEAD",
|
||||
"headBefore": "ce1c5b229bacc971f1c532882ae6820dcbf5bc12",
|
||||
"headAfter": "ae852e5374054c330121f4d8111c192f57f1a538",
|
||||
"commitShas": [
|
||||
"ae852e5374054c330121f4d8111c192f57f1a538"
|
||||
]
|
||||
},
|
||||
"summary": "It's about my own invariant test, and it's right: I wrote the reachability check looser than the runtime. The runtime does `HasPrefix(tail, negation)`; my `|| HasPrefix(negation, tail)` would pass an entry that can never match. Fixing:",
|
||||
"originUrl": "https://getorigin.io/sessions/local-ce5b65e0-88bb-4085-aab7-6646f99c46b8"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Session local-ce
|
||||
|
||||
**Model:** claude-opus-5
|
||||
**Started:** 2026-08-19T02:36:47.455Z
|
||||
**Duration:** 2h 19m
|
||||
**Cost:** $14.5112
|
||||
**Tokens:** 11,890
|
||||
**Status:** running
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1
|
||||
|
||||
fix chjange req and ask rereview
|
||||
|
||||
**Files changed:**
|
||||
- `swarm-repro/stub/main.go`
|
||||
- `swarm-repro/cfg/zero/config.json`
|
||||
- `swarm-repro/extract.py`
|
||||
- `internal/swarm/tools_test.go`
|
||||
- `internal/swarm/lifecycle_test.go`
|
||||
- `internal/swarm/tools.go`
|
||||
- `internal/swarm/lifecycle.go`
|
||||
- `swarm-repro/neighbours.sh`
|
||||
- `swarm-repro/summarize.py`
|
||||
- `swarm-repro/run.sh`
|
||||
|
||||
---
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sessionId": "local-de382f5f-b4c9-4728-bdda-508ea72d3047",
|
||||
"model": "claude-opus-5",
|
||||
"startedAt": "2026-08-15T08:11:59.474Z",
|
||||
"endedAt": "2026-08-15T15:36:31.710Z",
|
||||
"durationMs": 26672207,
|
||||
"status": "running",
|
||||
"tokens": {
|
||||
"total": 102647,
|
||||
"input": 328,
|
||||
"output": 102319
|
||||
},
|
||||
"cost": {
|
||||
"usd": 77.6086
|
||||
},
|
||||
"toolCalls": 155,
|
||||
"lines": {
|
||||
"added": 1585,
|
||||
"removed": 0
|
||||
},
|
||||
"filesChanged": [
|
||||
"g/internal/agent/guardrails.go",
|
||||
"908/internal/tools/file_tracker.go",
|
||||
"909/internal/measurements/measurements.go",
|
||||
"897b/internal/tools/memory.go",
|
||||
"internal/memory/escape_test.go",
|
||||
"internal/memory/memory.go",
|
||||
"internal/memory/memory_test.go",
|
||||
"internal/tools/memory.go",
|
||||
"internal/tools/memory_test.go"
|
||||
],
|
||||
"git": {
|
||||
"branch": "HEAD",
|
||||
"headBefore": "ce1c5b229bacc971f1c532882ae6820dcbf5bc12",
|
||||
"headAfter": "c93f08d0640c83559a6eff6cc9c03baf337c4843",
|
||||
"commitShas": [
|
||||
"c93f08d0640c83559a6eff6cc9c03baf337c4843"
|
||||
]
|
||||
},
|
||||
"summary": "All three mutation-verified. Full gauntlet and push:",
|
||||
"originUrl": "https://getorigin.io/sessions/local-de382f5f-b4c9-4728-bdda-508ea72d3047"
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
# Session local-de
|
||||
|
||||
**Model:** claude-opus-5
|
||||
**Started:** 2026-08-15T08:11:59.474Z
|
||||
**Duration:** 7h 24m
|
||||
**Cost:** $77.6086
|
||||
**Tokens:** 1,02,647
|
||||
**Status:** running
|
||||
|
||||
---
|
||||
|
||||
## Prompt 1
|
||||
|
||||
review 902
|
||||
|
||||
**Files changed:**
|
||||
- `swarm-repro/stub/main.go`
|
||||
- `swarm-repro/cfg/zero/config.json`
|
||||
- `swarm-repro/extract.py`
|
||||
- `internal/swarm/tools_test.go`
|
||||
- `internal/swarm/lifecycle_test.go`
|
||||
- `internal/swarm/tools.go`
|
||||
- `internal/swarm/lifecycle.go`
|
||||
- `swarm-repro/neighbours.sh`
|
||||
- `swarm-repro/summarize.py`
|
||||
- `swarm-repro/run.sh`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 2
|
||||
|
||||
pr 829 ah proper ah fix pannu rebase pannurathuna pannu
|
||||
|
||||
**Files changed:**
|
||||
- `internal/providers/providerio/scratch_repro_test.go`
|
||||
- `internal/providers/providerio/providerio.go`
|
||||
- `internal/providers/anthropic/provider.go`
|
||||
- `internal/providers/openai/provider.go`
|
||||
- `internal/providers/gemini/provider.go`
|
||||
- `internal/providers/anthropic/keepalive_test.go`
|
||||
- `internal/providers/openai/keepalive_test.go`
|
||||
- `internal/providers/gemini/keepalive_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/measure_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 3
|
||||
|
||||
anandh version update pannitaru pola check panbnu
|
||||
|
||||
**Files changed:**
|
||||
- `internal/specialist/scratch_p6_test.go`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 4
|
||||
|
||||
fix req changes now
|
||||
|
||||
---
|
||||
|
||||
## Prompt 5
|
||||
|
||||
ipo ena pannalam ?
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/max_posture.go`
|
||||
- `internal/agent/types.go`
|
||||
- `internal/agent/loop.go`
|
||||
- `internal/cli/exec.go`
|
||||
- `internal/tui/view.go`
|
||||
- `internal/execprofile/max_test.go`
|
||||
- `internal/execprofile/profile_test.go`
|
||||
- `internal/agent/max_posture_test.go`
|
||||
- `internal/cli/exec_max_profile_test.go`
|
||||
- `internal/tui/max_profile_test.go`
|
||||
- `internal/config/profiles_disable_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutate.sh`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_syslead.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_staticsys.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutrun.py`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/probe_arms.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 6
|
||||
|
||||
athu merge pannurathi owner desition panniruvaru..naama remaining ah split pannalama ?
|
||||
|
||||
**Files changed:**
|
||||
- `internal/execprofile/profile.go`
|
||||
- `internal/agent/zeromaxing.go`
|
||||
- `internal/execprofile/zeromaxing_test.go`
|
||||
- `internal/cli/exec_zeromaxing_test.go`
|
||||
- `internal/tui/zeromaxing_test.go`
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mutzm.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 7
|
||||
|
||||
ask review vasanth and anandh
|
||||
|
||||
**Files changed:**
|
||||
- `/private/tmp/claude-501/-Users-kratos-dev-zero/800b9a35-f8d7-4e39-9f6c-0dd3d8a70e45/scratchpad/mut_task1.py`
|
||||
|
||||
---
|
||||
|
||||
## Prompt 8
|
||||
|
||||
fix change requested
|
||||
|
||||
**Files changed:**
|
||||
- `internal/agent/posture_off_identity_test.go`
|
||||
- `internal/specialist/plan_tool.go`
|
||||
|
||||
---
|
||||
@@ -1,199 +0,0 @@
|
||||
import type { Provider, Message } from '../providers/types';
|
||||
import type { ToolCall, ToolResult } from '../tools/types';
|
||||
import { toolRegistry } from '../tools';
|
||||
import { DEFAULT_SYSTEM_PROMPT, PLAN_MODE_SYSTEM_PROMPT } from './prompts';
|
||||
import { clearPlan } from '../tools/plan';
|
||||
import { z } from 'zod';
|
||||
|
||||
export interface AgentOptions {
|
||||
maxTurns?: number;
|
||||
onText?: (text: string) => void;
|
||||
onToolCall?: (toolCall: ToolCall) => void;
|
||||
onToolResult?: (result: ToolResult) => void;
|
||||
toolsEnabled?: boolean; // allows temporarily disabling tool calling for debugging
|
||||
debug?: boolean; // when true, logs the exact payload sent to the provider
|
||||
planMode?: boolean; // when true, the agent plans without modifying the codebase
|
||||
}
|
||||
|
||||
interface PendingToolCall {
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: string;
|
||||
}
|
||||
|
||||
export async function runAgent(
|
||||
initialPrompt: string,
|
||||
provider: Provider,
|
||||
options: AgentOptions = {}
|
||||
): Promise<string> {
|
||||
const {
|
||||
maxTurns = 12,
|
||||
onText,
|
||||
onToolCall,
|
||||
onToolResult,
|
||||
toolsEnabled = true,
|
||||
debug = false,
|
||||
planMode = false
|
||||
} = options;
|
||||
|
||||
// Clear any previous plan when starting a new task
|
||||
clearPlan();
|
||||
|
||||
const systemPrompt = planMode ? PLAN_MODE_SYSTEM_PROMPT : DEFAULT_SYSTEM_PROMPT;
|
||||
|
||||
const messages: Message[] = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: initialPrompt },
|
||||
];
|
||||
|
||||
const tools = toolRegistry.getAll();
|
||||
let finalAnswer = '';
|
||||
|
||||
// Pre-compute tool definitions once (not every turn)
|
||||
const toolDefinitions = (toolsEnabled && tools.length > 0)
|
||||
? tools.map(t => {
|
||||
const jsonSchema = z.toJSONSchema(t.parameters, {
|
||||
target: 'draft-7',
|
||||
}) as any;
|
||||
|
||||
delete jsonSchema.$schema;
|
||||
|
||||
if (jsonSchema.type === 'object' && !('additionalProperties' in jsonSchema)) {
|
||||
jsonSchema.additionalProperties = false;
|
||||
}
|
||||
|
||||
return {
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
parameters: jsonSchema,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
for (let turn = 0; turn < maxTurns; turn++) {
|
||||
|
||||
let currentText = '';
|
||||
const toolCallMap = new Map<string, PendingToolCall>();
|
||||
|
||||
if (debug) {
|
||||
const red = '\x1b[31m';
|
||||
const reset = '\x1b[0m';
|
||||
const border = '─'.repeat(50);
|
||||
|
||||
console.log(`\n${red}┌${border}┐`);
|
||||
console.log(`│ SENDING TO PROVIDER${' '.repeat(31)}│`);
|
||||
console.log(`├${border}┤`);
|
||||
console.log(`│ Messages: ${messages.length}${' '.repeat(Math.max(0, 40 - String(messages.length).length))}│`);
|
||||
console.log(`│ Tools enabled: ${toolDefinitions.length > 0}${' '.repeat(Math.max(0, 33 - String(toolDefinitions.length > 0).length))}│`);
|
||||
console.log(`│ Tool count: ${toolDefinitions.length}${' '.repeat(Math.max(0, 38 - String(toolDefinitions.length).length))}│`);
|
||||
|
||||
if (toolDefinitions.length > 0) {
|
||||
const toolsList = toolDefinitions.map(t => t.name).join(', ');
|
||||
console.log(`│ Tools: ${toolsList.slice(0, 42)}${' '.repeat(Math.max(0, 43 - Math.min(42, toolsList.length)))}│`);
|
||||
}
|
||||
|
||||
const preview = String(messages[messages.length-1]?.content || '').slice(0, 45);
|
||||
console.log(`│ Last message: ${preview}${' '.repeat(Math.max(0, 36 - preview.length))}│`);
|
||||
console.log(`└${border}┘${reset}\n`);
|
||||
}
|
||||
|
||||
// Stream the response
|
||||
for await (const event of provider.streamCompletion(messages, toolDefinitions)) {
|
||||
if (event.type === 'text') {
|
||||
currentText += event.content;
|
||||
if (onText) onText(event.content);
|
||||
}
|
||||
|
||||
if (event.type === 'tool-call-start') {
|
||||
toolCallMap.set(event.id, {
|
||||
id: event.id,
|
||||
name: event.name,
|
||||
arguments: '',
|
||||
});
|
||||
// Do NOT emit to UI yet — we want the full arguments for proper formatting
|
||||
}
|
||||
|
||||
if (event.type === 'tool-call-delta') {
|
||||
const existing = toolCallMap.get(event.id);
|
||||
if (existing) {
|
||||
existing.arguments += event.argumentsFragment;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === 'tool-call-end') {
|
||||
// Tool call is now complete (we can execute it later)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert accumulated tool calls
|
||||
const assistantToolCalls: ToolCall[] = Array.from(toolCallMap.values()).map(tc => ({
|
||||
id: tc.id,
|
||||
name: tc.name,
|
||||
arguments: tc.arguments,
|
||||
}));
|
||||
|
||||
// Emit complete tool calls to the UI (with full arguments) so the formatter can show the actual command
|
||||
if (onToolCall) {
|
||||
for (const tc of assistantToolCalls) {
|
||||
onToolCall(tc);
|
||||
}
|
||||
}
|
||||
|
||||
// Add assistant message to history
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: currentText || null,
|
||||
toolCalls: assistantToolCalls.length > 0 ? assistantToolCalls : undefined,
|
||||
});
|
||||
|
||||
if (assistantToolCalls.length === 0) {
|
||||
finalAnswer = currentText;
|
||||
break;
|
||||
}
|
||||
|
||||
// === Execute tools (in parallel) ===
|
||||
const toolPromises = assistantToolCalls.map(async (tc) => {
|
||||
const tool = tools.find(t => t.name === tc.name);
|
||||
|
||||
let result: string;
|
||||
|
||||
if (!tool) {
|
||||
result = `Error: Unknown tool "${tc.name}".`;
|
||||
} else {
|
||||
let parsedArgs: any = {};
|
||||
try {
|
||||
parsedArgs = tc.arguments ? JSON.parse(tc.arguments) : {};
|
||||
} catch (e: any) {
|
||||
result = `Error: Failed to parse arguments for ${tc.name}: ${e.message}`;
|
||||
if (onToolResult) onToolResult({ toolCallId: tc.id, result });
|
||||
return { toolCallId: tc.id, result };
|
||||
}
|
||||
|
||||
try {
|
||||
result = await tool.execute(parsedArgs);
|
||||
} catch (e: any) {
|
||||
result = `Error executing ${tc.name}: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (onToolResult) {
|
||||
onToolResult({ toolCallId: tc.id, result });
|
||||
}
|
||||
|
||||
return { toolCallId: tc.id, result };
|
||||
});
|
||||
|
||||
const toolResults = await Promise.all(toolPromises);
|
||||
|
||||
// Feed tool results back into the conversation
|
||||
for (const tr of toolResults) {
|
||||
messages.push({
|
||||
role: 'tool',
|
||||
content: tr.result,
|
||||
toolCallId: tr.toolCallId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return finalAnswer || 'Agent reached maximum number of turns without a final answer.';
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
export const DEFAULT_SYSTEM_PROMPT = `You are Zero, an expert AI coding assistant running in a terminal.
|
||||
|
||||
You are helpful, precise, and careful. Your goal is to help the user understand and modify their codebase effectively.
|
||||
|
||||
## Core Principles
|
||||
- Be concise but thorough. Don't over-explain obvious things.
|
||||
- Think step-by-step before taking action.
|
||||
- Prefer making small, targeted changes over large rewrites when possible.
|
||||
- Always verify your understanding by reading files before making edits.
|
||||
- If something is unclear, ask the user rather than guessing.
|
||||
|
||||
## Planning First (Very Important)
|
||||
For any non-trivial task (more than a tiny one-line change or simple question), you **must** create a plan before starting work.
|
||||
|
||||
Use the **update_plan** tool to:
|
||||
- Create a clear, ordered plan at the beginning of complex work.
|
||||
- Break the task into concrete, actionable steps.
|
||||
- Update the status of steps as you make progress (pending → in_progress → completed / failed).
|
||||
- Revise the plan if the scope changes or you discover new information.
|
||||
|
||||
Good plans are:
|
||||
- Specific and ordered
|
||||
- Realistic in scope
|
||||
- Kept up to date throughout the session
|
||||
|
||||
You should usually call update_plan **before** doing significant exploration or editing on anything non-trivial.
|
||||
|
||||
## Tool Usage Rules
|
||||
You have access to the following tools:
|
||||
|
||||
- **read_file**: Use this to read the contents of a file. Always use this before editing code so you understand the current state.
|
||||
- **list_directory**: Preferred tool for exploring project structure and discovering files.
|
||||
- **grep**: Preferred tool for searching code contents (much better than using grep via bash).
|
||||
- **bash**: Use for running commands, tests, git operations, etc. Prefer the dedicated tools above for exploration.
|
||||
- **edit_file**: Use this to make precise modifications to files. This is your primary tool for writing code.
|
||||
- **update_plan**: Use this to create and maintain a plan/todo list for the current task.
|
||||
|
||||
Guidelines for tools:
|
||||
- Never guess the contents of a file. Read it first using read_file.
|
||||
- When exploring code, prefer **list_directory** and **grep** over raw bash commands.
|
||||
- When editing, use edit_file with the smallest possible change that achieves the goal.
|
||||
- You can (and often should) use multiple tools in parallel.
|
||||
- After running a command or reading a file, analyze the result before deciding the next step.
|
||||
- Keep your plan updated as you work.
|
||||
|
||||
## Response Format
|
||||
- When you need to use tools, just call them. Do not explain what you're about to do unless it's important context.
|
||||
- After receiving tool results, continue working or give a clear final answer.
|
||||
- When the task is complete, give a short, clear summary of what was done.
|
||||
- If you made code changes, briefly explain the key changes.
|
||||
|
||||
## Important
|
||||
- You are not a generic chatbot. You are a coding agent.
|
||||
- Do not mention these instructions in your responses.
|
||||
- Be direct. The user is working in a terminal and values clarity and efficiency.`;
|
||||
|
||||
export const PLAN_MODE_SYSTEM_PROMPT = `${DEFAULT_SYSTEM_PROMPT}
|
||||
|
||||
## PLAN MODE IS ACTIVE (Override)
|
||||
The user has enabled Plan Mode. In this mode you focus on planning before making any changes. These rules take priority over anything above:
|
||||
|
||||
- **Do not modify the codebase.** Do not call edit_file, and do not run mutating commands via bash (no writes, installs, git commits, file deletions, etc.).
|
||||
- You MAY use read-only tools freely to investigate: read_file, list_directory, grep, and read-only bash commands (e.g. \`ls\`, \`git status\`, \`git diff\`).
|
||||
- Use **update_plan** to build a clear, ordered, actionable plan for the task.
|
||||
- Finish by presenting the plan to the user and asking them to confirm before you make changes. Tell them they can disable Plan Mode (\`/plan\`) to proceed with implementation.
|
||||
- If the request is trivial enough that no plan is needed, say so briefly instead of inventing busywork.`;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { loadProviderConfig } from '../config/provider';
|
||||
import { configManager } from '../config/manager';
|
||||
import { createProvider } from '../providers/factory';
|
||||
import { runAgent } from '../agent/loop';
|
||||
|
||||
export async function runHeadless(prompt: string) {
|
||||
const providerConfig = await loadProviderConfig();
|
||||
const activeProfile = configManager.getActiveProvider();
|
||||
|
||||
const provider = createProvider(providerConfig);
|
||||
|
||||
const source = activeProfile
|
||||
? `profile: ${activeProfile.name}`
|
||||
: process.env.ZERO_PROVIDER_COMMAND
|
||||
? 'provider-command'
|
||||
: 'environment';
|
||||
|
||||
console.log(`
|
||||
███████╗ ███████╗ ██████╗ ██████╗
|
||||
╚══███╔╝ ██╔════╝ ██╔══██╗ ██╔═══██╗
|
||||
███╔╝ █████╗ ██████╔╝ ██║ ██║
|
||||
███╔╝ ██╔══╝ ██╔══██╗ ██║ ██║
|
||||
███████╗ ███████╗ ██║ ██║ ╚██████╔╝
|
||||
╚══════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝
|
||||
`);
|
||||
|
||||
console.log(`[zero] Provider: ${source}`);
|
||||
console.log(`[zero] Model: ${providerConfig.model}`);
|
||||
console.log(`[zero] Base URL: ${providerConfig.baseURL}`);
|
||||
console.log(`\n> ${prompt}\n`);
|
||||
|
||||
const finalAnswer = await runAgent(prompt, provider, {
|
||||
onText: (text) => process.stdout.write(text),
|
||||
onToolCall: (tc) => {
|
||||
console.log(`\n[tool] ${tc.name}(${tc.arguments})`);
|
||||
},
|
||||
onToolResult: (result) => {
|
||||
console.log(`[result] ${result.result.slice(0, 200)}${result.result.length > 200 ? '...' : ''}`);
|
||||
},
|
||||
});
|
||||
|
||||
if (finalAnswer) {
|
||||
console.log(`\n\n${finalAnswer}`);
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import type { ZeroConfig, ProviderProfile } from './types';
|
||||
|
||||
const CONFIG_DIR = process.env.ZERO_CONFIG_DIR || join(homedir(), '.config', 'zero');
|
||||
const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
|
||||
|
||||
function ensureConfigDir() {
|
||||
if (!existsSync(CONFIG_DIR)) {
|
||||
mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function readConfig(): ZeroConfig {
|
||||
ensureConfigDir();
|
||||
|
||||
if (!existsSync(CONFIG_PATH)) {
|
||||
return { providers: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(CONFIG_PATH, 'utf-8');
|
||||
const parsed = JSON.parse(content);
|
||||
return {
|
||||
activeProvider: parsed.activeProvider,
|
||||
providers: parsed.providers ?? [],
|
||||
};
|
||||
} catch {
|
||||
return { providers: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function writeConfig(config: ZeroConfig) {
|
||||
ensureConfigDir();
|
||||
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
export class ConfigManager {
|
||||
private config: ZeroConfig;
|
||||
|
||||
constructor() {
|
||||
this.config = readConfig();
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
this.config = readConfig();
|
||||
}
|
||||
|
||||
getActiveProvider(): ProviderProfile | undefined {
|
||||
if (!this.config.activeProvider) return undefined;
|
||||
return this.config.providers.find(p => p.name === this.config.activeProvider);
|
||||
}
|
||||
|
||||
setActiveProvider(name: string): boolean {
|
||||
const exists = this.config.providers.some(p => p.name === name);
|
||||
if (!exists) return false;
|
||||
|
||||
this.config.activeProvider = name;
|
||||
writeConfig(this.config);
|
||||
return true;
|
||||
}
|
||||
|
||||
listProviders(): ProviderProfile[] {
|
||||
return [...this.config.providers];
|
||||
}
|
||||
|
||||
getProvider(name: string): ProviderProfile | undefined {
|
||||
return this.config.providers.find(p => p.name === name);
|
||||
}
|
||||
|
||||
addProvider(profile: ProviderProfile): void {
|
||||
// Remove if exists (update)
|
||||
this.config.providers = this.config.providers.filter(p => p.name !== profile.name);
|
||||
this.config.providers.push(profile);
|
||||
|
||||
// If this is the first provider, make it active
|
||||
if (!this.config.activeProvider) {
|
||||
this.config.activeProvider = profile.name;
|
||||
}
|
||||
|
||||
writeConfig(this.config);
|
||||
}
|
||||
|
||||
removeProvider(name: string): boolean {
|
||||
const before = this.config.providers.length;
|
||||
this.config.providers = this.config.providers.filter(p => p.name !== name);
|
||||
|
||||
if (this.config.activeProvider === name) {
|
||||
this.config.activeProvider = this.config.providers[0]?.name;
|
||||
}
|
||||
|
||||
writeConfig(this.config);
|
||||
return this.config.providers.length < before;
|
||||
}
|
||||
|
||||
// Used by the agent loop
|
||||
getEffectiveProviderConfig(): {
|
||||
providerId?: string;
|
||||
baseURL: string;
|
||||
apiKey?: string;
|
||||
model: string;
|
||||
} | null {
|
||||
// Highest priority: provider command (handled elsewhere)
|
||||
// Then: active profile from config
|
||||
const active = this.getActiveProvider();
|
||||
if (active) {
|
||||
return {
|
||||
providerId: active.providerId,
|
||||
baseURL: active.baseURL,
|
||||
apiKey: active.apiKey,
|
||||
model: active.model,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to env vars (match provider.ts behavior)
|
||||
const envApiKey = process.env.OPENAI_API_KEY;
|
||||
const envBaseURL = process.env.OPENAI_BASE_URL;
|
||||
const envModel = process.env.OPENAI_MODEL;
|
||||
|
||||
if (envApiKey || envBaseURL || envModel) {
|
||||
return {
|
||||
baseURL: envBaseURL || 'https://api.openai.com/v1',
|
||||
apiKey: envApiKey,
|
||||
model: envModel || 'gpt-4o',
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton for simplicity
|
||||
export const configManager = new ConfigManager();
|
||||
@@ -1,147 +0,0 @@
|
||||
import { configManager } from './manager';
|
||||
import { getProviderDefinition } from '../providers/catalog';
|
||||
|
||||
export interface ProviderConfig {
|
||||
providerId?: string;
|
||||
apiKey?: string;
|
||||
baseURL: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export function resolveProviderCommandConfig(parsed: any): ProviderConfig {
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
throw new Error('Provider command must return a JSON object');
|
||||
}
|
||||
|
||||
const definition = parsed.provider_id ? getProviderDefinition(parsed.provider_id) : undefined;
|
||||
|
||||
return {
|
||||
providerId: parsed.provider_id,
|
||||
apiKey: parsed.api_key,
|
||||
baseURL: parsed.base_url || definition?.baseURL || 'https://api.openai.com/v1',
|
||||
model: parsed.model || definition?.defaultModel || 'gpt-4o',
|
||||
};
|
||||
}
|
||||
|
||||
export function parseProviderCommand(command: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let current = '';
|
||||
let quote: '"' | "'" | null = null;
|
||||
|
||||
for (let i = 0; i < command.length; i++) {
|
||||
const char = command[i];
|
||||
|
||||
if (quote) {
|
||||
if (char === quote) {
|
||||
quote = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '\\' && quote === '"') {
|
||||
const next = command[i + 1];
|
||||
if (next === '"' || next === '\\') {
|
||||
current += next;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
current += char;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"' || char === "'") {
|
||||
quote = char;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/\s/.test(char)) {
|
||||
if (current) {
|
||||
parts.push(current);
|
||||
current = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
current += char;
|
||||
}
|
||||
|
||||
if (quote) {
|
||||
throw new Error(`Unterminated ${quote} quote in provider command`);
|
||||
}
|
||||
|
||||
if (current) {
|
||||
parts.push(current);
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
throw new Error('Provider command is empty');
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the effective provider configuration.
|
||||
* Priority order:
|
||||
* 1. ZERO_PROVIDER_COMMAND (external command) - highest
|
||||
* 2. Active profile from config (set via /provider)
|
||||
* 3. OPENAI_* environment variables
|
||||
*/
|
||||
export async function loadProviderConfig(): Promise<ProviderConfig> {
|
||||
// 1. Highest priority: external provider command
|
||||
const providerCommand = process.env.ZERO_PROVIDER_COMMAND;
|
||||
if (providerCommand) {
|
||||
try {
|
||||
const parts = parseProviderCommand(providerCommand);
|
||||
const program = parts[0];
|
||||
const args = parts.slice(1);
|
||||
|
||||
const proc = Bun.spawn([program, ...args], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
});
|
||||
|
||||
const stdout = await new Response(proc.stdout).text();
|
||||
const exitCode = await proc.exited;
|
||||
|
||||
if (exitCode !== 0) {
|
||||
const stderr = await new Response(proc.stderr).text();
|
||||
throw new Error(`Command exited with code ${exitCode}: ${stderr}`);
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(stdout);
|
||||
return resolveProviderCommandConfig(parsed);
|
||||
} catch (err: any) {
|
||||
throw new Error(
|
||||
`Failed to run ZERO_PROVIDER_COMMAND "${providerCommand}": ${err.message}\n` +
|
||||
'Expected JSON output with provider configuration.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Active profile from saved config
|
||||
const fromProfile = configManager.getEffectiveProviderConfig();
|
||||
if (fromProfile) {
|
||||
return fromProfile;
|
||||
}
|
||||
|
||||
// 3. Fallback to raw environment variables
|
||||
const envApiKey = process.env.OPENAI_API_KEY;
|
||||
const envBaseURL = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
||||
const envModel = process.env.OPENAI_MODEL || 'gpt-4o';
|
||||
|
||||
// If we have no API key, give a helpful error
|
||||
if (!envApiKey) {
|
||||
throw new Error(
|
||||
'No LLM provider configured.\n\n' +
|
||||
'Please run /provider to add one, or set OPENAI_API_KEY environment variable.'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
apiKey: envApiKey,
|
||||
baseURL: envBaseURL,
|
||||
model: envModel,
|
||||
};
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export interface ProviderProfile {
|
||||
name: string;
|
||||
providerId?: string;
|
||||
kind?: 'provider' | 'gateway' | 'localhost' | 'custom';
|
||||
baseURL: string;
|
||||
apiKey?: string;
|
||||
model: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ZeroConfig {
|
||||
activeProvider?: string;
|
||||
providers: ProviderProfile[];
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { Command } from 'commander';
|
||||
import { runHeadless } from './cli';
|
||||
import { configManager } from './config/manager';
|
||||
import { startTUI } from './tui';
|
||||
import { listProviderDefinitions } from './providers/catalog';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('zero')
|
||||
.description('A clean terminal AI coding agent')
|
||||
.version('0.1.0');
|
||||
|
||||
program
|
||||
.option('-p, --prompt <prompt>', 'Run in headless mode with the given prompt')
|
||||
.action(async (options) => {
|
||||
if (options.prompt) {
|
||||
await runHeadless(options.prompt);
|
||||
} else {
|
||||
// Launch the interactive TUI (Grok Build style)
|
||||
startTUI();
|
||||
}
|
||||
});
|
||||
|
||||
// Providers subcommand (temporary until we have a nice /provider in the TUI)
|
||||
const providersCmd = program.command('providers');
|
||||
|
||||
providersCmd
|
||||
.command('list')
|
||||
.description('List all saved providers')
|
||||
.action(() => {
|
||||
const providers = configManager.listProviders();
|
||||
const active = configManager.getActiveProvider()?.name;
|
||||
|
||||
if (providers.length === 0) {
|
||||
console.log('No providers configured yet.');
|
||||
console.log('Use the /provider command once the TUI is ready, or edit ~/.config/zero/config.json');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\nSaved Providers:\n');
|
||||
providers.forEach(p => {
|
||||
const isActive = p.name === active ? ' (active)' : '';
|
||||
console.log(` ${p.name}${isActive}`);
|
||||
console.log(` Model: ${p.model}`);
|
||||
console.log(` BaseURL: ${p.baseURL}`);
|
||||
if (p.description) console.log(` Desc: ${p.description}`);
|
||||
console.log('');
|
||||
});
|
||||
});
|
||||
|
||||
providersCmd
|
||||
.command('switch <name>')
|
||||
.description('Switch the active provider')
|
||||
.action((name: string) => {
|
||||
const success = configManager.setActiveProvider(name);
|
||||
if (success) {
|
||||
console.log(`Switched to provider: ${name}`);
|
||||
} else {
|
||||
console.error(`Provider "${name}" not found.`);
|
||||
}
|
||||
});
|
||||
|
||||
providersCmd
|
||||
.command('current')
|
||||
.description('Show the currently active provider')
|
||||
.action(() => {
|
||||
const active = configManager.getActiveProvider();
|
||||
if (active) {
|
||||
console.log(`Active provider: ${active.name}`);
|
||||
console.log(`Model: ${active.model}`);
|
||||
console.log(`Base URL: ${active.baseURL}`);
|
||||
} else {
|
||||
console.log('No active provider set.');
|
||||
}
|
||||
});
|
||||
|
||||
providersCmd
|
||||
.command('catalog')
|
||||
.description('List built-in provider and gateway catalog entries')
|
||||
.action(() => {
|
||||
const catalog = listProviderDefinitions();
|
||||
|
||||
if (catalog.length === 0) {
|
||||
console.log('No catalog providers found.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\nProvider Catalog:\n');
|
||||
catalog.forEach((provider) => {
|
||||
console.log(` ${provider.name} (${provider.kind})`);
|
||||
console.log(` ID: ${provider.id}`);
|
||||
console.log(` Model: ${provider.defaultModel}`);
|
||||
console.log(` BaseURL: ${provider.baseURL}`);
|
||||
console.log('');
|
||||
});
|
||||
});
|
||||
|
||||
program.parse();
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { AuthHeaderConfig } from '../providers/catalog/types';
|
||||
|
||||
/**
|
||||
* Build auth headers from API key and auth header config.
|
||||
* Returns null values to suppress default headers.
|
||||
*/
|
||||
export function buildAuthHeaders(
|
||||
apiKey: string,
|
||||
authHeader?: AuthHeaderConfig
|
||||
): Record<string, string | null> {
|
||||
if (!authHeader) return {};
|
||||
if (!apiKey) {
|
||||
console.warn('[providers] Auth header configured but no API key provided');
|
||||
return {};
|
||||
}
|
||||
|
||||
const headerName = authHeader.name;
|
||||
const lowerName = headerName.toLowerCase();
|
||||
const headerValue = authHeader.scheme === 'raw' ? apiKey : `Bearer ${apiKey}`;
|
||||
|
||||
if (lowerName === 'authorization' && authHeader.scheme !== 'raw') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
[headerName]: headerValue,
|
||||
...(lowerName === 'authorization' ? {} : { Authorization: null }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve header values, interpolating env vars.
|
||||
* Values starting with $ are resolved from process.env.
|
||||
* Null values are preserved (used to suppress default headers).
|
||||
*/
|
||||
export function resolveHeaders(headers: Record<string, string | null>): Record<string, string | null> {
|
||||
const resolved: Record<string, string | null> = {};
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
if (value == null) {
|
||||
resolved[name] = null;
|
||||
continue;
|
||||
}
|
||||
if (value.startsWith('$')) {
|
||||
const envVar = value.slice(1);
|
||||
const envValue = process.env[envVar];
|
||||
if (envValue) {
|
||||
resolved[name] = envValue;
|
||||
} else {
|
||||
console.warn(`[providers] Header "${name}" references unset env var $${envVar}`);
|
||||
}
|
||||
} else {
|
||||
resolved[name] = value;
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
# Groq - Fast inference gateway
|
||||
|
||||
id = "groq"
|
||||
name = "Groq"
|
||||
kind = "gateway"
|
||||
description = "Fast inference with LPU hardware"
|
||||
category = "hosted"
|
||||
baseURL = "https://api.groq.com/openai/v1"
|
||||
defaultModel = "llama-3.3-70b-versatile"
|
||||
apiKeyRequired = true
|
||||
credentialEnvVars = ["GROQ_API_KEY"]
|
||||
costCurrency = "USD"
|
||||
|
||||
[setup]
|
||||
requiresAuth = true
|
||||
authMode = "api-key"
|
||||
|
||||
[transportConfig]
|
||||
kind = "openai-compatible"
|
||||
maxTokensField = "max_completion_tokens"
|
||||
removeBodyFields = ["store"]
|
||||
|
||||
[catalog]
|
||||
source = "hybrid"
|
||||
discoveryCacheTtl = "30m"
|
||||
discoveryRefreshMode = "on-open"
|
||||
discovery.kind = "openai-compatible"
|
||||
discovery.requiresAuth = true
|
||||
|
||||
[[catalog.models]]
|
||||
id = "llama-3.3-70b-versatile"
|
||||
name = "Llama 3.3 70B"
|
||||
tier = "hosted"
|
||||
contextWindow = 131072
|
||||
maxOutputTokens = 32768
|
||||
capabilities.supportsFunctionCalling = true
|
||||
capabilities.supportsStreaming = true
|
||||
capabilities.supportsJsonMode = true
|
||||
capabilities.supportsReasoning = false
|
||||
cost.inputPerMillion = 0.59
|
||||
cost.outputPerMillion = 0.79
|
||||
|
||||
[[catalog.models]]
|
||||
id = "llama-3.1-8b-instant"
|
||||
name = "Llama 3.1 8B Instant"
|
||||
tier = "hosted"
|
||||
contextWindow = 131072
|
||||
maxOutputTokens = 8192
|
||||
capabilities.supportsFunctionCalling = true
|
||||
capabilities.supportsStreaming = true
|
||||
capabilities.supportsJsonMode = true
|
||||
capabilities.supportsReasoning = false
|
||||
cost.inputPerMillion = 0.05
|
||||
cost.outputPerMillion = 0.08
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "deepseek-v3"
|
||||
apiName = "deepseek-r1-distill-llama-70b"
|
||||
contextWindow = 131072
|
||||
maxOutputTokens = 16384
|
||||
capabilities.supportsReasoning = true
|
||||
cost.inputPerMillion = 0.75
|
||||
cost.outputPerMillion = 0.99
|
||||
|
||||
[preset]
|
||||
id = "groq"
|
||||
description = "Groq fast inference"
|
||||
fallbackBaseUrl = "https://api.groq.com/openai/v1"
|
||||
fallbackModel = "llama-3.3-70b-versatile"
|
||||
@@ -1,41 +0,0 @@
|
||||
# Ollama - Local models via Ollama
|
||||
# No auth required, auto-detectable on localhost
|
||||
|
||||
id = "ollama"
|
||||
name = "Ollama"
|
||||
kind = "localhost"
|
||||
description = "Local models via Ollama"
|
||||
category = "local"
|
||||
baseURL = "http://localhost:11434/v1"
|
||||
defaultModel = "llama3.2"
|
||||
apiKeyRequired = false
|
||||
|
||||
[setup]
|
||||
requiresAuth = false
|
||||
authMode = "none"
|
||||
|
||||
[startup]
|
||||
autoDetectable = true
|
||||
probeReadiness = "openai-compatible-models"
|
||||
|
||||
[transportConfig]
|
||||
kind = "openai-compatible"
|
||||
|
||||
[catalog]
|
||||
source = "dynamic"
|
||||
discoveryCacheTtl = "5m"
|
||||
discoveryRefreshMode = "on-open"
|
||||
allowManualRefresh = true
|
||||
# Ollama models are discovered dynamically from /api/tags
|
||||
discovery.kind = "ollama"
|
||||
discovery.requiresAuth = false
|
||||
|
||||
[usage]
|
||||
supported = false
|
||||
silentlyIgnore = true
|
||||
|
||||
[preset]
|
||||
id = "ollama"
|
||||
description = "Local Ollama models"
|
||||
fallbackBaseUrl = "http://localhost:11434/v1"
|
||||
fallbackModel = "llama3.2"
|
||||
@@ -1,50 +0,0 @@
|
||||
# OpenAI - Direct API access
|
||||
# First-party provider: auto-exposes all global models
|
||||
|
||||
id = "openai"
|
||||
name = "OpenAI"
|
||||
kind = "provider"
|
||||
description = "OpenAI direct API"
|
||||
category = "hosted"
|
||||
baseURL = "https://api.openai.com/v1"
|
||||
defaultModel = "gpt-4o"
|
||||
isFirstParty = true
|
||||
apiKeyRequired = true
|
||||
credentialEnvVars = ["OPENAI_API_KEY"]
|
||||
costCurrency = "USD"
|
||||
|
||||
[setup]
|
||||
requiresAuth = true
|
||||
authMode = "api-key"
|
||||
|
||||
[transportConfig]
|
||||
kind = "openai-compatible"
|
||||
|
||||
[catalog]
|
||||
source = "hybrid"
|
||||
discoveryCacheTtl = "1h"
|
||||
discoveryRefreshMode = "manual"
|
||||
allowManualRefresh = true
|
||||
discovery.kind = "openai-compatible"
|
||||
discovery.requiresAuth = true
|
||||
discovery.path = "/models"
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "gpt-4o"
|
||||
cost.inputPerMillion = 2.50
|
||||
cost.outputPerMillion = 10.00
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "gpt-4o-mini"
|
||||
cost.inputPerMillion = 0.15
|
||||
cost.outputPerMillion = 0.60
|
||||
|
||||
[validation]
|
||||
kind = "credential-env"
|
||||
missingCredentialMessage = "An OpenAI API key is required."
|
||||
|
||||
[preset]
|
||||
id = "openai"
|
||||
description = "OpenAI direct API"
|
||||
fallbackBaseUrl = "https://api.openai.com/v1"
|
||||
fallbackModel = "gpt-4o"
|
||||
@@ -1,63 +0,0 @@
|
||||
# OpenGateway - OpenAI-compatible gateway hosted by Gitlawb
|
||||
|
||||
id = "opengateway"
|
||||
name = "OpenGateway"
|
||||
kind = "gateway"
|
||||
description = "OpenAI-compatible gateway hosted by Gitlawb"
|
||||
category = "aggregating"
|
||||
baseURL = "https://opengateway.gitlawb.com/v1"
|
||||
defaultModel = "mimo-v2.5-pro"
|
||||
supportsModelRouting = true
|
||||
apiKeyLabel = "OpenGateway API key"
|
||||
apiKeyPlaceholder = "ogw_live_..."
|
||||
apiKeyRequired = true
|
||||
credentialEnvVars = ["OPENGATEWAY_API_KEY"]
|
||||
|
||||
[setup]
|
||||
requiresAuth = true
|
||||
authMode = "api-key"
|
||||
|
||||
[validation]
|
||||
kind = "credential-env"
|
||||
missingCredentialMessage = "An OpenGateway API key is required."
|
||||
matchBaseUrlHosts = ["opengateway.gitlawb.com", "opengateway.fly.dev"]
|
||||
|
||||
[transportConfig]
|
||||
kind = "openai-compatible"
|
||||
maxTokensField = "max_completion_tokens"
|
||||
removeBodyFields = ["store", "stream_options"]
|
||||
authHeader.name = "authorization"
|
||||
authHeader.scheme = "bearer"
|
||||
|
||||
[catalog]
|
||||
source = "hybrid"
|
||||
discoveryCacheTtl = "1h"
|
||||
discoveryRefreshMode = "manual"
|
||||
allowManualRefresh = true
|
||||
discovery.kind = "openai-compatible"
|
||||
discovery.requiresAuth = true
|
||||
discovery.path = "/models"
|
||||
|
||||
# Native model to this gateway
|
||||
[[catalog.models]]
|
||||
id = "mimo-v2.5-pro"
|
||||
apiName = "mimo-v2.5-pro"
|
||||
name = "Mimo v2.5 Pro"
|
||||
tier = "hosted"
|
||||
|
||||
# Global model with reduced limits
|
||||
[[catalog.models]]
|
||||
globalModelId = "gpt-4o"
|
||||
contextWindow = 32000
|
||||
maxOutputTokens = 4096
|
||||
|
||||
[usage]
|
||||
supported = false
|
||||
|
||||
[preset]
|
||||
id = "opengateway"
|
||||
description = "OpenGateway hosted models"
|
||||
fallbackBaseUrl = "https://opengateway.gitlawb.com/v1"
|
||||
fallbackModel = "mimo-v2.5-pro"
|
||||
badge.text = "FREE"
|
||||
badge.color = "success"
|
||||
@@ -1,62 +0,0 @@
|
||||
# OpenRouter - Multi-provider gateway
|
||||
|
||||
id = "openrouter"
|
||||
name = "OpenRouter"
|
||||
kind = "gateway"
|
||||
description = "Multi-provider gateway with hundreds of models"
|
||||
category = "aggregating"
|
||||
baseURL = "https://openrouter.ai/api/v1"
|
||||
defaultModel = "openai/gpt-4o"
|
||||
apiKeyRequired = true
|
||||
credentialEnvVars = ["OPENROUTER_API_KEY"]
|
||||
costCurrency = "USD"
|
||||
|
||||
[setup]
|
||||
requiresAuth = true
|
||||
authMode = "api-key"
|
||||
|
||||
[transportConfig]
|
||||
kind = "openai-compatible"
|
||||
# Optional attribution headers
|
||||
headers.HTTP-Referer = "$OPENROUTER_SITE_URL"
|
||||
headers.X-OpenRouter-Title = "$OPENROUTER_SITE_NAME"
|
||||
|
||||
[catalog]
|
||||
source = "hybrid"
|
||||
discoveryCacheTtl = "1h"
|
||||
discoveryRefreshMode = "manual"
|
||||
# OpenRouter uses /api/v1/models (full path from baseURL)
|
||||
discovery.kind = "openai-compatible"
|
||||
discovery.requiresAuth = true
|
||||
discovery.path = "/models"
|
||||
|
||||
# Curated static entries shown first
|
||||
[[catalog.models]]
|
||||
globalModelId = "gpt-4o"
|
||||
apiName = "openai/gpt-4o"
|
||||
contextWindow = 128000
|
||||
maxOutputTokens = 16384
|
||||
cost.inputPerMillion = 2.50
|
||||
cost.outputPerMillion = 10.00
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "claude-sonnet-4"
|
||||
apiName = "anthropic/claude-sonnet-4"
|
||||
contextWindow = 200000
|
||||
maxOutputTokens = 64000
|
||||
cost.inputPerMillion = 3.00
|
||||
cost.outputPerMillion = 15.00
|
||||
|
||||
[[catalog.models]]
|
||||
globalModelId = "deepseek-v3"
|
||||
apiName = "deepseek/deepseek-chat"
|
||||
contextWindow = 65536
|
||||
maxOutputTokens = 8192
|
||||
cost.inputPerMillion = 0.14
|
||||
cost.outputPerMillion = 0.28
|
||||
|
||||
[preset]
|
||||
id = "openrouter"
|
||||
description = "OpenRouter multi-provider gateway"
|
||||
fallbackBaseUrl = "https://openrouter.ai/api/v1"
|
||||
fallbackModel = "openai/gpt-4o"
|
||||
@@ -1,358 +0,0 @@
|
||||
import { existsSync, readdirSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { parse } from 'smol-toml';
|
||||
import type {
|
||||
ModelDefinition,
|
||||
ProviderModelDefinition,
|
||||
ProviderDefinition,
|
||||
ResolvedModelDefinition,
|
||||
TransportConfig,
|
||||
} from './types';
|
||||
|
||||
type EmbeddedFile = Blob & { name: string };
|
||||
|
||||
function parseTomlFile(content: string, directory: 'definitions' | 'models'): unknown[] {
|
||||
const parsed = parse(content);
|
||||
|
||||
if (directory === 'definitions') {
|
||||
// Each definition file is a single object
|
||||
return [parsed];
|
||||
}
|
||||
|
||||
// Model files use [[model]] array-of-tables syntax.
|
||||
// Parsed result is { model: [...] }. Extract the array.
|
||||
if (parsed && typeof parsed === 'object' && 'model' in parsed) {
|
||||
const models = (parsed as any).model;
|
||||
return Array.isArray(models) ? models : [models];
|
||||
}
|
||||
|
||||
return [parsed];
|
||||
}
|
||||
|
||||
async function readTomlFiles<T>(directory: 'definitions' | 'models'): Promise<T[]> {
|
||||
const directoryPath = join(import.meta.dir, directory);
|
||||
let items: unknown[] = [];
|
||||
|
||||
if (existsSync(directoryPath)) {
|
||||
items = readdirSync(directoryPath)
|
||||
.filter((file) => file.endsWith('.toml'))
|
||||
.flatMap((file) => parseTomlFile(readFileSync(join(directoryPath, file), 'utf-8'), directory));
|
||||
} else {
|
||||
// Fallback for bundled/embedded files
|
||||
const embeddedFiles = (globalThis as any).Bun?.embeddedFiles as EmbeddedFile[] | undefined;
|
||||
if (!embeddedFiles) {
|
||||
console.warn(`[providers] No ${directory} directory and no embedded files found`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const tomlFiles = embeddedFiles.filter((file) => file.name.endsWith('.toml'));
|
||||
if (tomlFiles.length === 0) {
|
||||
console.warn(`[providers] No .toml files found in embedded files for ${directory}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
items = (await Promise.all(
|
||||
tomlFiles.map(async (file) => parseTomlFile(await file.text(), directory))
|
||||
)).flat();
|
||||
}
|
||||
|
||||
if (directory === 'definitions') {
|
||||
return items.filter(isProviderDefinition) as T[];
|
||||
}
|
||||
|
||||
return items.filter(isModelDefinition) as T[];
|
||||
}
|
||||
|
||||
function isProviderDefinition(value: any): value is ProviderDefinition {
|
||||
return (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.id === 'string' &&
|
||||
typeof value.name === 'string' &&
|
||||
(value.kind === 'provider' || value.kind === 'gateway' || value.kind === 'localhost') &&
|
||||
typeof value.baseURL === 'string' &&
|
||||
typeof value.defaultModel === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
function isModelDefinition(value: any): value is ModelDefinition {
|
||||
return (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.id === 'string' &&
|
||||
// Positive: model definitions have these fields
|
||||
!('baseURL' in value) &&
|
||||
!('defaultModel' in value) &&
|
||||
// Negative: provider definitions have 'kind' as a string enum
|
||||
// Models don't have 'kind' at the top level
|
||||
!('kind' in value && typeof value.kind === 'string' &&
|
||||
['provider', 'gateway', 'localhost'].includes(value.kind))
|
||||
);
|
||||
}
|
||||
|
||||
function validateModelDefinition(model: ModelDefinition): void {
|
||||
if ('cost' in model) {
|
||||
console.warn(`[providers] Global model "${model.id}": cost belongs on provider catalog models, not global models`);
|
||||
}
|
||||
if (model.contextWindow != null && typeof model.contextWindow !== 'number') {
|
||||
console.warn(`[providers] Model "${model.id}": contextWindow should be a number, got ${typeof model.contextWindow}`);
|
||||
}
|
||||
if (model.maxOutputTokens != null && typeof model.maxOutputTokens !== 'number') {
|
||||
console.warn(`[providers] Model "${model.id}": maxOutputTokens should be a number, got ${typeof model.maxOutputTokens}`);
|
||||
}
|
||||
if (model.defaultTemperature != null && typeof model.defaultTemperature !== 'number') {
|
||||
console.warn(`[providers] Model "${model.id}": defaultTemperature should be a number, got ${typeof model.defaultTemperature}`);
|
||||
}
|
||||
if (model.capabilities != null && typeof model.capabilities !== 'object') {
|
||||
console.warn(`[providers] Model "${model.id}": capabilities should be an object, got ${typeof model.capabilities}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateProviderModelDefinition(model: ProviderModelDefinition, providerId: string): void {
|
||||
if (model.cost != null) {
|
||||
if (typeof model.cost !== 'object') {
|
||||
console.warn(`[providers] Provider "${providerId}" model "${model.id ?? model.globalModelId}": cost should be an object, got ${typeof model.cost}`);
|
||||
} else {
|
||||
if (typeof model.cost.inputPerMillion !== 'number') {
|
||||
console.warn(`[providers] Provider "${providerId}" model "${model.id ?? model.globalModelId}": cost.inputPerMillion should be a number`);
|
||||
}
|
||||
if (typeof model.cost.outputPerMillion !== 'number') {
|
||||
console.warn(`[providers] Provider "${providerId}" model "${model.id ?? model.globalModelId}": cost.outputPerMillion should be a number`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a model entry that references a global model via globalModelId.
|
||||
* The global model's fields are used as base; the entry's explicit fields override.
|
||||
*/
|
||||
function resolveGlobalModelReference(
|
||||
model: ProviderModelDefinition,
|
||||
globalModelById: Map<string, ModelDefinition>
|
||||
): ProviderModelDefinition {
|
||||
if (!model.globalModelId) return model;
|
||||
|
||||
const globalModel = globalModelById.get(model.globalModelId);
|
||||
if (!globalModel) return model;
|
||||
|
||||
return {
|
||||
...globalModel,
|
||||
...model,
|
||||
// Only override id/apiName/name if the entry explicitly sets them
|
||||
id: model.id ?? globalModel.id,
|
||||
apiName: model.apiName ?? model.id ?? globalModel.apiName ?? globalModel.id,
|
||||
name: model.name ?? globalModel.name,
|
||||
tier: model.tier ?? globalModel.tier,
|
||||
description: model.description ?? globalModel.description,
|
||||
classification: model.classification ?? globalModel.classification,
|
||||
capabilities: {
|
||||
...globalModel.capabilities,
|
||||
...model.capabilities,
|
||||
},
|
||||
transportOverrides: model.transportOverrides ?? globalModel.transportOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a definition with defaults, and resolve vendorId links.
|
||||
*/
|
||||
function normalizeDefinition(
|
||||
definition: ProviderDefinition,
|
||||
definitionById: Map<string, ProviderDefinition>
|
||||
): ProviderDefinition {
|
||||
const credentialEnvVars =
|
||||
definition.credentialEnvVars ??
|
||||
definition.setup?.credentialEnvVars ??
|
||||
definition.validation?.credentialEnvVars ??
|
||||
definition.preset?.apiKeyEnvVars;
|
||||
|
||||
const setup = {
|
||||
...(definition.setup ?? {
|
||||
requiresAuth: definition.apiKeyRequired !== false,
|
||||
authMode: definition.apiKeyRequired === false ? 'none' : 'api-key',
|
||||
} as const),
|
||||
credentialEnvVars: definition.setup?.credentialEnvVars ?? credentialEnvVars,
|
||||
};
|
||||
|
||||
const validation = definition.validation
|
||||
? {
|
||||
...definition.validation,
|
||||
credentialEnvVars: definition.validation.credentialEnvVars ?? credentialEnvVars,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const preset = definition.preset
|
||||
? {
|
||||
...definition.preset,
|
||||
apiKeyEnvVars: definition.preset.apiKeyEnvVars ?? credentialEnvVars,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const catalog = definition.catalog ?? {
|
||||
source: definition.models ? 'static' : 'dynamic',
|
||||
models: definition.models,
|
||||
} as const;
|
||||
|
||||
// Resolve vendorId: inherit transport config from linked vendor
|
||||
let transportConfig = definition.transportConfig;
|
||||
if (definition.vendorId && !transportConfig) {
|
||||
const vendor = definitionById.get(definition.vendorId);
|
||||
if (vendor?.transportConfig) {
|
||||
transportConfig = vendor.transportConfig;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...definition,
|
||||
setup,
|
||||
validation,
|
||||
preset,
|
||||
transportConfig: transportConfig ?? {
|
||||
kind: 'openai-compatible',
|
||||
},
|
||||
catalog,
|
||||
apiKeyRequired: definition.apiKeyRequired ?? setup.requiresAuth,
|
||||
credentialEnvVars,
|
||||
models: definition.models ?? catalog.models,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveModel(
|
||||
model: ProviderModelDefinition,
|
||||
definition?: ProviderDefinition
|
||||
): ResolvedModelDefinition {
|
||||
return {
|
||||
...model,
|
||||
apiName: model.apiName ?? model.id,
|
||||
providerId: definition?.id,
|
||||
providerName: definition?.name,
|
||||
providerKind: definition?.kind,
|
||||
};
|
||||
}
|
||||
|
||||
// First pass: raw definitions for vendorId resolution
|
||||
const rawDefinitions = (await readTomlFiles<ProviderDefinition>('definitions'))
|
||||
.filter((definition): definition is ProviderDefinition => Boolean(definition));
|
||||
|
||||
const rawDefinitionById = new Map(rawDefinitions.map((d) => [d.id, d]));
|
||||
|
||||
// Second pass: normalize with vendorId resolution
|
||||
const definitions = rawDefinitions
|
||||
.map((definition) => normalizeDefinition(definition, rawDefinitionById))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const globalModels = (await readTomlFiles<ModelDefinition>('models'))
|
||||
.sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
// Validate parsed models
|
||||
for (const model of globalModels) {
|
||||
validateModelDefinition(model);
|
||||
}
|
||||
|
||||
for (const definition of definitions) {
|
||||
for (const model of definition.catalog?.models ?? definition.models ?? []) {
|
||||
validateProviderModelDefinition(model, definition.id);
|
||||
}
|
||||
}
|
||||
|
||||
const definitionById = new Map(definitions.map((definition) => [definition.id, definition]));
|
||||
const globalModelById = new Map(globalModels.map((model) => [model.id, model]));
|
||||
|
||||
export function listProviderDefinitions(): ProviderDefinition[] {
|
||||
return [...definitions];
|
||||
}
|
||||
|
||||
export function getProviderDefinition(id: string): ProviderDefinition | undefined {
|
||||
return definitionById.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resolved models for a specific provider definition.
|
||||
*/
|
||||
export function getModelsForProvider(definition: ProviderDefinition): ResolvedModelDefinition[] {
|
||||
const catalogModels = definition.catalog?.models ?? definition.models ?? [];
|
||||
|
||||
const resolvedCatalogModels = catalogModels.map((model) =>
|
||||
resolveGlobalModelReference(model, globalModelById)
|
||||
);
|
||||
|
||||
if (definition.isFirstParty) {
|
||||
const catalogApiNames = new Set(
|
||||
resolvedCatalogModels.map((m) => (m.apiName ?? m.id).toLowerCase())
|
||||
);
|
||||
const additionalGlobals = globalModels.filter(
|
||||
(m) => m.ownerProviderId === definition.id && !catalogApiNames.has(m.id.toLowerCase())
|
||||
);
|
||||
return [
|
||||
...resolvedCatalogModels.map((m) => resolveModel(m, definition)),
|
||||
...additionalGlobals.map((m) => resolveModel(m, definition)),
|
||||
];
|
||||
}
|
||||
|
||||
return resolvedCatalogModels.map((m) => resolveModel(m, definition));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective transport config for a specific model within a provider.
|
||||
* Definition-level config is the base; per-model overrides are merged on top.
|
||||
* removeBodyFields is unioned (not replaced).
|
||||
*/
|
||||
export function resolveEffectiveTransportConfig(
|
||||
definition: ProviderDefinition,
|
||||
model?: ProviderModelDefinition
|
||||
): TransportConfig | undefined {
|
||||
const base = definition.transportConfig;
|
||||
if (!base) return undefined;
|
||||
if (!model?.transportOverrides) return base;
|
||||
|
||||
const overrides = model.transportOverrides;
|
||||
return {
|
||||
...base,
|
||||
...overrides,
|
||||
headers: {
|
||||
...(base.headers ?? {}),
|
||||
...(overrides.headers ?? {}),
|
||||
},
|
||||
removeBodyFields: [
|
||||
...new Set([
|
||||
...(base.removeBodyFields ?? []),
|
||||
...(overrides.removeBodyFields ?? []),
|
||||
]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function listModelDefinitions(): ResolvedModelDefinition[] {
|
||||
const routeModels = definitions.flatMap((definition) =>
|
||||
getModelsForProvider(definition)
|
||||
);
|
||||
|
||||
// Deduplicate by apiName, preferring provider-attributed versions
|
||||
const allModels = [...globalModels.map((model) => resolveModel(model)), ...routeModels];
|
||||
const seen = new Map<string, ResolvedModelDefinition>();
|
||||
for (const model of allModels) {
|
||||
const key = model.apiName.toLowerCase();
|
||||
// Prefer versions with providerId over bare global models
|
||||
if (!seen.has(key) || (model.providerId && !seen.get(key)?.providerId)) {
|
||||
seen.set(key, model);
|
||||
}
|
||||
}
|
||||
return [...seen.values()];
|
||||
}
|
||||
|
||||
export function getGlobalModelDefinition(id: string): ModelDefinition | undefined {
|
||||
return globalModelById.get(id);
|
||||
}
|
||||
|
||||
export function listGlobalModels(): ModelDefinition[] {
|
||||
return [...globalModels];
|
||||
}
|
||||
|
||||
export function listProvidersByKind(kind: ProviderDefinition['kind']): ProviderDefinition[] {
|
||||
return definitions.filter((d) => d.kind === kind);
|
||||
}
|
||||
|
||||
export function listFirstPartyProviders(): ProviderDefinition[] {
|
||||
return definitions.filter((d) => d.isFirstParty === true);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
# Anthropic Claude models
|
||||
|
||||
[[model]]
|
||||
id = "claude-sonnet-4"
|
||||
name = "Claude Sonnet 4"
|
||||
ownerProviderId = "anthropic"
|
||||
tier = "first-party"
|
||||
description = "Anthropic balanced model"
|
||||
classification = ["chat", "vision", "coding"]
|
||||
contextWindow = 200000
|
||||
maxOutputTokens = 64000
|
||||
capabilities.supportsVision = true
|
||||
capabilities.supportsStreaming = true
|
||||
capabilities.supportsFunctionCalling = true
|
||||
capabilities.supportsJsonMode = true
|
||||
capabilities.supportsReasoning = true
|
||||
@@ -1,16 +0,0 @@
|
||||
# DeepSeek models
|
||||
|
||||
[[model]]
|
||||
id = "deepseek-v3"
|
||||
name = "DeepSeek V3"
|
||||
ownerProviderId = "deepseek"
|
||||
tier = "first-party"
|
||||
description = "DeepSeek general model"
|
||||
classification = ["chat", "coding"]
|
||||
contextWindow = 65536
|
||||
maxOutputTokens = 8192
|
||||
capabilities.supportsVision = false
|
||||
capabilities.supportsStreaming = true
|
||||
capabilities.supportsFunctionCalling = true
|
||||
capabilities.supportsJsonMode = true
|
||||
capabilities.supportsReasoning = false
|
||||
@@ -1,31 +0,0 @@
|
||||
# Google Gemini models
|
||||
|
||||
[[model]]
|
||||
id = "gemini-2.5-pro"
|
||||
name = "Gemini 2.5 Pro"
|
||||
ownerProviderId = "google"
|
||||
tier = "first-party"
|
||||
description = "Google flagship model"
|
||||
classification = ["chat", "vision", "coding"]
|
||||
contextWindow = 1048576
|
||||
maxOutputTokens = 65536
|
||||
capabilities.supportsVision = true
|
||||
capabilities.supportsStreaming = true
|
||||
capabilities.supportsFunctionCalling = true
|
||||
capabilities.supportsJsonMode = true
|
||||
capabilities.supportsReasoning = true
|
||||
|
||||
[[model]]
|
||||
id = "gemini-2.5-flash"
|
||||
name = "Gemini 2.5 Flash"
|
||||
ownerProviderId = "google"
|
||||
tier = "first-party"
|
||||
description = "Google fast model"
|
||||
classification = ["chat", "vision"]
|
||||
contextWindow = 1048576
|
||||
maxOutputTokens = 65536
|
||||
capabilities.supportsVision = true
|
||||
capabilities.supportsStreaming = true
|
||||
capabilities.supportsFunctionCalling = true
|
||||
capabilities.supportsJsonMode = true
|
||||
capabilities.supportsReasoning = false
|
||||
@@ -1,31 +0,0 @@
|
||||
# OpenAI GPT models
|
||||
|
||||
[[model]]
|
||||
id = "gpt-4o"
|
||||
name = "GPT-4o"
|
||||
ownerProviderId = "openai"
|
||||
tier = "first-party"
|
||||
description = "OpenAI flagship multimodal model"
|
||||
classification = ["chat", "vision", "coding"]
|
||||
contextWindow = 128000
|
||||
maxOutputTokens = 16384
|
||||
capabilities.supportsVision = true
|
||||
capabilities.supportsStreaming = true
|
||||
capabilities.supportsFunctionCalling = true
|
||||
capabilities.supportsJsonMode = true
|
||||
capabilities.supportsReasoning = false
|
||||
|
||||
[[model]]
|
||||
id = "gpt-4o-mini"
|
||||
name = "GPT-4o Mini"
|
||||
ownerProviderId = "openai"
|
||||
tier = "first-party"
|
||||
description = "OpenAI small multimodal model"
|
||||
classification = ["chat", "vision"]
|
||||
contextWindow = 128000
|
||||
maxOutputTokens = 16384
|
||||
capabilities.supportsVision = true
|
||||
capabilities.supportsStreaming = true
|
||||
capabilities.supportsFunctionCalling = true
|
||||
capabilities.supportsJsonMode = true
|
||||
capabilities.supportsReasoning = false
|
||||
@@ -1,31 +0,0 @@
|
||||
# OpenAI reasoning models
|
||||
|
||||
[[model]]
|
||||
id = "o1"
|
||||
name = "o1"
|
||||
ownerProviderId = "openai"
|
||||
tier = "first-party"
|
||||
description = "OpenAI reasoning model"
|
||||
classification = ["chat", "reasoning", "coding"]
|
||||
contextWindow = 200000
|
||||
maxOutputTokens = 100000
|
||||
capabilities.supportsVision = false
|
||||
capabilities.supportsStreaming = true
|
||||
capabilities.supportsFunctionCalling = true
|
||||
capabilities.supportsJsonMode = true
|
||||
capabilities.supportsReasoning = true
|
||||
|
||||
[[model]]
|
||||
id = "o1-mini"
|
||||
name = "o1-mini"
|
||||
ownerProviderId = "openai"
|
||||
tier = "first-party"
|
||||
description = "OpenAI small reasoning model"
|
||||
classification = ["chat", "reasoning"]
|
||||
contextWindow = 128000
|
||||
maxOutputTokens = 65536
|
||||
capabilities.supportsVision = false
|
||||
capabilities.supportsStreaming = true
|
||||
capabilities.supportsFunctionCalling = true
|
||||
capabilities.supportsJsonMode = true
|
||||
capabilities.supportsReasoning = true
|
||||
@@ -1,166 +0,0 @@
|
||||
export type ProviderKind = 'provider' | 'gateway' | 'localhost';
|
||||
export type AuthMode = 'api-key' | 'oauth' | 'adc' | 'token' | 'none';
|
||||
export type TransportKind =
|
||||
| 'openai-compatible'
|
||||
| 'anthropic-compatible';
|
||||
export type ModelCatalogSource = 'static' | 'dynamic' | 'hybrid';
|
||||
export type DiscoveryRefreshMode = 'manual' | 'on-open' | 'background-if-stale' | 'startup';
|
||||
export type ModelDiscoveryKind = 'openai-compatible' | 'ollama' | 'custom';
|
||||
export type MaxTokensField = 'max_tokens' | 'max_completion_tokens';
|
||||
|
||||
export interface ModelDefinition {
|
||||
id: string;
|
||||
name?: string;
|
||||
apiName?: string;
|
||||
label?: string;
|
||||
default?: boolean;
|
||||
hidden?: boolean;
|
||||
globalModelId?: string;
|
||||
ownerProviderId?: string;
|
||||
classification?: ('chat' | 'reasoning' | 'vision' | 'coding')[];
|
||||
tier?: 'first-party' | 'hosted' | 'local' | 'community';
|
||||
description?: string;
|
||||
capabilities?: CapabilityFlags;
|
||||
contextWindow?: number;
|
||||
maxOutputTokens?: number;
|
||||
defaultTemperature?: number;
|
||||
temperatureRange?: TemperatureRange;
|
||||
transportOverrides?: Partial<TransportConfig>;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface ProviderModelDefinition extends ModelDefinition {
|
||||
cost?: ModelCost;
|
||||
}
|
||||
|
||||
export interface CapabilityFlags {
|
||||
supportsVision?: boolean;
|
||||
supportsStreaming?: boolean;
|
||||
supportsFunctionCalling?: boolean;
|
||||
supportsJsonMode?: boolean;
|
||||
supportsReasoning?: boolean;
|
||||
supportsPreciseTokenCount?: boolean;
|
||||
supportsEmbeddings?: boolean;
|
||||
supportsTemperature?: boolean;
|
||||
}
|
||||
|
||||
export interface TemperatureRange {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface ModelCost {
|
||||
inputPerMillion: number;
|
||||
outputPerMillion: number;
|
||||
cachePerMillion?: number;
|
||||
}
|
||||
|
||||
export interface AuthHeaderConfig {
|
||||
name: string;
|
||||
scheme?: 'bearer' | 'raw';
|
||||
}
|
||||
|
||||
export interface TransportConfig {
|
||||
kind: TransportKind;
|
||||
headers?: Record<string, string>;
|
||||
authHeader?: AuthHeaderConfig;
|
||||
maxTokensField?: MaxTokensField;
|
||||
removeBodyFields?: string[];
|
||||
endpointPath?: string;
|
||||
}
|
||||
|
||||
export interface ModelDiscoveryConfig {
|
||||
kind: ModelDiscoveryKind;
|
||||
requiresAuth?: boolean;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface ModelCatalogConfig {
|
||||
source: ModelCatalogSource;
|
||||
discovery?: ModelDiscoveryConfig;
|
||||
discoveryCacheTtl?: string | number;
|
||||
discoveryRefreshMode?: DiscoveryRefreshMode;
|
||||
allowManualRefresh?: boolean;
|
||||
models?: ProviderModelDefinition[];
|
||||
}
|
||||
|
||||
export interface SetupMetadata {
|
||||
requiresAuth: boolean;
|
||||
authMode: AuthMode;
|
||||
credentialEnvVars?: string[];
|
||||
setupPrompt?: string;
|
||||
}
|
||||
|
||||
export interface StartupMetadata {
|
||||
autoDetectable?: boolean;
|
||||
probeReadiness?: 'ollama-generation' | 'openai-compatible-models';
|
||||
enablementEnvVar?: string;
|
||||
}
|
||||
|
||||
export interface UsageMetadata {
|
||||
supported: boolean;
|
||||
silentlyIgnore?: boolean;
|
||||
}
|
||||
|
||||
export interface ValidationMetadata {
|
||||
kind: 'credential-env';
|
||||
credentialEnvVars?: string[];
|
||||
missingCredentialMessage?: string;
|
||||
matchBaseUrlHosts?: string[];
|
||||
}
|
||||
|
||||
export interface PresetBadge {
|
||||
text: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface ProviderPresetMetadata {
|
||||
id: string;
|
||||
description: string;
|
||||
label?: string;
|
||||
apiKeyEnvVars?: string[];
|
||||
fallbackBaseUrl?: string;
|
||||
fallbackModel?: string;
|
||||
badge?: PresetBadge;
|
||||
}
|
||||
|
||||
export interface ProviderDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: ProviderKind;
|
||||
vendorId?: string;
|
||||
category?: 'local' | 'hosted' | 'aggregating';
|
||||
description: string;
|
||||
baseURL: string;
|
||||
defaultModel: string;
|
||||
supportsModelRouting?: boolean;
|
||||
setup?: SetupMetadata;
|
||||
startup?: StartupMetadata;
|
||||
transportConfig?: TransportConfig;
|
||||
catalog?: ModelCatalogConfig;
|
||||
validation?: ValidationMetadata;
|
||||
usage?: UsageMetadata;
|
||||
preset?: ProviderPresetMetadata;
|
||||
isFirstParty?: boolean;
|
||||
apiKeyLabel?: string;
|
||||
apiKeyPlaceholder?: string;
|
||||
apiKeyRequired?: boolean;
|
||||
credentialEnvVars?: string[];
|
||||
costCurrency?: string;
|
||||
models?: ProviderModelDefinition[];
|
||||
}
|
||||
|
||||
export interface ResolvedModelDefinition extends ProviderModelDefinition {
|
||||
apiName: string;
|
||||
providerId?: string;
|
||||
providerName?: string;
|
||||
providerKind?: ProviderKind;
|
||||
}
|
||||
|
||||
export interface ModelDiscoveryResult {
|
||||
providerId: string;
|
||||
models: ResolvedModelDefinition[];
|
||||
source: 'static' | 'network' | 'cache' | 'stale-cache' | 'error';
|
||||
stale: boolean;
|
||||
error?: string;
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { createHash } from 'crypto';
|
||||
import { getProviderDefinition, getModelsForProvider, resolveModel } from './catalog';
|
||||
import { buildAuthHeaders, resolveHeaders } from './auth';
|
||||
import type {
|
||||
ModelDefinition,
|
||||
ModelDiscoveryResult,
|
||||
ProviderDefinition,
|
||||
ResolvedModelDefinition,
|
||||
} from './catalog/types';
|
||||
|
||||
interface CacheEntry {
|
||||
recordedAt: number;
|
||||
models: ResolvedModelDefinition[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface DiscoveryCache {
|
||||
entries: Record<string, CacheEntry>;
|
||||
}
|
||||
|
||||
const DEFAULT_CACHE_TTL_MS = 60 * 60 * 1000;
|
||||
|
||||
function cacheDir(): string {
|
||||
return process.env.ZERO_CONFIG_DIR || join(homedir(), '.config', 'zero');
|
||||
}
|
||||
|
||||
function cachePath(): string {
|
||||
return join(cacheDir(), 'model-cache.json');
|
||||
}
|
||||
|
||||
function ensureCacheDir() {
|
||||
const dir = cacheDir();
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function readCache(): DiscoveryCache {
|
||||
ensureCacheDir();
|
||||
|
||||
const path = cachePath();
|
||||
if (!existsSync(path)) {
|
||||
return { entries: {} };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
||||
return { entries: parsed.entries ?? {} };
|
||||
} catch {
|
||||
return { entries: {} };
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(cache: DiscoveryCache) {
|
||||
ensureCacheDir();
|
||||
const path = cachePath();
|
||||
const data = JSON.stringify(cache, null, 2);
|
||||
writeFileSync(path, data, 'utf-8');
|
||||
}
|
||||
|
||||
function parseDuration(value: string | number | undefined): number {
|
||||
if (typeof value === 'number') return value;
|
||||
if (!value) return DEFAULT_CACHE_TTL_MS;
|
||||
|
||||
const match = value.trim().match(/^(\d+)(m|h|d)$/);
|
||||
if (!match) {
|
||||
console.warn(`[providers] Unrecognized duration format: "${value}", using default 1h`);
|
||||
return DEFAULT_CACHE_TTL_MS;
|
||||
}
|
||||
|
||||
const amount = Number(match[1]);
|
||||
const unit = match[2];
|
||||
|
||||
if (unit === 'm') return amount * 60 * 1000;
|
||||
if (unit === 'h') return amount * 60 * 60 * 1000;
|
||||
return amount * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
function cacheKey(definition: ProviderDefinition, apiKey?: string): string {
|
||||
const baseURL = definition.baseURL.replace(/\/+$/, '').toLowerCase();
|
||||
const authPartition = definition.catalog?.discovery?.requiresAuth === false || !apiKey
|
||||
? 'public'
|
||||
: `key:${createHash('sha256').update(apiKey).digest('hex').slice(0, 16)}`;
|
||||
return `${definition.id}:${baseURL}:${authPartition}`;
|
||||
}
|
||||
|
||||
function staticModels(definition: ProviderDefinition): ResolvedModelDefinition[] {
|
||||
return getModelsForProvider(definition);
|
||||
}
|
||||
|
||||
function mergeModels(
|
||||
staticEntries: ResolvedModelDefinition[],
|
||||
discoveredEntries: ResolvedModelDefinition[]
|
||||
): ResolvedModelDefinition[] {
|
||||
const merged = [...staticEntries];
|
||||
const seen = new Set(staticEntries.map((model) => model.apiName.toLowerCase()));
|
||||
|
||||
for (const entry of discoveredEntries) {
|
||||
if (seen.has(entry.apiName.toLowerCase())) continue;
|
||||
seen.add(entry.apiName.toLowerCase());
|
||||
merged.push(entry);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
function discoveryUrl(definition: ProviderDefinition): string {
|
||||
const baseURL = definition.baseURL.replace(/\/+$/, '');
|
||||
const path = definition.catalog?.discovery?.path ?? '/models';
|
||||
return `${baseURL}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
function discoveryHeaders(definition: ProviderDefinition, apiKey?: string): Record<string, string> {
|
||||
const rawHeaders: Record<string, string | null> = {
|
||||
...(definition.transportConfig?.headers ?? {}),
|
||||
};
|
||||
|
||||
// Add auth header if needed
|
||||
if (apiKey && definition.catalog?.discovery?.requiresAuth !== false) {
|
||||
const authHeader = definition.transportConfig?.authHeader ?? {
|
||||
name: 'authorization',
|
||||
scheme: 'bearer' as const,
|
||||
};
|
||||
rawHeaders[authHeader.name] = authHeader.scheme === 'raw' ? apiKey : `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
// Resolve env vars and filter nulls
|
||||
const resolved = resolveHeaders(rawHeaders);
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [name, value] of Object.entries(resolved)) {
|
||||
if (value != null) {
|
||||
headers[name] = value;
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function discoverOpenAICompatibleModels(
|
||||
definition: ProviderDefinition,
|
||||
apiKey?: string
|
||||
): Promise<ResolvedModelDefinition[]> {
|
||||
const response = await fetch(discoveryUrl(definition), {
|
||||
headers: discoveryHeaders(definition, apiKey),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`model discovery failed (${response.status})`);
|
||||
}
|
||||
|
||||
const parsed: any = await response.json();
|
||||
const rows = Array.isArray(parsed?.data) ? parsed.data : Array.isArray(parsed) ? parsed : [];
|
||||
|
||||
return rows
|
||||
.map((row: any) => typeof row === 'string' ? row : row?.id)
|
||||
.filter((id: unknown): id is string => typeof id === 'string' && id.length > 0)
|
||||
.map((id) => resolveModel({ id, name: id }, definition));
|
||||
}
|
||||
|
||||
async function discoverOllamaModels(definition: ProviderDefinition): Promise<ResolvedModelDefinition[]> {
|
||||
const baseURL = definition.baseURL.replace(/\/v1\/?$/, '').replace(/\/+$/, '');
|
||||
const response = await fetch(`${baseURL}/api/tags`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`ollama discovery failed (${response.status})`);
|
||||
}
|
||||
|
||||
const parsed: any = await response.json();
|
||||
const rows = Array.isArray(parsed?.models) ? parsed.models : [];
|
||||
|
||||
return rows
|
||||
.map((row: any) => row?.name)
|
||||
.filter((id: unknown): id is string => typeof id === 'string' && id.length > 0)
|
||||
.map((id) => resolveModel({ id, name: id, tier: 'local' }, definition));
|
||||
}
|
||||
|
||||
async function runDiscovery(
|
||||
definition: ProviderDefinition,
|
||||
apiKey?: string
|
||||
): Promise<ResolvedModelDefinition[]> {
|
||||
const discovery = definition.catalog?.discovery;
|
||||
if (!discovery) return [];
|
||||
|
||||
if (discovery.kind === 'openai-compatible') {
|
||||
return discoverOpenAICompatibleModels(definition, apiKey);
|
||||
}
|
||||
|
||||
if (discovery.kind === 'ollama') {
|
||||
return discoverOllamaModels(definition);
|
||||
}
|
||||
|
||||
throw new Error(`custom discovery is not executable from JSON descriptors`);
|
||||
}
|
||||
|
||||
export async function discoverModelsForProvider(
|
||||
providerId: string,
|
||||
options: {
|
||||
apiKey?: string;
|
||||
forceRefresh?: boolean;
|
||||
allowNetwork?: boolean;
|
||||
} = {}
|
||||
): Promise<ModelDiscoveryResult | null> {
|
||||
const definition = getProviderDefinition(providerId);
|
||||
if (!definition) return null;
|
||||
|
||||
const staticEntries = staticModels(definition);
|
||||
const discovery = definition.catalog?.discovery;
|
||||
if (!discovery) {
|
||||
return {
|
||||
providerId,
|
||||
models: staticEntries,
|
||||
source: 'static',
|
||||
stale: false,
|
||||
};
|
||||
}
|
||||
|
||||
const ttlMs = parseDuration(definition.catalog?.discoveryCacheTtl);
|
||||
const key = cacheKey(definition, options.apiKey);
|
||||
const cache = readCache();
|
||||
const cached = cache.entries[key];
|
||||
const stale = cached ? Date.now() - cached.recordedAt > ttlMs : false;
|
||||
|
||||
if (!options.forceRefresh && cached && !stale) {
|
||||
return {
|
||||
providerId,
|
||||
models: mergeModels(staticEntries, cached.models),
|
||||
source: 'cache',
|
||||
stale: false,
|
||||
error: cached.error,
|
||||
};
|
||||
}
|
||||
|
||||
if (options.allowNetwork === false) {
|
||||
return {
|
||||
providerId,
|
||||
models: mergeModels(staticEntries, cached?.models ?? []),
|
||||
source: cached ? 'stale-cache' : 'static',
|
||||
stale: Boolean(cached && stale),
|
||||
error: cached?.error,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const discovered = await runDiscovery(definition, options.apiKey);
|
||||
cache.entries[key] = { recordedAt: Date.now(), models: discovered };
|
||||
writeCache(cache);
|
||||
|
||||
return {
|
||||
providerId,
|
||||
models: mergeModels(staticEntries, discovered),
|
||||
source: 'network',
|
||||
stale: false,
|
||||
};
|
||||
} catch (error: any) {
|
||||
const message = error?.message ?? String(error);
|
||||
if (cached) {
|
||||
cache.entries[key] = {
|
||||
...cached,
|
||||
error: message,
|
||||
};
|
||||
writeCache(cache);
|
||||
}
|
||||
|
||||
return {
|
||||
providerId,
|
||||
models: mergeModels(staticEntries, cached?.models ?? []),
|
||||
source: cached ? 'stale-cache' : 'error',
|
||||
stale: Boolean(cached),
|
||||
error: message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { OpenAIProvider } from './openai';
|
||||
import type { Provider } from './types';
|
||||
import type { ProviderConfig } from '../config/provider';
|
||||
import { getProviderDefinition, resolveEffectiveTransportConfig, getGlobalModelDefinition, getModelsForProvider } from './catalog';
|
||||
import type { ProviderDefinition } from './catalog/types';
|
||||
import { buildAuthHeaders, resolveHeaders } from './auth';
|
||||
|
||||
export function createProvider(config: ProviderConfig): Provider {
|
||||
const definition = config.providerId ? getProviderDefinition(config.providerId) : undefined;
|
||||
if (definition && !isProviderRuntimeSupported(definition)) {
|
||||
throw new Error(
|
||||
`Provider "${definition.name}" uses unsupported transport "${definition.transportConfig?.kind}". ` +
|
||||
'Supported: openai-compatible'
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve per-model transport overrides
|
||||
const globalModel = getGlobalModelDefinition(config.model);
|
||||
const catalogModel = definition && getModelsForProvider(definition).find(
|
||||
(m) => m.apiName === config.model || m.id === config.model
|
||||
);
|
||||
const modelDef = catalogModel ?? globalModel;
|
||||
|
||||
const effectiveTransport = definition
|
||||
? resolveEffectiveTransportConfig(definition, modelDef)
|
||||
: undefined;
|
||||
|
||||
const apiKey = config.apiKey || '';
|
||||
const rawHeaders = {
|
||||
...(effectiveTransport?.headers ?? {}),
|
||||
...buildAuthHeaders(apiKey, effectiveTransport?.authHeader),
|
||||
};
|
||||
const headers = resolveHeaders(rawHeaders);
|
||||
|
||||
return new OpenAIProvider({
|
||||
apiKey,
|
||||
baseURL: config.baseURL,
|
||||
model: config.model,
|
||||
defaultHeaders: Object.keys(headers).length > 0 ? headers : undefined,
|
||||
maxTokensField: effectiveTransport?.maxTokensField,
|
||||
removeBodyFields: effectiveTransport?.removeBodyFields,
|
||||
});
|
||||
}
|
||||
|
||||
export function isProviderRuntimeSupported(definition: ProviderDefinition): boolean {
|
||||
const kind = definition.transportConfig?.kind ?? 'openai-compatible';
|
||||
// TODO: implement anthropic-compatible transport
|
||||
return kind === 'openai-compatible';
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
import OpenAI from 'openai';
|
||||
import type { Provider, Message, ToolDefinition, StreamEvent } from './types';
|
||||
import type { MaxTokensField } from './catalog/types';
|
||||
|
||||
interface OpenAIProviderOptions {
|
||||
apiKey: string;
|
||||
baseURL?: string;
|
||||
model: string;
|
||||
defaultHeaders?: Record<string, string | null>;
|
||||
maxTokensField?: MaxTokensField;
|
||||
removeBodyFields?: string[];
|
||||
}
|
||||
|
||||
export class OpenAIProvider implements Provider {
|
||||
private client: OpenAI;
|
||||
private model: string;
|
||||
private maxTokensField: MaxTokensField;
|
||||
private removeBodyFields: Set<string>;
|
||||
|
||||
constructor({ apiKey, baseURL, model, defaultHeaders, maxTokensField, removeBodyFields }: OpenAIProviderOptions) {
|
||||
this.client = new OpenAI({
|
||||
apiKey,
|
||||
baseURL: baseURL || 'https://api.openai.com/v1',
|
||||
defaultHeaders,
|
||||
});
|
||||
this.model = model;
|
||||
this.maxTokensField = maxTokensField ?? 'max_tokens';
|
||||
this.removeBodyFields = new Set(removeBodyFields ?? []);
|
||||
}
|
||||
|
||||
async *streamCompletion(
|
||||
messages: Message[],
|
||||
tools: ToolDefinition[]
|
||||
): AsyncIterable<StreamEvent> {
|
||||
const openaiMessages = messages.map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
tool_calls: m.toolCalls?.map((tc) => ({
|
||||
id: tc.id,
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: tc.name,
|
||||
arguments: tc.arguments,
|
||||
},
|
||||
})),
|
||||
tool_call_id: m.toolCallId,
|
||||
}));
|
||||
|
||||
const openaiTools = tools.length > 0
|
||||
? tools.map((t) => ({
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
parameters: t.parameters,
|
||||
},
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
// Build request body
|
||||
const body: Record<string, unknown> = {
|
||||
model: this.model,
|
||||
messages: openaiMessages,
|
||||
tools: openaiTools,
|
||||
stream: true,
|
||||
};
|
||||
|
||||
// Strip configured body fields
|
||||
for (const field of this.removeBodyFields) {
|
||||
delete body[field];
|
||||
}
|
||||
|
||||
// Validate required fields before sending
|
||||
if (!body.model) {
|
||||
throw new Error('Provider request missing required field: model');
|
||||
}
|
||||
if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) {
|
||||
throw new Error('Provider request missing required field: messages');
|
||||
}
|
||||
|
||||
let stream;
|
||||
try {
|
||||
stream = await this.client.chat.completions.create(
|
||||
body as Parameters<typeof this.client.chat.completions.create>[0],
|
||||
{ timeout: 120_000 } // 2 minute timeout
|
||||
);
|
||||
} catch (err: any) {
|
||||
const message = getDetailedErrorMessage(err);
|
||||
if (message.includes('401') || message.toLowerCase().includes('invalid') || message.toLowerCase().includes('unauthorized')) {
|
||||
throw new Error(`Provider authentication error (check your API key): ${message}`);
|
||||
}
|
||||
if (message.toLowerCase().includes('rate') || message.toLowerCase().includes('quota')) {
|
||||
throw new Error(`Provider rate limit error: ${message}`);
|
||||
}
|
||||
throw new Error(`Provider returned error: ${message}`);
|
||||
}
|
||||
|
||||
const toolCallAccumulators = new Map<number, {
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: string;
|
||||
started: boolean;
|
||||
ended: boolean;
|
||||
}>();
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
// Some OpenAI-compatible servers send errors as special chunks
|
||||
if ((chunk as any).error) {
|
||||
const errData = (chunk as any).error;
|
||||
const msg = errData.message || JSON.stringify(errData);
|
||||
throw new Error(`Provider returned error: ${msg}`);
|
||||
}
|
||||
|
||||
const delta = chunk.choices[0]?.delta;
|
||||
const finishReason = chunk.choices[0]?.finish_reason;
|
||||
|
||||
if (delta?.content) {
|
||||
yield { type: 'text', content: delta.content };
|
||||
}
|
||||
|
||||
if (delta?.tool_calls) {
|
||||
for (const tc of delta.tool_calls) {
|
||||
if (tc.index === undefined) continue;
|
||||
|
||||
let acc = toolCallAccumulators.get(tc.index);
|
||||
if (!acc) {
|
||||
acc = { id: '', name: '', arguments: '', started: false, ended: false };
|
||||
toolCallAccumulators.set(tc.index, acc);
|
||||
}
|
||||
|
||||
// If we already had data at this index and now get a new id,
|
||||
// the previous tool call is complete.
|
||||
if (tc.id && acc.id && acc.id !== tc.id) {
|
||||
yield { type: 'tool-call-end', id: acc.id };
|
||||
acc = { id: '', name: '', arguments: '', started: false, ended: false };
|
||||
toolCallAccumulators.set(tc.index, acc);
|
||||
}
|
||||
|
||||
if (tc.id) acc.id = tc.id;
|
||||
if (tc.function?.name) acc.name = tc.function.name;
|
||||
if (tc.function?.arguments) {
|
||||
acc.arguments += tc.function.arguments;
|
||||
// Only yield deltas once we have the real id
|
||||
if (acc.id) {
|
||||
yield {
|
||||
type: 'tool-call-delta',
|
||||
id: acc.id,
|
||||
argumentsFragment: tc.function.arguments,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Emit start event the first time we have both id and name
|
||||
if (acc.id && acc.name && !acc.started) {
|
||||
yield { type: 'tool-call-start', id: acc.id, name: acc.name };
|
||||
acc.started = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: 'usage',
|
||||
promptTokens: chunk.usage.prompt_tokens,
|
||||
completionTokens: chunk.usage.completion_tokens,
|
||||
};
|
||||
}
|
||||
|
||||
// If the model signaled it's done with tool calls, close any open ones
|
||||
if (finishReason === 'tool_calls') {
|
||||
for (const [_, acc] of toolCallAccumulators) {
|
||||
if (acc.id && !acc.ended) {
|
||||
yield { type: 'tool-call-end', id: acc.id };
|
||||
acc.ended = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End of stream - close any remaining open tool calls
|
||||
for (const [_, acc] of toolCallAccumulators) {
|
||||
if (acc.id && !acc.ended) {
|
||||
yield { type: 'tool-call-end', id: acc.id };
|
||||
acc.ended = true;
|
||||
}
|
||||
}
|
||||
|
||||
yield { type: 'done' };
|
||||
} catch (err: any) {
|
||||
const message = getDetailedErrorMessage(err);
|
||||
throw new Error(`Provider returned error during streaming: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDetailedErrorMessage(err: any): string {
|
||||
if (!err) return 'Unknown error';
|
||||
|
||||
// Try common places where real error lives (especially for custom gateways)
|
||||
if (err.message && !err.message.includes('Provider returned error')) {
|
||||
return err.message;
|
||||
}
|
||||
|
||||
if (err.error) {
|
||||
if (typeof err.error === 'string') return err.error;
|
||||
if (err.error.message) return err.error.message;
|
||||
try { return JSON.stringify(err.error); } catch { return String(err.error); }
|
||||
}
|
||||
|
||||
if (err.response?.data) {
|
||||
const data = err.response.data;
|
||||
if (data.error?.message) return data.error.message;
|
||||
if (typeof data.error === 'string') return data.error;
|
||||
try { return JSON.stringify(data); } catch { return '[unserializable response data]'; }
|
||||
}
|
||||
|
||||
if (err.cause?.message) return err.cause.message;
|
||||
|
||||
return err.message || String(err);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
export interface Message {
|
||||
role: 'system' | 'user' | 'assistant' | 'tool';
|
||||
content: string | null;
|
||||
toolCalls?: ToolCall[];
|
||||
toolCallId?: string;
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: string; // JSON string
|
||||
}
|
||||
|
||||
export interface ToolDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>; // JSON Schema
|
||||
}
|
||||
|
||||
export type StreamEvent =
|
||||
| { type: 'text'; content: string }
|
||||
| { type: 'tool-call-start'; id: string; name: string }
|
||||
| { type: 'tool-call-delta'; id: string; argumentsFragment: string }
|
||||
| { type: 'tool-call-end'; id: string }
|
||||
| { type: 'usage'; promptTokens: number; completionTokens: number }
|
||||
| { type: 'done' };
|
||||
|
||||
export interface Provider {
|
||||
streamCompletion(
|
||||
messages: Message[],
|
||||
tools: ToolDefinition[]
|
||||
): AsyncIterable<StreamEvent>;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import { execa } from 'execa';
|
||||
import type { Tool } from './types';
|
||||
|
||||
const BashParams = z.object({
|
||||
command: z.string().min(1),
|
||||
cwd: z.string().optional(),
|
||||
});
|
||||
|
||||
export const bashTool: Tool = {
|
||||
name: 'bash',
|
||||
description: 'Execute a shell command and return the output. Use for running commands, git, tests, etc.',
|
||||
parameters: BashParams,
|
||||
async execute(args) {
|
||||
const { command, cwd } = BashParams.parse(args);
|
||||
|
||||
try {
|
||||
const result = await execa(command, {
|
||||
cwd: cwd || process.cwd(),
|
||||
shell: true,
|
||||
timeout: 120_000, // 2 minutes
|
||||
reject: false, // Don't throw on non-zero exit
|
||||
});
|
||||
|
||||
let output = '';
|
||||
if (result.exitCode !== 0) {
|
||||
output += `[Command failed with exit code ${result.exitCode}]\n`;
|
||||
}
|
||||
if (result.stdout) output += `stdout:\n${result.stdout}\n`;
|
||||
if (result.stderr) output += `stderr:\n${result.stderr}\n`;
|
||||
|
||||
return output.trim() || 'Command completed with no output.';
|
||||
} catch (err: any) {
|
||||
if (err.timedOut) {
|
||||
return 'Error: Command timed out after 2 minutes.';
|
||||
}
|
||||
return `Error executing command: ${err.message}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import type { Tool } from './types';
|
||||
|
||||
const EditFileParams = z.object({
|
||||
path: z.string().min(1),
|
||||
old_string: z.string().min(1),
|
||||
new_string: z.string(),
|
||||
});
|
||||
|
||||
export const editFileTool: Tool = {
|
||||
name: 'edit_file',
|
||||
description: 'Edit a file by replacing one exact string with another. Use this for precise code changes.',
|
||||
parameters: EditFileParams,
|
||||
async execute(args) {
|
||||
const { path, old_string, new_string } = EditFileParams.parse(args);
|
||||
|
||||
// Detect no-op edits
|
||||
if (old_string === new_string) {
|
||||
return `No-op edit: old_string and new_string are identical in ${path}.`;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await readFile(path, 'utf-8');
|
||||
|
||||
if (!content.includes(old_string)) {
|
||||
return `Error: Could not find the exact string to replace in ${path}.`;
|
||||
}
|
||||
|
||||
// Warn if multiple matches exist
|
||||
const matchCount = content.split(old_string).length - 1;
|
||||
if (matchCount > 1) {
|
||||
return `Error: Found ${matchCount} occurrences of the string in ${path}. Please provide a more specific string to match a single occurrence.`;
|
||||
}
|
||||
|
||||
// Use function replacement to avoid $&/$'/$` pattern expansion
|
||||
const newContent = content.replace(old_string, () => new_string);
|
||||
await writeFile(path, newContent, 'utf-8');
|
||||
|
||||
return `Successfully edited ${path}.`;
|
||||
} catch (err: any) {
|
||||
return `Error editing file ${path}: ${err.message}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import { execa } from 'execa';
|
||||
import type { Tool } from './types';
|
||||
|
||||
const GrepParams = z.object({
|
||||
pattern: z.string().min(1).describe('Regular expression pattern to search for'),
|
||||
path: z.string().optional().describe('Directory or file to search in. Defaults to current working directory.'),
|
||||
glob: z.string().optional().describe('Glob pattern to filter files (e.g. "**/*.ts", "*.tsx")'),
|
||||
output_mode: z.enum(['content', 'files_with_matches', 'count']).optional().default('content'),
|
||||
case_insensitive: z.boolean().optional().describe('Case insensitive search'),
|
||||
show_line_numbers: z.boolean().optional().default(true).describe('Show line numbers'),
|
||||
head_limit: z.number().int().min(1).optional().default(50).describe('Maximum number of results to return'),
|
||||
});
|
||||
|
||||
export const grepTool: Tool = {
|
||||
name: 'grep',
|
||||
description: `Fast content search using ripgrep (if available) or fallback.
|
||||
|
||||
This is the preferred tool for searching code. Use it instead of running grep via bash when possible.
|
||||
|
||||
Features:
|
||||
- Full regex support
|
||||
- Filter by glob
|
||||
- Different output modes (content, files_with_matches, count)
|
||||
- Automatically respects .gitignore`,
|
||||
parameters: GrepParams,
|
||||
async execute(args) {
|
||||
const params = GrepParams.parse(args);
|
||||
const { pattern, path = '.', glob, output_mode, case_insensitive, show_line_numbers, head_limit } = params;
|
||||
|
||||
const cwd = process.cwd();
|
||||
const targetPath = path === '.' ? cwd : path;
|
||||
|
||||
// Try ripgrep first (much faster and better)
|
||||
try {
|
||||
const rgArgs = ['--json', '--no-heading', '--with-filename'];
|
||||
|
||||
if (case_insensitive) rgArgs.push('-i');
|
||||
if (show_line_numbers) rgArgs.push('-n');
|
||||
if (glob) rgArgs.push('--glob', glob);
|
||||
|
||||
rgArgs.push('-m', String(head_limit));
|
||||
rgArgs.push(pattern, targetPath);
|
||||
|
||||
const { stdout } = await execa('rg', rgArgs, {
|
||||
cwd,
|
||||
reject: false,
|
||||
});
|
||||
|
||||
if (!stdout) {
|
||||
return output_mode === 'count' ? '0 matches found' : 'No matches found.';
|
||||
}
|
||||
|
||||
// Parse ripgrep JSON output
|
||||
const lines = stdout.trim().split('\n');
|
||||
const results: any[] = [];
|
||||
const parseErrors: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
if (parsed.type === 'match') {
|
||||
results.push(parsed);
|
||||
}
|
||||
} catch {
|
||||
parseErrors.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (parseErrors.length > 0 && results.length === 0) {
|
||||
return `Error: Failed to parse ripgrep output. Raw output:\n${stdout.slice(0, 500)}`;
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return 'No matches found.';
|
||||
}
|
||||
|
||||
if (output_mode === 'count') {
|
||||
return `${results.length} matches found (limited to first ${head_limit})`;
|
||||
}
|
||||
|
||||
if (output_mode === 'files_with_matches') {
|
||||
const files = new Set(results.map(r => r.data.path.text));
|
||||
return Array.from(files).slice(0, head_limit).join('\n');
|
||||
}
|
||||
|
||||
// content mode
|
||||
const formatted = results.slice(0, head_limit).map((r: any) => {
|
||||
const file = r.data.path.text;
|
||||
const lineNum = r.data.line_number;
|
||||
const text = r.data.lines.text.trim();
|
||||
return `${file}:${lineNum}: ${text}`;
|
||||
});
|
||||
|
||||
return formatted.join('\n');
|
||||
} catch (err) {
|
||||
// Fallback: use basic grep via bash
|
||||
try {
|
||||
const grepArgs = ['-r'];
|
||||
if (show_line_numbers) grepArgs.push('-n');
|
||||
if (case_insensitive) grepArgs.push('-i');
|
||||
grepArgs.push('-m', String(head_limit));
|
||||
|
||||
let searchTarget = targetPath;
|
||||
if (glob) {
|
||||
// Use find to filter by glob pattern
|
||||
grepArgs.push('--include', glob);
|
||||
}
|
||||
|
||||
grepArgs.push(pattern, searchTarget);
|
||||
|
||||
const { stdout } = await execa('grep', grepArgs, {
|
||||
cwd,
|
||||
reject: false,
|
||||
});
|
||||
|
||||
if (!stdout) {
|
||||
return output_mode === 'count' ? '0 matches found' : 'No matches found.';
|
||||
}
|
||||
|
||||
const lines = stdout.trim().split('\n');
|
||||
|
||||
if (output_mode === 'count') {
|
||||
return `${lines.length} matches found (limited to first ${head_limit})`;
|
||||
}
|
||||
|
||||
if (output_mode === 'files_with_matches') {
|
||||
const files = new Set(lines.map(l => l.split(':')[0]));
|
||||
return Array.from(files).slice(0, head_limit).join('\n');
|
||||
}
|
||||
|
||||
return lines.slice(0, head_limit).join('\n');
|
||||
} catch {
|
||||
return `Search failed: ripgrep (rg) not available and grep fallback failed. Please install ripgrep for best results.`;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { toolRegistry } from './registry';
|
||||
import { readFileTool } from './read-file';
|
||||
import { bashTool } from './bash';
|
||||
import { editFileTool } from './edit-file';
|
||||
import { planTool } from './plan';
|
||||
import { listDirectoryTool } from './list-directory';
|
||||
import { grepTool } from './grep';
|
||||
|
||||
toolRegistry.register(readFileTool);
|
||||
toolRegistry.register(bashTool);
|
||||
toolRegistry.register(editFileTool);
|
||||
toolRegistry.register(planTool);
|
||||
toolRegistry.register(listDirectoryTool);
|
||||
toolRegistry.register(grepTool);
|
||||
|
||||
export { toolRegistry };
|
||||
export * from './types';
|
||||
export { getCurrentPlan, clearPlan } from './plan';
|
||||
export type { PlanItem } from './plan';
|
||||
@@ -1,108 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import { readdir, stat } from 'fs/promises';
|
||||
import { join, relative } from 'path';
|
||||
import type { Tool } from './types';
|
||||
|
||||
const ListDirectoryParams = z.object({
|
||||
path: z.string().optional().describe('Directory to list. Defaults to current working directory.'),
|
||||
recursive: z.boolean().optional().default(false).describe('Whether to list recursively'),
|
||||
max_depth: z.number().int().min(1).max(5).optional().default(2).describe('Max recursion depth when recursive=true'),
|
||||
});
|
||||
|
||||
export const listDirectoryTool: Tool = {
|
||||
name: 'list_directory',
|
||||
description: `List files and directories in a given path. Use this when you need to explore the project structure.
|
||||
|
||||
This is the preferred tool for understanding the codebase layout before planning or making changes.
|
||||
- Returns a clean tree-like structure
|
||||
- Supports recursive listing with depth control
|
||||
- Ignores common junk directories (.git, node_modules, dist, etc.) by default`,
|
||||
parameters: ListDirectoryParams,
|
||||
async execute(args) {
|
||||
const { path = '.', recursive, max_depth } = ListDirectoryParams.parse(args);
|
||||
|
||||
const ignoreDirs = new Set([
|
||||
'.git', 'node_modules', 'dist', 'build', '.next', '.turbo', 'coverage',
|
||||
'.cache', 'tmp', 'temp', '.DS_Store',
|
||||
]);
|
||||
|
||||
try {
|
||||
const root = process.cwd();
|
||||
const targetPath = join(root, path);
|
||||
|
||||
const entries = await listDirRecursive(targetPath, root, 0, recursive ? max_depth : 0, ignoreDirs);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return `Directory is empty: ${path}`;
|
||||
}
|
||||
|
||||
return `Contents of ${path}:\n\n${entries.join('\n')}`;
|
||||
} catch (err: any) {
|
||||
return `Error listing directory ${path}: ${err.message}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
async function listDirRecursive(
|
||||
currentPath: string,
|
||||
root: string,
|
||||
currentDepth: number,
|
||||
maxDepth: number,
|
||||
ignoreDirs: Set<string>,
|
||||
): Promise<string[]> {
|
||||
const results: string[] = [];
|
||||
|
||||
try {
|
||||
const dirents = await readdir(currentPath, { withFileTypes: true });
|
||||
|
||||
for (const dirent of dirents) {
|
||||
const fullPath = join(currentPath, dirent.name);
|
||||
const relPath = relative(root, fullPath);
|
||||
|
||||
if (dirent.isDirectory() || dirent.isSymbolicLink()) {
|
||||
if (ignoreDirs.has(dirent.name)) continue;
|
||||
|
||||
// For symlinks, verify it's actually a directory
|
||||
if (dirent.isSymbolicLink()) {
|
||||
try {
|
||||
const linkStat = await stat(fullPath);
|
||||
if (!linkStat.isDirectory()) {
|
||||
// Symlink points to a file, treat as file
|
||||
const indent = ' '.repeat(currentDepth);
|
||||
results.push(`${indent}📄 ${dirent.name} -> symlink`);
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
// Broken symlink
|
||||
const indent = ' '.repeat(currentDepth);
|
||||
results.push(`${indent}❓ ${dirent.name} -> broken symlink`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const indent = ' '.repeat(currentDepth);
|
||||
results.push(`${indent}📁 ${dirent.name}/`);
|
||||
|
||||
if (currentDepth < maxDepth) {
|
||||
const children = await listDirRecursive(fullPath, root, currentDepth + 1, maxDepth, ignoreDirs);
|
||||
results.push(...children);
|
||||
}
|
||||
} else {
|
||||
// Skip ignored files
|
||||
if (ignoreDirs.has(dirent.name)) continue;
|
||||
|
||||
const indent = ' '.repeat(currentDepth);
|
||||
results.push(`${indent}📄 ${dirent.name}`);
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
// Only ignore permission errors; log others
|
||||
if (e.code === 'EACCES' || e.code === 'EPERM') {
|
||||
// Permission denied - skip silently
|
||||
} else {
|
||||
console.warn(`[list-directory] Error reading ${currentPath}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import type { Tool } from './types';
|
||||
|
||||
const PlanItemSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
content: z.string().min(1),
|
||||
status: z.enum(['pending', 'in_progress', 'completed', 'failed']),
|
||||
notes: z.string().optional(),
|
||||
});
|
||||
|
||||
const UpdatePlanParams = z.object({
|
||||
plan: z.array(PlanItemSchema),
|
||||
});
|
||||
|
||||
type PlanItem = z.infer<typeof PlanItemSchema>;
|
||||
|
||||
// In-memory plan storage for the current session
|
||||
// NOTE: This is module-level state. If multiple agent loops run in parallel,
|
||||
// they will share and overwrite each other's plans. This is acceptable for
|
||||
// single-agent usage but should be refactored for parallel agents.
|
||||
let currentPlan: PlanItem[] = [];
|
||||
|
||||
function formatPlan(plan: PlanItem[]): string {
|
||||
if (plan.length === 0) {
|
||||
return 'Plan is currently empty.';
|
||||
}
|
||||
|
||||
const statusEmoji: Record<PlanItem['status'], string> = {
|
||||
pending: '○',
|
||||
in_progress: '◉',
|
||||
completed: '✓',
|
||||
failed: '✕',
|
||||
};
|
||||
|
||||
const lines = plan.map((item, index) => {
|
||||
const emoji = statusEmoji[item.status];
|
||||
const notes = item.notes ? `\n Notes: ${item.notes}` : '';
|
||||
return `${index + 1}. ${emoji} [${item.status}] ${item.content}${notes}`;
|
||||
});
|
||||
|
||||
return 'Current Plan:\n' + lines.join('\n');
|
||||
}
|
||||
|
||||
export const planTool: Tool = {
|
||||
name: 'update_plan',
|
||||
description: `Create or update the plan for the current task.
|
||||
|
||||
Use this tool proactively for any non-trivial task. This is your primary way to organize work, track progress, and communicate your plan to the user.
|
||||
|
||||
When to use:
|
||||
- At the start of a complex task, create a high-level plan first.
|
||||
- Before making significant changes, break the work into clear steps.
|
||||
- After completing a step, update its status.
|
||||
- If the scope changes, revise the plan.
|
||||
|
||||
The plan should be clear, actionable, and ordered. Keep it up to date as you work.`,
|
||||
parameters: UpdatePlanParams,
|
||||
async execute(args) {
|
||||
const { plan } = UpdatePlanParams.parse(args);
|
||||
|
||||
currentPlan = plan;
|
||||
|
||||
return formatPlan(currentPlan);
|
||||
},
|
||||
};
|
||||
|
||||
// Helper to get the current plan (can be used by other parts of the system later)
|
||||
export function getCurrentPlan(): PlanItem[] {
|
||||
return [...currentPlan];
|
||||
}
|
||||
|
||||
// Helper to clear the plan (useful for new conversations)
|
||||
export function clearPlan() {
|
||||
currentPlan = [];
|
||||
}
|
||||
|
||||
// Export the schema type for potential future use in the TUI
|
||||
export type { PlanItem };
|
||||
@@ -1,41 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import { readFile, stat } from 'fs/promises';
|
||||
import type { Tool } from './types';
|
||||
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
const ReadFileParams = z.object({
|
||||
path: z.string().min(1),
|
||||
});
|
||||
|
||||
export const readFileTool: Tool = {
|
||||
name: 'read_file',
|
||||
description: 'Read the contents of a file from the filesystem.',
|
||||
parameters: ReadFileParams,
|
||||
async execute(args) {
|
||||
const { path } = ReadFileParams.parse(args);
|
||||
|
||||
try {
|
||||
const fileStat = await stat(path);
|
||||
if (fileStat.size > MAX_FILE_SIZE) {
|
||||
return `Error: File ${path} is too large (${(fileStat.size / 1024 / 1024).toFixed(1)}MB). Maximum size is 10MB.`;
|
||||
}
|
||||
|
||||
// Check for binary content by reading first 8KB
|
||||
const buffer = Buffer.alloc(Math.min(8192, fileStat.size));
|
||||
const file = await import('fs/promises').then(fs => fs.open(path, 'r'));
|
||||
await file.read(buffer, 0, buffer.length, 0);
|
||||
await file.close();
|
||||
|
||||
// If file contains null bytes, it's likely binary
|
||||
if (buffer.includes(0)) {
|
||||
return `Error: File ${path} appears to be binary and cannot be read as text.`;
|
||||
}
|
||||
|
||||
const content = await readFile(path, 'utf-8');
|
||||
return `File: ${path}\n\n${content}`;
|
||||
} catch (err: any) {
|
||||
return `Error reading file ${path}: ${err.message}`;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { Tool } from './types';
|
||||
|
||||
export class ToolRegistry {
|
||||
private tools = new Map<string, Tool>();
|
||||
|
||||
register(tool: Tool) {
|
||||
this.tools.set(tool.name, tool);
|
||||
}
|
||||
|
||||
get(name: string): Tool | undefined {
|
||||
return this.tools.get(name);
|
||||
}
|
||||
|
||||
getAll(): Tool[] {
|
||||
return Array.from(this.tools.values());
|
||||
}
|
||||
}
|
||||
|
||||
export const toolRegistry = new ToolRegistry();
|
||||
@@ -1,16 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToolCall } from '../providers/types';
|
||||
|
||||
export type { ToolCall };
|
||||
|
||||
export interface Tool {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: z.ZodObject<any>; // Zod schema for validation
|
||||
execute: (args: any) => Promise<string>; // Returns tool result as string
|
||||
}
|
||||
|
||||
export interface ToolResult {
|
||||
toolCallId: string;
|
||||
result: string;
|
||||
}
|
||||
@@ -1,403 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import TextInput from 'ink-text-input';
|
||||
import { configManager } from '../config/manager';
|
||||
import { listProviderDefinitions } from '../providers/catalog';
|
||||
import { discoverModelsForProvider } from '../providers/discovery';
|
||||
import { isProviderRuntimeSupported } from '../providers/factory';
|
||||
import type { ProviderDefinition, ResolvedModelDefinition } from '../providers/catalog/types';
|
||||
|
||||
type AddMode = 'choose' | 'catalog' | 'generic';
|
||||
|
||||
interface AddProviderProps {
|
||||
onDone: (providerName?: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const catalogProviders = listProviderDefinitions().filter(isProviderRuntimeSupported);
|
||||
|
||||
export const AddProvider: React.FC<AddProviderProps> = ({ onDone, onCancel }) => {
|
||||
const [mode, setMode] = useState<AddMode>('choose');
|
||||
const [selectedOption, setSelectedOption] = useState(0);
|
||||
const [selectedDefinition, setSelectedDefinition] = useState<ProviderDefinition | null>(null);
|
||||
const [catalogStep, setCatalogStep] = useState(0);
|
||||
|
||||
const [catalogKey, setCatalogKey] = useState('');
|
||||
const [catalogModel, setCatalogModel] = useState('');
|
||||
const catalogModelTouched = React.useRef(false);
|
||||
const [modelOptions, setModelOptions] = useState<ResolvedModelDefinition[]>([]);
|
||||
const [selectedModelIndex, setSelectedModelIndex] = useState(0);
|
||||
const [discoveryStatus, setDiscoveryStatus] = useState('');
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [baseURL, setBaseURL] = useState('https://api.openai.com/v1');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [model, setModel] = useState('gpt-4o');
|
||||
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const totalOptions = catalogProviders.length + 1;
|
||||
const visibleModelStart = Math.max(
|
||||
0,
|
||||
Math.min(selectedModelIndex - 3, Math.max(0, modelOptions.length - 8))
|
||||
);
|
||||
const visibleModelOptions = modelOptions.slice(visibleModelStart, visibleModelStart + 8);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mode !== 'catalog' || catalogStep !== 1 || !selectedDefinition) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const loadModels = async () => {
|
||||
const staticModels = selectedDefinition.catalog?.models ?? selectedDefinition.models ?? [];
|
||||
setModelOptions(staticModels.map((model) => ({
|
||||
...model,
|
||||
apiName: model.apiName ?? model.id,
|
||||
providerId: selectedDefinition.id,
|
||||
providerName: selectedDefinition.name,
|
||||
providerKind: selectedDefinition.kind,
|
||||
})));
|
||||
|
||||
if (!selectedDefinition.catalog?.discovery) {
|
||||
setDiscoveryStatus(staticModels.length > 0 ? 'Using static catalog models.' : '');
|
||||
return;
|
||||
}
|
||||
|
||||
setDiscoveryStatus('Discovering models...');
|
||||
const result = await discoverModelsForProvider(selectedDefinition.id, {
|
||||
apiKey: catalogKey.trim() || undefined,
|
||||
});
|
||||
|
||||
if (cancelled || !result) return;
|
||||
|
||||
setModelOptions(result.models);
|
||||
setDiscoveryStatus(
|
||||
result.error
|
||||
? `Discovery ${result.source}: ${result.error}`
|
||||
: `Models loaded from ${result.source}.`
|
||||
);
|
||||
|
||||
const firstModel = result.models[0];
|
||||
if (firstModel && !catalogModelTouched.current) {
|
||||
setCatalogModel(firstModel.apiName);
|
||||
setSelectedModelIndex(0);
|
||||
}
|
||||
};
|
||||
|
||||
loadModels();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [mode, catalogStep, selectedDefinition, catalogKey]);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.escape) {
|
||||
if (mode === 'choose') {
|
||||
onCancel();
|
||||
} else {
|
||||
setMode('choose');
|
||||
setCatalogStep(0);
|
||||
setSelectedDefinition(null);
|
||||
setError('');
|
||||
setSuccess(false);
|
||||
setSelectedOption(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'catalog' && catalogStep === 1 && modelOptions.length > 0) {
|
||||
if (key.upArrow) {
|
||||
setSelectedModelIndex((prev) => {
|
||||
const next = Math.max(0, prev - 1);
|
||||
catalogModelTouched.current = true;
|
||||
setCatalogModel(modelOptions[next]?.apiName ?? catalogModel);
|
||||
return next;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.downArrow) {
|
||||
setSelectedModelIndex((prev) => {
|
||||
const next = Math.min(modelOptions.length - 1, prev + 1);
|
||||
catalogModelTouched.current = true;
|
||||
setCatalogModel(modelOptions[next]?.apiName ?? catalogModel);
|
||||
return next;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mode !== 'choose') return;
|
||||
|
||||
if (key.upArrow) {
|
||||
setSelectedOption((prev) => Math.max(0, prev - 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.downArrow) {
|
||||
setSelectedOption((prev) => Math.min(totalOptions - 1, prev + 1));
|
||||
return;
|
||||
}
|
||||
|
||||
const chooseOption = (index: number) => {
|
||||
const definition = catalogProviders[index];
|
||||
if (definition) {
|
||||
setSelectedDefinition(definition);
|
||||
catalogModelTouched.current = false;
|
||||
setCatalogModel(definition.defaultModel);
|
||||
setModelOptions([]);
|
||||
setSelectedModelIndex(0);
|
||||
setDiscoveryStatus('');
|
||||
setMode('catalog');
|
||||
setCatalogStep(definition.apiKeyRequired === false ? 1 : 0);
|
||||
} else {
|
||||
setMode('generic');
|
||||
}
|
||||
};
|
||||
|
||||
if (key.return) {
|
||||
chooseOption(selectedOption);
|
||||
return;
|
||||
}
|
||||
|
||||
const quickNumber = parseInt(input, 10);
|
||||
if (!isNaN(quickNumber) && quickNumber >= 1 && quickNumber <= totalOptions) {
|
||||
chooseOption(quickNumber - 1);
|
||||
}
|
||||
});
|
||||
|
||||
const saveCatalogProvider = () => {
|
||||
if (!selectedDefinition) return;
|
||||
|
||||
if (selectedDefinition.apiKeyRequired !== false && !catalogKey.trim()) {
|
||||
setError('API key is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const profileName = selectedDefinition.id;
|
||||
|
||||
configManager.addProvider({
|
||||
name: profileName,
|
||||
providerId: selectedDefinition.id,
|
||||
kind: selectedDefinition.kind,
|
||||
baseURL: selectedDefinition.baseURL,
|
||||
apiKey: catalogKey.trim() || undefined,
|
||||
model: catalogModel.trim() || selectedDefinition.defaultModel,
|
||||
description: selectedDefinition.name,
|
||||
});
|
||||
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
onDone(profileName);
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const saveGeneric = () => {
|
||||
if (!name.trim() || !baseURL.trim() || !model.trim()) {
|
||||
setError('Name, Base URL, and Model are required');
|
||||
return;
|
||||
}
|
||||
|
||||
configManager.addProvider({
|
||||
name: name.trim(),
|
||||
kind: 'custom',
|
||||
baseURL: baseURL.trim(),
|
||||
apiKey: apiKey.trim() || undefined,
|
||||
model: model.trim(),
|
||||
description: 'Custom OpenAI-compatible',
|
||||
});
|
||||
|
||||
setSuccess(true);
|
||||
setTimeout(() => onDone(name.trim()), 1200);
|
||||
};
|
||||
|
||||
if (mode === 'choose') {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text bold color="cyan">Add New Provider</Text>
|
||||
<Text color="gray">Esc to go back - Up/Down to navigate - Enter to select</Text>
|
||||
|
||||
<Box marginY={1} flexDirection="column">
|
||||
{catalogProviders.map((definition, index) => (
|
||||
<Box key={definition.id} flexDirection="column" marginBottom={1}>
|
||||
<Text color={selectedOption === index ? 'greenBright' : 'white'}>
|
||||
{selectedOption === index ? '> ' : ' '}
|
||||
{index + 1}. Add {definition.name}
|
||||
</Text>
|
||||
{selectedOption === index && (
|
||||
<Text color="gray" dimColor>
|
||||
{' '}{definition.kind}: {definition.description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Text color={selectedOption === catalogProviders.length ? 'greenBright' : 'white'}>
|
||||
{selectedOption === catalogProviders.length ? '> ' : ' '}
|
||||
{catalogProviders.length + 1}. Add custom OpenAI-compatible provider
|
||||
</Text>
|
||||
{selectedOption === catalogProviders.length && (
|
||||
<Text color="gray" dimColor>
|
||||
{' '}For providers not yet described by a catalog file
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === 'catalog' && selectedDefinition) {
|
||||
if (success) {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text color="greenBright" bold>
|
||||
{selectedDefinition.name} provider added successfully!
|
||||
</Text>
|
||||
<Text color="gray" dimColor>
|
||||
It is now your active provider.
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text bold color="cyan">Add {selectedDefinition.name}</Text>
|
||||
<Text color="gray">Esc to go back</Text>
|
||||
|
||||
{catalogStep === 0 && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color="yellowBright">Step 1/2 - Enter API key</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text>{selectedDefinition.apiKeyLabel ?? 'API key'}: </Text>
|
||||
<TextInput
|
||||
value={catalogKey}
|
||||
onChange={setCatalogKey}
|
||||
mask="*"
|
||||
placeholder={selectedDefinition.apiKeyPlaceholder}
|
||||
/>
|
||||
</Box>
|
||||
{error && <Text color="red">{error}</Text>}
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>Press Enter to continue</Text>
|
||||
</Box>
|
||||
<TextInput
|
||||
value=""
|
||||
onChange={() => {}}
|
||||
onSubmit={() => {
|
||||
if (selectedDefinition.apiKeyRequired === false || catalogKey.trim()) {
|
||||
setCatalogStep(1);
|
||||
setError('');
|
||||
} else {
|
||||
setError('API key cannot be empty');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{catalogStep === 1 && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color="yellowBright">Step 2/2 - Model name</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text>Model: </Text>
|
||||
<TextInput
|
||||
value={catalogModel}
|
||||
onChange={(value) => {
|
||||
catalogModelTouched.current = true;
|
||||
setCatalogModel(value);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{selectedDefinition.models && selectedDefinition.models.length > 0 && (
|
||||
<Text color="gray" dimColor>
|
||||
Default catalog model: {selectedDefinition.defaultModel}
|
||||
</Text>
|
||||
)}
|
||||
{discoveryStatus && (
|
||||
<Text color="gray" dimColor>{discoveryStatus}</Text>
|
||||
)}
|
||||
{modelOptions.length > 0 && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color="gray" dimColor>Up/Down to choose a catalog model</Text>
|
||||
{visibleModelOptions.map((option, index) => {
|
||||
const modelIndex = visibleModelStart + index;
|
||||
return (
|
||||
<Text
|
||||
key={`${option.providerId ?? 'global'}:${option.apiName}`}
|
||||
color={modelIndex === selectedModelIndex ? 'greenBright' : 'white'}
|
||||
>
|
||||
{modelIndex === selectedModelIndex ? '> ' : ' '}
|
||||
{option.name ?? option.label ?? option.apiName}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
{error && <Text color="red">{error}</Text>}
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>Press Enter to save</Text>
|
||||
</Box>
|
||||
<TextInput
|
||||
value=""
|
||||
onChange={() => {}}
|
||||
onSubmit={saveCatalogProvider}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === 'generic') {
|
||||
if (success) {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text color="greenBright" bold>
|
||||
Provider added successfully!
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text bold color="cyan">Add Custom Provider</Text>
|
||||
<Text color="gray">Esc to go back</Text>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text>Name: </Text>
|
||||
<TextInput value={name} onChange={setName} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Base URL: </Text>
|
||||
<TextInput value={baseURL} onChange={setBaseURL} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>API Key: </Text>
|
||||
<TextInput value={apiKey} onChange={setApiKey} mask="*" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Model: </Text>
|
||||
<TextInput value={model} onChange={setModel} />
|
||||
</Box>
|
||||
|
||||
{error && <Text color="red">{error}</Text>}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray">Press Enter to save</Text>
|
||||
</Box>
|
||||
|
||||
<TextInput
|
||||
value=""
|
||||
onChange={() => {}}
|
||||
onSubmit={saveGeneric}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
-637
@@ -1,637 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, useApp, useInput } from 'ink';
|
||||
import { ProviderPicker } from './ProviderPicker';
|
||||
import { AddProvider } from './AddProvider';
|
||||
import { Logo } from './Logo';
|
||||
import { ThinkingSpinner } from './Spinner';
|
||||
import { MessageRenderer } from './MessageRenderer';
|
||||
import { ToolCallRenderer } from './ToolCallRenderer';
|
||||
import { configManager } from '../config/manager';
|
||||
import { loadProviderConfig } from '../config/provider';
|
||||
import { createProvider } from '../providers/factory';
|
||||
import { runAgent } from '../agent/loop';
|
||||
|
||||
type Screen = 'chat' | 'provider-picker' | 'add-provider';
|
||||
|
||||
// Map low-level errors back to actionable guidance for the user. The full
|
||||
// error object is still surfaced separately when debug mode is on.
|
||||
function toFriendlyError(err: any): string {
|
||||
const raw = err?.message || String(err);
|
||||
const lower = raw.toLowerCase();
|
||||
|
||||
if (lower.includes('no llm provider configured') || lower.includes('no provider')) {
|
||||
return 'No provider set up. Type /provider to add one.';
|
||||
}
|
||||
|
||||
if (lower.includes('auth') || lower.includes('unauthorized') || lower.includes('invalid') || lower.includes('401') || lower.includes('api key')) {
|
||||
return `Authentication failed — check your API key. Type /provider to update it.\n(${raw})`;
|
||||
}
|
||||
|
||||
if (lower.includes('rate') || lower.includes('quota')) {
|
||||
return `Provider rate limit or quota reached. Try again shortly.\n(${raw})`;
|
||||
}
|
||||
|
||||
if (lower.includes('enotfound') || lower.includes('econnrefused') || lower.includes('etimedout') || lower.includes('fetch failed') || lower.includes('network')) {
|
||||
return `Network error reaching the provider. Check your connection and base URL.\n(${raw})`;
|
||||
}
|
||||
|
||||
return `Error: ${raw}`;
|
||||
}
|
||||
|
||||
type ChatMessage =
|
||||
| { type: 'user'; content: string }
|
||||
| { type: 'assistant'; content: string }
|
||||
| { type: 'tool-call'; name: string; args: string; result?: string }
|
||||
| { type: 'tool-result'; content: string } // legacy - results now attach to tool-call
|
||||
| { type: 'system'; content: string };
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const { exit } = useApp();
|
||||
const [screen, setScreen] = useState<Screen>('chat');
|
||||
const [input, setInput] = useState('');
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{ type: 'system', content: 'Welcome to zero. Type /provider to manage providers.' },
|
||||
{ type: 'system', content: 'Type /help for available commands.' },
|
||||
]);
|
||||
|
||||
// Check on startup if we have any usable provider
|
||||
React.useEffect(() => {
|
||||
const checkProvider = async () => {
|
||||
try {
|
||||
await loadProviderConfig();
|
||||
} catch (err: any) {
|
||||
if (err.message?.includes('No LLM provider configured')) {
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
type: 'system',
|
||||
content: '⚠️ No provider configured yet. Use /provider to add one (OpenGateway recommended).'
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
};
|
||||
checkProvider();
|
||||
}, []);
|
||||
const [isThinking, setIsThinking] = useState(false);
|
||||
const [streamingMessageIndex, setStreamingMessageIndex] = useState<number | null>(null);
|
||||
|
||||
// Plan Mode (inspired by OpenClaude / Claude Code)
|
||||
const [isPlanMode, setIsPlanMode] = useState(false);
|
||||
|
||||
// Debug mode - when enabled, prints full error objects to console
|
||||
const [debugMode, setDebugMode] = useState(false);
|
||||
const [lastError, setLastError] = useState<any>(null);
|
||||
|
||||
// Tools enabled (useful for debugging provider errors)
|
||||
const [toolsEnabled, setToolsEnabled] = useState(true);
|
||||
|
||||
// Command suggestions
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
|
||||
const knownCommands = ['/provider', '/plan', '/debug-mode', '/debug', '/tools', '/help', '/exit', '/quit'];
|
||||
|
||||
// Update suggestions when input changes
|
||||
React.useEffect(() => {
|
||||
if (input.startsWith('/')) {
|
||||
const query = input.toLowerCase();
|
||||
const matches = knownCommands.filter(cmd => cmd.startsWith(query));
|
||||
setSuggestions(matches.slice(0, 6)); // limit suggestions
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
}
|
||||
}, [input]);
|
||||
|
||||
// Scrolling state (Grok Build style internal scrolling)
|
||||
const [scrollOffset, setScrollOffset] = useState(0);
|
||||
const [terminalRows, setTerminalRows] = useState(24); // default fallback
|
||||
|
||||
// Current provider info for the input bar (Grok Build style)
|
||||
const activeProfile = configManager.getActiveProvider();
|
||||
const currentProviderName = activeProfile?.name || (process.env.ZERO_PROVIDER_COMMAND ? 'command' : 'env');
|
||||
const currentModel = activeProfile?.model || process.env.OPENAI_MODEL || 'default';
|
||||
|
||||
// Track terminal size for proper scrolling
|
||||
React.useEffect(() => {
|
||||
const updateSize = () => {
|
||||
setTerminalRows(process.stdout.rows || 24);
|
||||
};
|
||||
process.stdout.on('resize', updateSize);
|
||||
updateSize();
|
||||
return () => {
|
||||
process.stdout.off('resize', updateSize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Auto-scroll to bottom when new messages arrive (unless user scrolled up)
|
||||
React.useEffect(() => {
|
||||
// Only auto-scroll if user is near the bottom
|
||||
if (scrollOffset <= 3) {
|
||||
setScrollOffset(0);
|
||||
}
|
||||
}, [messages.length]);
|
||||
|
||||
// Only capture main chat input when we're actually in the chat screen
|
||||
const isInChat = screen === 'chat';
|
||||
|
||||
useInput((inputChar, key) => {
|
||||
if (key.ctrl && inputChar === 'c') {
|
||||
exit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't process chat input while in provider picker or add flow
|
||||
if (!isInChat) return;
|
||||
|
||||
// Scrolling controls (when input is empty)
|
||||
if (!input) {
|
||||
if (key.upArrow) {
|
||||
setScrollOffset((prev) => Math.min(prev + 1, messages.length - 1));
|
||||
return;
|
||||
}
|
||||
if (key.downArrow) {
|
||||
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
||||
return;
|
||||
}
|
||||
if (key.pageUp) {
|
||||
setScrollOffset((prev) => Math.min(prev + 8, messages.length - 1));
|
||||
return;
|
||||
}
|
||||
if (key.pageDown) {
|
||||
setScrollOffset((prev) => Math.max(prev - 8, 0));
|
||||
return;
|
||||
}
|
||||
if (key.home) {
|
||||
setScrollOffset(messages.length - 1);
|
||||
return;
|
||||
}
|
||||
if (key.end) {
|
||||
setScrollOffset(0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (key.return) {
|
||||
handleSubmit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Autocomplete first suggestion with Tab when typing a command
|
||||
if (key.tab && suggestions.length > 0) {
|
||||
setInput(suggestions[0] + ' ');
|
||||
setSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.backspace || key.delete) {
|
||||
setInput((prev) => prev.slice(0, -1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputChar && !key.ctrl && !key.meta) {
|
||||
setInput((prev) => prev + inputChar);
|
||||
}
|
||||
}, { isActive: isInChat });
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!input.trim()) return;
|
||||
|
||||
const trimmed = input.trim();
|
||||
setInput('');
|
||||
setSuggestions([]);
|
||||
|
||||
// Handle slash commands
|
||||
if (trimmed.startsWith('/')) {
|
||||
setMessages((prev) => [...prev, { type: 'user', content: trimmed }]);
|
||||
handleSlashCommand(trimmed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular message → send to agent
|
||||
setMessages((prev) => [...prev, { type: 'user', content: trimmed }]);
|
||||
|
||||
const runAgentLoop = async () => {
|
||||
setIsThinking(true);
|
||||
|
||||
try {
|
||||
const providerConfig = await loadProviderConfig();
|
||||
const provider = createProvider(providerConfig);
|
||||
|
||||
// Add empty assistant message that we'll stream into
|
||||
setMessages((prev) => {
|
||||
const newMessages = [...prev, { type: 'assistant' as const, content: '' }];
|
||||
setStreamingMessageIndex(newMessages.length - 1);
|
||||
return newMessages;
|
||||
});
|
||||
|
||||
await runAgent(trimmed, provider, {
|
||||
debug: debugMode,
|
||||
toolsEnabled,
|
||||
planMode: isPlanMode,
|
||||
onText: (text: string) => {
|
||||
setIsThinking(false);
|
||||
setMessages((prev) => {
|
||||
const newMessages = [...prev];
|
||||
const idx = streamingMessageIndex ?? newMessages.length - 1;
|
||||
|
||||
if (newMessages[idx]?.type === 'assistant') {
|
||||
const current = newMessages[idx] as { type: 'assistant'; content: string };
|
||||
newMessages[idx] = {
|
||||
...current,
|
||||
content: current.content + text,
|
||||
};
|
||||
}
|
||||
return newMessages;
|
||||
});
|
||||
},
|
||||
onToolCall: (tc) => {
|
||||
setIsThinking(false);
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{ type: 'tool-call', name: tc.name, args: tc.arguments },
|
||||
]);
|
||||
// Reset streaming index since we inserted a message
|
||||
setStreamingMessageIndex(null);
|
||||
},
|
||||
onToolResult: (result) => {
|
||||
// Attach result to the most recent tool call that doesn't have one yet
|
||||
setMessages((prev) => {
|
||||
const newMessages = [...prev];
|
||||
for (let i = newMessages.length - 1; i >= 0; i--) {
|
||||
const msg = newMessages[i];
|
||||
if (msg && msg.type === 'tool-call' && (msg as any).result === undefined) {
|
||||
(newMessages as any)[i] = {
|
||||
...msg,
|
||||
result: result.result,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
return newMessages;
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
setIsThinking(false);
|
||||
|
||||
if (debugMode) {
|
||||
setLastError(err);
|
||||
try {
|
||||
const red = '\x1b[31m';
|
||||
const reset = '\x1b[0m';
|
||||
const border = '─'.repeat(50);
|
||||
|
||||
console.error(`\n${red}┌${border}┐`);
|
||||
console.error(`│ FULL PROVIDER ERROR${' '.repeat(29)}│`);
|
||||
console.error(`├${border}┤`);
|
||||
console.error(`│ Message: ${(err?.message || String(err)).slice(0, 40)}${' '.repeat(9)}│`);
|
||||
console.error(`│ Name: ${err?.name || 'Error'}${' '.repeat(42 - (err?.name || 'Error').length)}│`);
|
||||
|
||||
if (err?.response?.status) {
|
||||
console.error(`│ Status: ${err.response.status}${' '.repeat(42 - String(err.response.status).length)}│`);
|
||||
}
|
||||
|
||||
console.error(`└${border}┘${reset}`);
|
||||
console.error('Full object:');
|
||||
console.dir(err, { depth: 6 });
|
||||
console.error(`${red}${'='.repeat(52)}${reset}\n`);
|
||||
} catch (logErr) {
|
||||
console.error('Failed to log full error:', logErr);
|
||||
}
|
||||
} else {
|
||||
setLastError(null);
|
||||
}
|
||||
|
||||
const friendlyMessage = toFriendlyError(err);
|
||||
setMessages((prev) => [...prev, { type: 'system', content: friendlyMessage }]);
|
||||
} finally {
|
||||
setIsThinking(false);
|
||||
setStreamingMessageIndex(null);
|
||||
}
|
||||
};
|
||||
|
||||
runAgentLoop();
|
||||
};
|
||||
|
||||
const handleSlashCommand = (command: string) => {
|
||||
const parts = command.trim().split(/\s+/);
|
||||
const cmd = parts[0].toLowerCase();
|
||||
const arg = parts[1]?.toLowerCase();
|
||||
|
||||
if (cmd === '/provider') {
|
||||
setScreen('provider-picker');
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === '/plan') {
|
||||
setIsPlanMode(prev => {
|
||||
const next = !prev;
|
||||
setMessages((msgs) => [
|
||||
...msgs,
|
||||
{
|
||||
type: 'system',
|
||||
content: next
|
||||
? 'Plan mode enabled. The agent will focus on planning before making changes.'
|
||||
: 'Plan mode disabled.'
|
||||
},
|
||||
]);
|
||||
return next;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === '/debug-mode' || cmd === '/debug') {
|
||||
// Support "/debug-mode true", "/debug false", or just toggle
|
||||
let nextDebug: boolean;
|
||||
|
||||
if (arg === 'true') nextDebug = true;
|
||||
else if (arg === 'false') nextDebug = false;
|
||||
else nextDebug = !debugMode;
|
||||
|
||||
setDebugMode(nextDebug);
|
||||
if (!nextDebug) setLastError(null);
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{ type: 'system', content: `Debug mode ${nextDebug ? 'enabled' : 'disabled'}.` },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === '/tools') {
|
||||
const arg2 = parts[1]?.toLowerCase();
|
||||
let nextEnabled: boolean;
|
||||
|
||||
if (arg2 === 'on' || arg2 === 'true') nextEnabled = true;
|
||||
else if (arg2 === 'off' || arg2 === 'false') nextEnabled = false;
|
||||
else nextEnabled = !toolsEnabled;
|
||||
|
||||
setToolsEnabled(nextEnabled);
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{ type: 'system', content: `Tool calling ${nextEnabled ? 'enabled' : 'disabled'}.` },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === '/help') {
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{ type: 'system', content: 'Available commands:' },
|
||||
{ type: 'system', content: ' /provider - Manage LLM providers (fix provider errors here)' },
|
||||
{ type: 'system', content: ' /plan - Toggle Plan Mode (agent plans first, makes no edits)' },
|
||||
{ type: 'system', content: ' /debug-mode - Toggle debug mode (prints full errors to console)' },
|
||||
{ type: 'system', content: ' /tools - Toggle tool calling (useful for debugging provider errors)' },
|
||||
{ type: 'system', content: ' /help - Show this help' },
|
||||
{ type: 'system', content: ' /exit - Quit' },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === '/exit' || cmd === '/quit') {
|
||||
exit();
|
||||
return;
|
||||
}
|
||||
|
||||
setMessages((prev) => [...prev, { type: 'system', content: `Unknown command: ${command}` }]);
|
||||
};
|
||||
|
||||
const handleProviderSelected = (name: string) => {
|
||||
const success = configManager.setActiveProvider(name);
|
||||
if (success) {
|
||||
setMessages((prev) => [...prev, { type: 'system', content: `Switched to provider: ${name}` }]);
|
||||
}
|
||||
setScreen('chat');
|
||||
};
|
||||
|
||||
const handleProviderPickerCancel = () => {
|
||||
setScreen('chat');
|
||||
};
|
||||
|
||||
const handleOpenAddProvider = () => {
|
||||
setScreen('add-provider');
|
||||
};
|
||||
|
||||
const handleAddProviderDone = (providerName?: string) => {
|
||||
setScreen('chat');
|
||||
|
||||
if (providerName) {
|
||||
// Automatically switch to the newly added provider
|
||||
const switched = configManager.setActiveProvider(providerName);
|
||||
|
||||
if (switched) {
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{ type: 'system', content: `Added and switched to provider: ${providerName}` },
|
||||
]);
|
||||
} else {
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{ type: 'system', content: `Provider added: ${providerName}` },
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
setMessages((prev) => [...prev, { type: 'system', content: 'Provider added successfully.' }]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddProviderCancel = () => {
|
||||
setScreen('provider-picker');
|
||||
};
|
||||
|
||||
if (screen === 'add-provider') {
|
||||
return (
|
||||
<AddProvider
|
||||
onDone={handleAddProviderDone}
|
||||
onCancel={handleAddProviderCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (screen === 'provider-picker') {
|
||||
return (
|
||||
<ProviderPicker
|
||||
onSelect={handleProviderSelected}
|
||||
onCancel={handleProviderPickerCancel}
|
||||
onAddNew={handleOpenAddProvider}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const showLogo = messages.length <= 2;
|
||||
|
||||
// Calculate visible messages for scrolling (Grok Build style)
|
||||
const chatHeight = Math.max(8, terminalRows - 6); // leave room for input + status
|
||||
const visibleMessages = messages.slice(scrollOffset, scrollOffset + chatHeight);
|
||||
|
||||
const canScrollUp = scrollOffset < messages.length - 1;
|
||||
const canScrollDown = scrollOffset > 0;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" height="100%">
|
||||
{/* Scrollable messages area with right-side scroll indicator (Grok Build style) */}
|
||||
<Box
|
||||
flexGrow={1}
|
||||
flexDirection="row"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* Main chat content */}
|
||||
<Box
|
||||
flexGrow={1}
|
||||
flexDirection="column"
|
||||
paddingX={1}
|
||||
paddingTop={1}
|
||||
>
|
||||
{showLogo && <Logo />}
|
||||
|
||||
{/* Scroll indicator */}
|
||||
{(canScrollUp || canScrollDown) && (
|
||||
<Text color="gray" dimColor>
|
||||
{canScrollUp ? '↑ ' : ' '}Scroll with ↑↓ / PgUp/PgDn / Home/End {canScrollDown ? '↓' : ''}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Box flexDirection="column">
|
||||
{visibleMessages.map((msg, index) => {
|
||||
const realIndex = scrollOffset + index;
|
||||
|
||||
if (msg.type === 'user') {
|
||||
return (
|
||||
<Box key={realIndex} marginBottom={1}>
|
||||
<Text color="blueBright">
|
||||
{`> ${msg.content}`}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.type === 'assistant') {
|
||||
const isStreaming = realIndex === streamingMessageIndex;
|
||||
return (
|
||||
<Box key={realIndex} marginBottom={1} flexDirection="row">
|
||||
<Text color="cyan" dimColor>● </Text>
|
||||
<Box flexDirection="column" flexGrow={1}>
|
||||
<MessageRenderer content={msg.content} />
|
||||
{isStreaming && (
|
||||
<Text color="cyan" dimColor>▌</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.type === 'tool-call') {
|
||||
const hasResult = !!msg.result;
|
||||
return (
|
||||
<Box key={realIndex} marginBottom={0}>
|
||||
<ToolCallRenderer
|
||||
name={msg.name}
|
||||
args={msg.args}
|
||||
result={msg.result}
|
||||
status={hasResult ? 'success' : 'running'}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.type === 'tool-result') {
|
||||
// Legacy separate results are no longer created; ignore for cleanliness
|
||||
return null;
|
||||
}
|
||||
|
||||
// system messages
|
||||
return (
|
||||
<Box key={realIndex} marginBottom={1}>
|
||||
<Text color="gray" dimColor>
|
||||
{msg.content}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
{isThinking && <ThinkingSpinner />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Scroll position (Grok Build style) */}
|
||||
{(canScrollUp || canScrollDown) && (
|
||||
<Box paddingX={1} justifyContent="flex-end">
|
||||
<Text color="gray" dimColor>
|
||||
{scrollOffset + 1}/{messages.length}{canScrollUp ? ' ↑' : ''}{canScrollDown ? ' ↓' : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Command suggestions */}
|
||||
{suggestions.length > 0 && (
|
||||
<Box paddingX={2} paddingBottom={0}>
|
||||
<Text color="gray" dimColor>
|
||||
Suggestions: {suggestions.map((s, i) => (
|
||||
<Text key={i} color={i === 0 ? 'cyan' : 'gray'}>{s}{i < suggestions.length - 1 ? ' ' : ''}</Text>
|
||||
))} (Tab to autocomplete)
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Debug error box */}
|
||||
{debugMode && lastError && (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor="red"
|
||||
paddingX={1}
|
||||
paddingY={0}
|
||||
marginBottom={1}
|
||||
>
|
||||
<Text color="red" bold>⚠ Debug Error</Text>
|
||||
<Text color="gray" dimColor>
|
||||
{lastError.message || String(lastError)}
|
||||
</Text>
|
||||
{lastError.stack && (
|
||||
<Text color="gray" dimColor>
|
||||
{lastError.stack.split('\n').slice(0, 8).join('\n')}
|
||||
</Text>
|
||||
)}
|
||||
<Text color="cyan" dimColor>
|
||||
(Full details in terminal • /debug-mode false to hide)
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Input box at the bottom */}
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={isPlanMode ? 'green' : 'gray'}
|
||||
paddingX={1}
|
||||
paddingY={0}
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
{/* Left: prompt + input */}
|
||||
<Box flexDirection="row">
|
||||
<Text color={isPlanMode ? 'green' : 'greenBright'}>› </Text>
|
||||
<Text color="white">{input}</Text>
|
||||
<Text color="gray">█</Text>
|
||||
</Box>
|
||||
|
||||
{/* Right: Current provider + model */}
|
||||
<Box flexDirection="row">
|
||||
<Text color="cyan" bold>{currentProviderName}</Text>
|
||||
<Text color="gray"> • </Text>
|
||||
<Text color="magenta" dimColor>{currentModel}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Very subtle status line */}
|
||||
<Box paddingX={1} flexDirection="row">
|
||||
<Text color="gray" dimColor>
|
||||
/help • ↑↓ scroll • Ctrl+C exit
|
||||
</Text>
|
||||
{isPlanMode && (
|
||||
<Text color="green"> • PLAN MODE</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
|
||||
export const Logo: React.FC = () => {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color="cyanBright" bold>
|
||||
{` ███████╗ ███████╗ ██████╗ ██████╗ `}
|
||||
</Text>
|
||||
<Text color="cyanBright" bold>
|
||||
{` ╚══███╔╝ ██╔════╝ ██╔══██╗ ██╔═══██╗`}
|
||||
</Text>
|
||||
<Text color="cyanBright" bold>
|
||||
{` ███╔╝ █████╗ ██████╔╝ ██║ ██║`}
|
||||
</Text>
|
||||
<Text color="cyanBright" bold>
|
||||
{` ███╔╝ ██╔══╝ ██╔══██╗ ██║ ██║`}
|
||||
</Text>
|
||||
<Text color="cyanBright" bold>
|
||||
{` ███████╗ ███████╗ ██║ ██║ ╚██████╔╝`}
|
||||
</Text>
|
||||
<Text color="cyanBright" bold>
|
||||
{` ╚══════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ `}
|
||||
</Text>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>
|
||||
A clean terminal AI coding agent
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,216 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { highlightCode } from './highlighter';
|
||||
|
||||
// Formatter for model responses: paragraphs, inline code, and highlighted code blocks
|
||||
interface MessageRendererProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const MessageRenderer: React.FC<MessageRendererProps> = ({ content }) => {
|
||||
const [highlightedBlocks, setHighlightedBlocks] = useState<Map<number, string>>(new Map());
|
||||
|
||||
const codeBlockRegex = /```(\w+)?\n?([\s\S]*?)```/g;
|
||||
const parts: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
const matches: Array<{ index: number; lang: string; code: string; fullMatch: string }> = [];
|
||||
|
||||
while ((match = codeBlockRegex.exec(content)) !== null) {
|
||||
matches.push({
|
||||
index: match.index,
|
||||
lang: match[1] || 'text',
|
||||
code: match[2].trim(),
|
||||
fullMatch: match[0],
|
||||
});
|
||||
}
|
||||
|
||||
// Highlight code blocks asynchronously
|
||||
useEffect(() => {
|
||||
const highlightAll = async () => {
|
||||
const newHighlights = new Map<number, string>();
|
||||
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
const m = matches[i];
|
||||
try {
|
||||
const ansi = await highlightCode(m.code, m.lang);
|
||||
newHighlights.set(i, ansi);
|
||||
} catch {
|
||||
newHighlights.set(i, m.code);
|
||||
}
|
||||
}
|
||||
|
||||
setHighlightedBlocks(newHighlights);
|
||||
};
|
||||
|
||||
if (matches.length > 0) {
|
||||
highlightAll();
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
for (let i = 0; i < matches.length; i++) {
|
||||
const m = matches[i];
|
||||
|
||||
// Add text before this code block (with paragraph formatting)
|
||||
if (m.index > lastIndex) {
|
||||
const textBefore = content.slice(lastIndex, m.index);
|
||||
parts.push(renderTextBlock(textBefore, `text-${parts.length}`));
|
||||
}
|
||||
|
||||
const highlighted = highlightedBlocks.get(i);
|
||||
|
||||
parts.push(
|
||||
<Box
|
||||
key={`code-${i}`}
|
||||
flexDirection="column"
|
||||
borderStyle="single"
|
||||
borderColor="gray"
|
||||
marginTop={1}
|
||||
marginBottom={1}
|
||||
paddingX={1}
|
||||
>
|
||||
{m.lang && m.lang !== 'text' && (
|
||||
<Text color="cyan" dimColor>{m.lang}</Text>
|
||||
)}
|
||||
{highlighted ? (
|
||||
<Text>{highlighted}</Text>
|
||||
) : (
|
||||
<Text color="gray" dimColor>Highlighting...</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
lastIndex = m.index + m.fullMatch.length;
|
||||
}
|
||||
|
||||
// Add remaining text after last code block (with paragraph formatting)
|
||||
if (lastIndex < content.length) {
|
||||
const remaining = content.slice(lastIndex);
|
||||
parts.push(renderTextBlock(remaining, `text-${parts.length}`));
|
||||
}
|
||||
|
||||
return <>{parts.length > 0 ? parts : renderTextBlock(content, 'full')}</>;
|
||||
};
|
||||
|
||||
// Stronger formatter for model responses (paragraphs + basic lists + inline formatting)
|
||||
function renderTextBlock(text: string, keyPrefix: string): React.ReactNode {
|
||||
const lines = text.split('\n');
|
||||
|
||||
// Group into logical blocks: paragraphs vs lists
|
||||
const blocks: Array<{ type: 'paragraph' | 'list'; lines: string[] }> = [];
|
||||
let current: { type: 'paragraph' | 'list'; lines: string[] } | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
const isListItem = /^\s*(\d+\.|[-*+])\s+/.test(line);
|
||||
|
||||
if (isListItem) {
|
||||
if (!current || current.type !== 'list') {
|
||||
if (current) blocks.push(current);
|
||||
current = { type: 'list', lines: [] };
|
||||
}
|
||||
current.lines.push(line);
|
||||
} else {
|
||||
if (!current || current.type !== 'paragraph') {
|
||||
if (current) blocks.push(current);
|
||||
current = { type: 'paragraph', lines: [] };
|
||||
}
|
||||
current.lines.push(line);
|
||||
}
|
||||
}
|
||||
if (current) blocks.push(current);
|
||||
|
||||
return (
|
||||
<Box key={keyPrefix} flexDirection="column">
|
||||
{blocks.map((block, bIndex) => {
|
||||
const isLastBlock = bIndex === blocks.length - 1;
|
||||
|
||||
if (block.type === 'list') {
|
||||
return (
|
||||
<Box
|
||||
key={`${keyPrefix}-list-${bIndex}`}
|
||||
flexDirection="column"
|
||||
marginBottom={isLastBlock ? 0 : 1}
|
||||
>
|
||||
{block.lines.map((line, lIndex) => (
|
||||
<Text key={`${keyPrefix}-l-${lIndex}`}>
|
||||
{formatInline(line)}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Paragraph block — join lines and split on double newlines for sub-paragraphs
|
||||
const paragraphText = block.lines.join('\n').trim();
|
||||
const subParagraphs = paragraphText
|
||||
.split(/\n\s*\n/)
|
||||
.map(p => p.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={`${keyPrefix}-p-${bIndex}`}
|
||||
flexDirection="column"
|
||||
marginBottom={isLastBlock ? 0 : 1}
|
||||
>
|
||||
{subParagraphs.map((p, pIndex) => (
|
||||
<Box key={`${keyPrefix}-sp-${pIndex}`} marginBottom={pIndex < subParagraphs.length - 1 ? 1 : 0}>
|
||||
<Text>{formatInline(p)}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Handles inline formatting: **bold**, `inline code`
|
||||
function formatInline(text: string): React.ReactNode {
|
||||
const elements: React.ReactNode[] = [];
|
||||
let remaining = text;
|
||||
let idx = 0;
|
||||
|
||||
// Simple sequential parser for **bold** and `code`
|
||||
while (remaining.length > 0) {
|
||||
// Try bold first
|
||||
const boldMatch = remaining.match(/^\*\*([^*]+)\*\*/);
|
||||
if (boldMatch) {
|
||||
elements.push(
|
||||
<Text key={`bold-${idx++}`} bold color="white">
|
||||
{boldMatch[1]}
|
||||
</Text>
|
||||
);
|
||||
remaining = remaining.slice(boldMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try inline code
|
||||
const codeMatch = remaining.match(/^`([^`]+)`/);
|
||||
if (codeMatch) {
|
||||
elements.push(
|
||||
<Text key={`code-${idx++}`} color="cyan" backgroundColor="#2d2d2d">
|
||||
{codeMatch[1]}
|
||||
</Text>
|
||||
);
|
||||
remaining = remaining.slice(codeMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Take plain text until next special char
|
||||
const nextSpecial = remaining.search(/(\*\*|`)/);
|
||||
const plain = nextSpecial === -1 ? remaining : remaining.slice(0, nextSpecial);
|
||||
if (plain) {
|
||||
elements.push(<Text key={`plain-${idx++}`}>{plain}</Text>);
|
||||
remaining = remaining.slice(plain.length);
|
||||
} else {
|
||||
// Fallback single char
|
||||
elements.push(<Text key={`plain-${idx++}`}>{remaining[0]}</Text>);
|
||||
remaining = remaining.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
return <>{elements}</>;
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Text, useInput } from 'ink';
|
||||
import { configManager } from '../config/manager';
|
||||
import type { ProviderProfile } from '../config/types';
|
||||
|
||||
interface ProviderPickerProps {
|
||||
onSelect: (name: string) => void;
|
||||
onCancel: () => void;
|
||||
onAddNew: () => void;
|
||||
}
|
||||
|
||||
export const ProviderPicker: React.FC<ProviderPickerProps> = ({ onSelect, onCancel, onAddNew }) => {
|
||||
const providers = configManager.listProviders();
|
||||
const activeProvider = configManager.getActiveProvider()?.name;
|
||||
|
||||
const totalItems = providers.length + 1; // +1 for "Add new"
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.escape || (key.ctrl && input === 'c')) {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.upArrow) {
|
||||
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.downArrow) {
|
||||
setSelectedIndex((prev) => Math.min(totalItems - 1, prev + 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.return) {
|
||||
if (selectedIndex < providers.length) {
|
||||
const selected = providers[selectedIndex];
|
||||
onSelect(selected.name);
|
||||
} else {
|
||||
// Last item = Add new
|
||||
onAddNew();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Quick select by number
|
||||
const num = parseInt(input, 10);
|
||||
if (!isNaN(num) && num >= 1 && num <= totalItems) {
|
||||
if (num <= providers.length) {
|
||||
const selected = providers[num - 1];
|
||||
onSelect(selected.name);
|
||||
} else {
|
||||
onAddNew();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text bold color="cyan">
|
||||
Select Provider
|
||||
</Text>
|
||||
<Text color="gray" dimColor>
|
||||
↑↓ to navigate • Enter to select • Esc to cancel
|
||||
</Text>
|
||||
|
||||
<Box marginY={1} flexDirection="column">
|
||||
{providers.map((provider, index) => {
|
||||
const isSelected = index === selectedIndex;
|
||||
const isActive = provider.name === activeProvider;
|
||||
|
||||
return (
|
||||
<Box key={provider.name} paddingLeft={1}>
|
||||
<Text color={isSelected ? 'green' : 'white'}>
|
||||
{isSelected ? '› ' : ' '}
|
||||
{provider.name}
|
||||
{isActive && <Text color="blue"> (current)</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Add new option */}
|
||||
<Box paddingLeft={1}>
|
||||
<Text color={selectedIndex === providers.length ? 'green' : 'cyan'}>
|
||||
{selectedIndex === providers.length ? '› ' : ' '}
|
||||
+ Add new provider...
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{providers[selectedIndex] && (
|
||||
<Box flexDirection="column" marginLeft={2} borderStyle="round" paddingX={1}>
|
||||
<Text>
|
||||
<Text bold>Model:</Text> {providers[selectedIndex].model}
|
||||
</Text>
|
||||
<Text>
|
||||
<Text bold>Base URL:</Text> {providers[selectedIndex].baseURL}
|
||||
</Text>
|
||||
{providers[selectedIndex].description && (
|
||||
<Text>
|
||||
<Text bold>Description:</Text> {providers[selectedIndex].description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color="gray" dimColor>
|
||||
Press 1-{totalItems} for quick selection
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Text } from 'ink';
|
||||
import Spinner from 'ink-spinner';
|
||||
|
||||
interface ThinkingSpinnerProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const ThinkingSpinner: React.FC<ThinkingSpinnerProps> = ({ label = 'thinking' }) => {
|
||||
return (
|
||||
<Text color="gray" dimColor>
|
||||
<Spinner type="dots" /> {label}...
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -1,202 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { highlightCode } from './highlighter';
|
||||
|
||||
interface ToolCallRendererProps {
|
||||
name: string;
|
||||
args: string;
|
||||
result?: string;
|
||||
status?: 'running' | 'success' | 'error';
|
||||
}
|
||||
|
||||
export const ToolCallRenderer: React.FC<ToolCallRendererProps> = ({
|
||||
name,
|
||||
args,
|
||||
result,
|
||||
status = 'success',
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [highlightedArgs, setHighlightedArgs] = useState<string | null>(null);
|
||||
const [highlightedResult, setHighlightedResult] = useState<string | null>(null);
|
||||
|
||||
const hasResult = !!result;
|
||||
const isLongResult = hasResult && (result!.length > 400 || result!.split('\n').length > 12);
|
||||
const [showFullResult, setShowFullResult] = useState(false);
|
||||
|
||||
// Generate a nice one-line summary for collapsed state
|
||||
const summary = getToolSummary(name, args);
|
||||
|
||||
// Highlight arguments (only when expanded)
|
||||
useEffect(() => {
|
||||
if (!isExpanded) return;
|
||||
|
||||
const highlight = async () => {
|
||||
try {
|
||||
let jsonArgs = args;
|
||||
try {
|
||||
const parsed = JSON.parse(args);
|
||||
jsonArgs = JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
// not valid JSON
|
||||
}
|
||||
const ansi = await highlightCode(jsonArgs, 'json');
|
||||
setHighlightedArgs(ansi);
|
||||
} catch {
|
||||
setHighlightedArgs(args);
|
||||
}
|
||||
};
|
||||
highlight();
|
||||
}, [args, isExpanded]);
|
||||
|
||||
// Highlight result (only when expanded)
|
||||
useEffect(() => {
|
||||
if (!isExpanded || !result) return;
|
||||
|
||||
const highlight = async () => {
|
||||
try {
|
||||
const looksLikeCode =
|
||||
result.includes('function') ||
|
||||
result.includes('const ') ||
|
||||
result.includes('import ') ||
|
||||
result.includes('=>') ||
|
||||
result.includes('class ');
|
||||
|
||||
const ansi = await highlightCode(result, looksLikeCode ? 'typescript' : 'text');
|
||||
setHighlightedResult(ansi);
|
||||
} catch {
|
||||
setHighlightedResult(result);
|
||||
}
|
||||
};
|
||||
highlight();
|
||||
}, [result, isExpanded]);
|
||||
|
||||
const borderColor =
|
||||
status === 'running' ? 'yellow' : status === 'error' ? 'red' : 'green';
|
||||
|
||||
const statusIcon =
|
||||
status === 'running' ? '⟳' : status === 'error' ? '✕' : '✓';
|
||||
|
||||
const statusColor =
|
||||
status === 'running' ? 'yellow' : status === 'error' ? 'red' : 'green';
|
||||
|
||||
// === COLLAPSED VIEW — clean formatter for model actions ===
|
||||
if (!isExpanded) {
|
||||
const showToggle = args || hasResult;
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" paddingX={1} paddingY={0}>
|
||||
<Text color={statusColor} bold>
|
||||
{statusIcon} {name}
|
||||
</Text>
|
||||
<Text color="gray" dimColor> {summary}</Text>
|
||||
|
||||
{showToggle && (
|
||||
<Text
|
||||
color="cyan"
|
||||
dimColor
|
||||
{...({ onPress: () => setIsExpanded(true) } as any)}
|
||||
>
|
||||
{' '}[show]
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// === EXPANDED VIEW — formatted details ===
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="single"
|
||||
borderColor={borderColor}
|
||||
paddingX={0}
|
||||
paddingY={0}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box paddingX={1} flexDirection="row" justifyContent="space-between">
|
||||
<Text color={statusColor} bold>
|
||||
{statusIcon} {name}
|
||||
</Text>
|
||||
<Text
|
||||
color="cyan"
|
||||
dimColor
|
||||
{...({ onPress: () => setIsExpanded(false) } as any)}
|
||||
>
|
||||
[hide]
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Arguments */}
|
||||
<Box paddingX={1} paddingTop={0} flexDirection="column">
|
||||
<Text color="gray" dimColor bold>args</Text>
|
||||
{highlightedArgs ? (
|
||||
<Text>{highlightedArgs}</Text>
|
||||
) : (
|
||||
<Text color="gray" dimColor>…</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Result */}
|
||||
{hasResult && (
|
||||
<Box paddingX={1} paddingTop={0} flexDirection="column">
|
||||
<Text color="gray" dimColor bold>result</Text>
|
||||
{highlightedResult || result ? (
|
||||
<Text color="green" dimColor>
|
||||
{isLongResult && !showFullResult
|
||||
? (highlightedResult || result!).slice(0, 200) + '...'
|
||||
: (highlightedResult || result)}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{isLongResult && (
|
||||
<Text
|
||||
color="cyan"
|
||||
dimColor
|
||||
{...({ onPress: () => setShowFullResult(!showFullResult) } as any)}
|
||||
>
|
||||
{showFullResult ? ' [less]' : ' [more]'}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// Smart one-line summary for collapsed tool calls.
|
||||
// We want: "tool name + the command it actually ran" (e.g. "read src/App.tsx", "ls -la .")
|
||||
function getToolSummary(name: string, args: string): string {
|
||||
try {
|
||||
const parsed = JSON.parse(args);
|
||||
|
||||
if (name === 'bash') {
|
||||
const cmd = parsed.command || '';
|
||||
return cmd.length > 65 ? cmd.slice(0, 62) + '...' : cmd;
|
||||
}
|
||||
|
||||
if (name === 'read_file') {
|
||||
const path = parsed.path || '';
|
||||
const short = path.length > 60 ? path.slice(0, 57) + '...' : path;
|
||||
return `read ${short}`;
|
||||
}
|
||||
|
||||
if (name === 'edit_file') {
|
||||
const path = parsed.path || '';
|
||||
const short = path.length > 60 ? path.slice(0, 57) + '...' : path;
|
||||
return `edit ${short}`;
|
||||
}
|
||||
|
||||
// Fallback for future tools
|
||||
const keys = Object.keys(parsed);
|
||||
if (keys.length > 0) {
|
||||
const firstKey = keys[0]!;
|
||||
const val = String((parsed as any)[firstKey] ?? '');
|
||||
const shortVal = val.length > 50 ? val.slice(0, 47) + '...' : val;
|
||||
return `${firstKey}: ${shortVal}`;
|
||||
}
|
||||
|
||||
return args.length > 65 ? args.slice(0, 62) + '...' : args;
|
||||
} catch {
|
||||
return args.length > 65 ? args.slice(0, 62) + '...' : args;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { createHighlighter } from 'shiki'
|
||||
|
||||
let highlighterPromise: ReturnType<typeof createHighlighter> | null = null
|
||||
|
||||
export async function getHighlighter() {
|
||||
if (!highlighterPromise) {
|
||||
highlighterPromise = createHighlighter({
|
||||
themes: ['github-dark', 'github-light'],
|
||||
langs: [
|
||||
'typescript',
|
||||
'javascript',
|
||||
'tsx',
|
||||
'jsx',
|
||||
'json',
|
||||
'python',
|
||||
'rust',
|
||||
'go',
|
||||
'bash',
|
||||
'shell',
|
||||
'markdown',
|
||||
'css',
|
||||
'html',
|
||||
'sql',
|
||||
'yaml',
|
||||
'toml',
|
||||
'dockerfile',
|
||||
],
|
||||
})
|
||||
}
|
||||
return highlighterPromise
|
||||
}
|
||||
|
||||
export async function highlightCode(code: string, lang: string = 'text'): Promise<string> {
|
||||
try {
|
||||
const highlighter = await getHighlighter()
|
||||
return highlighter.codeToAnsi(code, {
|
||||
lang: lang || 'text',
|
||||
theme: 'github-dark',
|
||||
})
|
||||
} catch (error) {
|
||||
// Fallback to plain text if highlighting fails
|
||||
return code
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render } from 'ink';
|
||||
import { App } from './App';
|
||||
import { getHighlighter } from './highlighter';
|
||||
|
||||
// Preload Shiki highlighter early for fast syntax highlighting
|
||||
getHighlighter().catch(() => {
|
||||
// Silently fail - we'll fallback gracefully
|
||||
});
|
||||
|
||||
export function startTUI() {
|
||||
render(<App />);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import { runAgent } from '../src/agent/loop';
|
||||
import { DEFAULT_SYSTEM_PROMPT, PLAN_MODE_SYSTEM_PROMPT } from '../src/agent/prompts';
|
||||
import type { Provider, Message, StreamEvent } from '../src/providers/types';
|
||||
|
||||
// A mock provider that records the messages it receives and replays a
|
||||
// scripted sequence of stream events per turn.
|
||||
class MockProvider implements Provider {
|
||||
public received: Message[][] = [];
|
||||
private turns: StreamEvent[][];
|
||||
private turn = 0;
|
||||
|
||||
constructor(turns: StreamEvent[][]) {
|
||||
this.turns = turns;
|
||||
}
|
||||
|
||||
async *streamCompletion(messages: Message[]): AsyncIterable<StreamEvent> {
|
||||
// Snapshot the messages for this turn so tests can inspect the prompt.
|
||||
this.received.push(messages.map((m) => ({ ...m })));
|
||||
const events = this.turns[this.turn] ?? [{ type: 'done' as const }];
|
||||
this.turn++;
|
||||
for (const ev of events) yield ev;
|
||||
}
|
||||
}
|
||||
|
||||
describe('runAgent system prompt selection', () => {
|
||||
it('uses the default system prompt when plan mode is off', async () => {
|
||||
const provider = new MockProvider([[{ type: 'text', content: 'hi' }]]);
|
||||
await runAgent('hello', provider, { planMode: false });
|
||||
|
||||
expect(provider.received[0]?.[0]?.role).toBe('system');
|
||||
expect(provider.received[0]?.[0]?.content).toBe(DEFAULT_SYSTEM_PROMPT);
|
||||
});
|
||||
|
||||
it('uses the plan-mode system prompt when plan mode is on', async () => {
|
||||
const provider = new MockProvider([[{ type: 'text', content: 'hi' }]]);
|
||||
await runAgent('hello', provider, { planMode: true });
|
||||
|
||||
expect(provider.received[0]?.[0]?.content).toBe(PLAN_MODE_SYSTEM_PROMPT);
|
||||
expect(provider.received[0]?.[0]?.content).toContain('PLAN MODE IS ACTIVE');
|
||||
});
|
||||
});
|
||||
|
||||
describe('runAgent tool-call flow', () => {
|
||||
it('executes a tool call and feeds the result back to the model', async () => {
|
||||
const provider = new MockProvider([
|
||||
// Turn 1: the model asks to update the plan.
|
||||
[
|
||||
{ type: 'tool-call-start', id: 'call_1', name: 'update_plan' },
|
||||
{
|
||||
type: 'tool-call-delta',
|
||||
id: 'call_1',
|
||||
argumentsFragment: JSON.stringify({
|
||||
plan: [{ id: '1', content: 'do it', status: 'pending' }],
|
||||
}),
|
||||
},
|
||||
{ type: 'tool-call-end', id: 'call_1' },
|
||||
],
|
||||
// Turn 2: the model produces a final answer.
|
||||
[{ type: 'text', content: 'all done' }],
|
||||
]);
|
||||
|
||||
const toolCalls: string[] = [];
|
||||
const toolResults: string[] = [];
|
||||
|
||||
const answer = await runAgent('make a plan', provider, {
|
||||
onToolCall: (tc) => toolCalls.push(tc.name),
|
||||
onToolResult: (r) => toolResults.push(r.result),
|
||||
});
|
||||
|
||||
expect(answer).toBe('all done');
|
||||
expect(toolCalls).toEqual(['update_plan']);
|
||||
expect(toolResults[0]).toContain('do it');
|
||||
|
||||
// Second turn must include the tool result message fed back in.
|
||||
const secondTurn = provider.received[1] ?? [];
|
||||
const toolMsg = secondTurn.find((m) => m.role === 'tool');
|
||||
expect(toolMsg).toBeDefined();
|
||||
expect(toolMsg?.content).toContain('do it');
|
||||
});
|
||||
|
||||
it('stops and returns text when the model makes no tool calls', async () => {
|
||||
const provider = new MockProvider([[{ type: 'text', content: 'just an answer' }]]);
|
||||
const answer = await runAgent('hi', provider, {});
|
||||
expect(answer).toBe('just an answer');
|
||||
expect(provider.received).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { mkdtemp, rm, writeFile, readFile } from 'fs/promises';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { readFileTool } from '../src/tools/read-file';
|
||||
import { editFileTool } from '../src/tools/edit-file';
|
||||
|
||||
let dir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), 'zero-test-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('readFileTool', () => {
|
||||
it('returns the file contents', async () => {
|
||||
const file = join(dir, 'hello.txt');
|
||||
await writeFile(file, 'hello world', 'utf-8');
|
||||
|
||||
const result = await readFileTool.execute({ path: file });
|
||||
expect(result).toContain('hello world');
|
||||
expect(result).toContain(file);
|
||||
});
|
||||
|
||||
it('returns an error message for a missing file', async () => {
|
||||
const result = await readFileTool.execute({ path: join(dir, 'nope.txt') });
|
||||
expect(result).toContain('Error reading file');
|
||||
});
|
||||
});
|
||||
|
||||
describe('editFileTool', () => {
|
||||
it('replaces an exact string and writes it back', async () => {
|
||||
const file = join(dir, 'code.ts');
|
||||
await writeFile(file, 'const a = 1;\nconst b = 2;\n', 'utf-8');
|
||||
|
||||
const result = await editFileTool.execute({
|
||||
path: file,
|
||||
old_string: 'const a = 1;',
|
||||
new_string: 'const a = 42;',
|
||||
});
|
||||
|
||||
expect(result).toContain('Successfully edited');
|
||||
expect(await readFile(file, 'utf-8')).toBe('const a = 42;\nconst b = 2;\n');
|
||||
});
|
||||
|
||||
it('reports when the target string is not found', async () => {
|
||||
const file = join(dir, 'code.ts');
|
||||
await writeFile(file, 'const a = 1;\n', 'utf-8');
|
||||
|
||||
const result = await editFileTool.execute({
|
||||
path: file,
|
||||
old_string: 'does not exist',
|
||||
new_string: 'whatever',
|
||||
});
|
||||
|
||||
expect(result).toContain('Could not find the exact string');
|
||||
});
|
||||
|
||||
it('rejects when multiple occurrences exist', async () => {
|
||||
const file = join(dir, 'dup.txt');
|
||||
await writeFile(file, 'x\nx\n', 'utf-8');
|
||||
|
||||
const result = await editFileTool.execute({ path: file, old_string: 'x', new_string: 'y' });
|
||||
expect(result).toContain('2 occurrences');
|
||||
// File should not be modified
|
||||
expect(await readFile(file, 'utf-8')).toBe('x\nx\n');
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||
import { planTool, getCurrentPlan, clearPlan } from '../src/tools/plan';
|
||||
|
||||
const samplePlan = [
|
||||
{ id: '1', content: 'First step', status: 'completed' as const },
|
||||
{ id: '2', content: 'Second step', status: 'in_progress' as const, notes: 'halfway' },
|
||||
{ id: '3', content: 'Third step', status: 'pending' as const },
|
||||
];
|
||||
|
||||
describe('planTool', () => {
|
||||
beforeEach(() => clearPlan());
|
||||
|
||||
it('stores the plan and reflects it via getCurrentPlan', async () => {
|
||||
await planTool.execute({ plan: samplePlan });
|
||||
expect(getCurrentPlan()).toEqual(samplePlan);
|
||||
});
|
||||
|
||||
it('formats the plan with status markers and notes', async () => {
|
||||
const output = await planTool.execute({ plan: samplePlan });
|
||||
expect(output).toContain('1. ✓ [completed] First step');
|
||||
expect(output).toContain('2. ◉ [in_progress] Second step');
|
||||
expect(output).toContain('Notes: halfway');
|
||||
expect(output).toContain('3. ○ [pending] Third step');
|
||||
});
|
||||
|
||||
it('clearPlan empties the stored plan', async () => {
|
||||
await planTool.execute({ plan: samplePlan });
|
||||
clearPlan();
|
||||
expect(getCurrentPlan()).toEqual([]);
|
||||
});
|
||||
|
||||
it('getCurrentPlan returns a copy, not the internal reference', async () => {
|
||||
await planTool.execute({ plan: samplePlan });
|
||||
const snapshot = getCurrentPlan();
|
||||
snapshot.push({ id: 'x', content: 'mutation', status: 'pending' });
|
||||
expect(getCurrentPlan()).toHaveLength(samplePlan.length);
|
||||
});
|
||||
|
||||
it('rejects invalid status values', async () => {
|
||||
await expect(
|
||||
planTool.execute({ plan: [{ id: '1', content: 'bad', status: 'nope' }] })
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,663 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { existsSync, readFileSync, readdirSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
getGlobalModelDefinition,
|
||||
getModelsForProvider,
|
||||
getProviderDefinition,
|
||||
listFirstPartyProviders,
|
||||
listGlobalModels,
|
||||
listModelDefinitions,
|
||||
listProviderDefinitions,
|
||||
listProvidersByKind,
|
||||
resolveEffectiveTransportConfig,
|
||||
} from '../src/providers/catalog';
|
||||
import { discoverModelsForProvider } from '../src/providers/discovery';
|
||||
import { createProvider, isProviderRuntimeSupported } from '../src/providers/factory';
|
||||
import { OpenAIProvider } from '../src/providers/openai';
|
||||
import { parseProviderCommand, resolveProviderCommandConfig } from '../src/config/provider';
|
||||
import type { ProviderDefinition } from '../src/providers/catalog/types';
|
||||
|
||||
function testCacheDir(name: string): string {
|
||||
return join(
|
||||
import.meta.dir,
|
||||
'..',
|
||||
'.zero-test-cache',
|
||||
`${name}-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
);
|
||||
}
|
||||
|
||||
function cleanupTestCacheDir(path: string): void {
|
||||
try {
|
||||
rmSync(path, { recursive: true, force: true });
|
||||
} catch (err: any) {
|
||||
if (err?.code !== 'EACCES' && err?.code !== 'EPERM') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function catalogTomlFiles(): string[] {
|
||||
const catalogRoot = join(import.meta.dir, '..', 'src', 'providers', 'catalog');
|
||||
const definitionsDir = join(catalogRoot, 'definitions');
|
||||
const modelsDir = join(catalogRoot, 'models');
|
||||
|
||||
return [
|
||||
...readdirSync(definitionsDir)
|
||||
.filter((file) => file.endsWith('.toml'))
|
||||
.map((file) => join(definitionsDir, file)),
|
||||
...readdirSync(modelsDir)
|
||||
.filter((file) => file.endsWith('.toml'))
|
||||
.map((file) => join(modelsDir, file)),
|
||||
];
|
||||
}
|
||||
|
||||
function catalogDefinitionTomlFiles(): string[] {
|
||||
const definitionsDir = join(import.meta.dir, '..', 'src', 'providers', 'catalog', 'definitions');
|
||||
|
||||
return readdirSync(definitionsDir)
|
||||
.filter((file) => file.endsWith('.toml'))
|
||||
.map((file) => join(definitionsDir, file));
|
||||
}
|
||||
|
||||
function providerCatalogDocFiles(): string[] {
|
||||
const docsDir = join(import.meta.dir, '..', 'docs', 'provider-catalog');
|
||||
|
||||
return readdirSync(docsDir)
|
||||
.filter((file) => file.endsWith('.md'))
|
||||
.map((file) => join(docsDir, file));
|
||||
}
|
||||
|
||||
function catalogDocumentationAndTomlFiles(): string[] {
|
||||
return [
|
||||
join(import.meta.dir, '..', 'README.md'),
|
||||
...providerCatalogDocFiles(),
|
||||
...catalogTomlFiles(),
|
||||
];
|
||||
}
|
||||
|
||||
describe('provider catalog', () => {
|
||||
it('keeps model metadata in a single TOML block with dotted keys', () => {
|
||||
const forbiddenModelSubtables = /^\s*\[(?:catalog\.models|model)\.(?:capabilities|cost|transportOverrides|temperatureRange)\]\s*$/m;
|
||||
|
||||
for (const file of catalogTomlFiles()) {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
expect(content, `${file} should use dotted model keys instead of nested model sub-tables`)
|
||||
.not.toMatch(forbiddenModelSubtables);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps provider metadata sections in single TOML blocks with dotted keys', () => {
|
||||
const forbiddenProviderSubtables = /^\s*\[(?:transportConfig\.(?:authHeader|headers)|catalog\.discovery|preset\.badge|setup\..+|validation\..+|usage\..+)\]\s*$/m;
|
||||
|
||||
for (const file of catalogDocumentationAndTomlFiles()) {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
expect(content, `${file} should use dotted keys instead of nested provider sub-tables`)
|
||||
.not.toMatch(forbiddenProviderSubtables);
|
||||
}
|
||||
});
|
||||
|
||||
it('documents the provider catalog model schema without nested model sub-tables', () => {
|
||||
const modelDocs = readFileSync(join(import.meta.dir, '..', 'docs', 'provider-catalog', 'models.md'), 'utf-8');
|
||||
|
||||
expect(modelDocs).toContain('entry must stay as one TOML block');
|
||||
expect(modelDocs).toContain('Global models describe identity, ownership, limits, and');
|
||||
expect(modelDocs.match(/\[\[model\]\]/g)?.length).toBeGreaterThanOrEqual(2);
|
||||
expect(modelDocs).toContain('contextWindow');
|
||||
expect(modelDocs).toContain('maxOutputTokens');
|
||||
expect(modelDocs).toContain('temperatureRange.min');
|
||||
expect(modelDocs).toContain('temperatureRange.max');
|
||||
expect(modelDocs).toContain('cost.inputPerMillion');
|
||||
expect(modelDocs).toContain('cost.outputPerMillion');
|
||||
expect(modelDocs).toContain('cost.cachePerMillion');
|
||||
expect(modelDocs).toContain('capabilities.supportsVision');
|
||||
expect(modelDocs).toContain('transportOverrides.maxTokensField');
|
||||
expect(modelDocs).toContain('costCurrency');
|
||||
expect(modelDocs).toContain('Do not put `cost` or `costCurrency` in');
|
||||
expect(modelDocs).toContain('| Field | Required | Accepted values or behavior |');
|
||||
expect(modelDocs).not.toMatch(/^\s*\[(?:catalog\.models|model)\.(?:capabilities|cost|transportOverrides|temperatureRange)\]\s*$/m);
|
||||
expect(modelDocs).not.toContain('cost.currency');
|
||||
});
|
||||
|
||||
it('keeps README lightweight and links to detailed provider catalog docs', () => {
|
||||
const readme = readFileSync(join(import.meta.dir, '..', 'README.md'), 'utf-8');
|
||||
|
||||
expect(readme).toContain('[Provider definitions](docs/provider-catalog/providers.md)');
|
||||
expect(readme).toContain('[Model catalog entries](docs/provider-catalog/models.md)');
|
||||
expect(readme).toContain('[Examples](docs/provider-catalog/examples.md)');
|
||||
expect(readme).toContain('credentialEnvVars = ["MY_GATEWAY_API_KEY"]');
|
||||
expect(readme).toContain('costCurrency = "USD"');
|
||||
expect(readme.match(/MY_GATEWAY_API_KEY/g)?.length).toBe(1);
|
||||
expect(readme.match(/\[\[catalog\.models\]\]/g)?.length).toBe(2);
|
||||
expect(readme).not.toContain('cost.currency');
|
||||
});
|
||||
|
||||
it('documents provider schema guardrails outside the README', () => {
|
||||
const providerDocs = readFileSync(join(import.meta.dir, '..', 'docs', 'provider-catalog', 'providers.md'), 'utf-8');
|
||||
|
||||
expect(providerDocs).toContain('A top-level provider is the company or service that self-publishes a model');
|
||||
expect(providerDocs).toContain('| What you\'re adding | Files to touch | Brand/provider examples |');
|
||||
expect(providerDocs).toContain('OpenAI + GPT/o-series');
|
||||
expect(providerDocs).toContain('Anthropic + Claude/Opus');
|
||||
expect(providerDocs).toContain('Keep credential env vars at the provider top level');
|
||||
expect(providerDocs).toContain('costCurrency');
|
||||
expect(providerDocs).toContain('Keep these nested objects inside one TOML block with dotted keys');
|
||||
expect(providerDocs).toContain('authHeader.name = "authorization"');
|
||||
expect(providerDocs).toContain('discovery.kind = "openai-compatible"');
|
||||
expect(providerDocs).toContain('badge.text = "HOSTED"');
|
||||
});
|
||||
|
||||
it('keeps credential env vars declared once in provider definition TOML', () => {
|
||||
for (const file of catalogDefinitionTomlFiles()) {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
expect(content.match(/^credentialEnvVars\s*=/gm)?.length ?? 0, `${file} should declare credentialEnvVars at most once`)
|
||||
.toBeLessThanOrEqual(1);
|
||||
expect(content, `${file} should not duplicate credential env vars in presets`)
|
||||
.not.toMatch(/^apiKeyEnvVars\s*=/m);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps provider credential env vars scoped to the owning provider', () => {
|
||||
const expectedCredentialEnvVars: Record<string, string[] | undefined> = {
|
||||
groq: ['GROQ_API_KEY'],
|
||||
ollama: undefined,
|
||||
openai: ['OPENAI_API_KEY'],
|
||||
opengateway: ['OPENGATEWAY_API_KEY'],
|
||||
openrouter: ['OPENROUTER_API_KEY'],
|
||||
};
|
||||
|
||||
for (const definition of listProviderDefinitions()) {
|
||||
expect(
|
||||
definition.credentialEnvVars,
|
||||
`${definition.id} should not borrow another provider's credential env vars`
|
||||
).toEqual(expectedCredentialEnvVars[definition.id]);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps global model TOML free of provider pricing metadata', () => {
|
||||
for (const file of catalogTomlFiles().filter((path) => path.includes(`${join('catalog', 'models')}`))) {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
expect(content, `${file} should not define provider route pricing on global models`)
|
||||
.not.toMatch(/^(?:cost\.|costCurrency\s*=)/m);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps currency at provider level instead of model cost level', () => {
|
||||
for (const file of catalogDocumentationAndTomlFiles()) {
|
||||
const content = readFileSync(file, 'utf-8');
|
||||
expect(content, `${file} should use provider-level costCurrency instead of model cost.currency`)
|
||||
.not.toContain('cost.currency');
|
||||
}
|
||||
});
|
||||
|
||||
it('auto-discovers provider and gateway definitions', () => {
|
||||
const definitions = listProviderDefinitions();
|
||||
const opengateway = getProviderDefinition('opengateway');
|
||||
|
||||
expect(definitions.map((definition) => definition.id)).toContain('opengateway');
|
||||
expect(opengateway?.kind).toBe('gateway');
|
||||
expect(opengateway?.baseURL).toBe('https://opengateway.gitlawb.com/v1');
|
||||
expect(opengateway?.defaultModel).toBe('mimo-v2.5-pro');
|
||||
});
|
||||
|
||||
it('parses dotted TOML provider sections as nested config objects', () => {
|
||||
const opengateway = getProviderDefinition('opengateway');
|
||||
const openrouter = getProviderDefinition('openrouter');
|
||||
|
||||
expect(opengateway?.transportConfig?.authHeader).toEqual({
|
||||
name: 'authorization',
|
||||
scheme: 'bearer',
|
||||
});
|
||||
expect(opengateway?.catalog?.discovery).toEqual({
|
||||
kind: 'openai-compatible',
|
||||
requiresAuth: true,
|
||||
path: '/models',
|
||||
});
|
||||
expect(opengateway?.preset?.badge).toEqual({
|
||||
text: 'FREE',
|
||||
color: 'success',
|
||||
});
|
||||
|
||||
expect(openrouter?.transportConfig?.headers).toEqual({
|
||||
'HTTP-Referer': '$OPENROUTER_SITE_URL',
|
||||
'X-OpenRouter-Title': '$OPENROUTER_SITE_NAME',
|
||||
});
|
||||
expect(openrouter?.catalog?.discovery?.kind).toBe('openai-compatible');
|
||||
expect(openrouter?.catalog?.discovery?.path).toBe('/models');
|
||||
});
|
||||
|
||||
it('fans out top-level credential env vars into normalized provider metadata', () => {
|
||||
const opengateway = getProviderDefinition('opengateway');
|
||||
|
||||
expect(opengateway?.credentialEnvVars).toEqual(['OPENGATEWAY_API_KEY']);
|
||||
expect(opengateway?.setup?.credentialEnvVars).toEqual(opengateway?.credentialEnvVars);
|
||||
expect(opengateway?.validation?.credentialEnvVars).toEqual(opengateway?.credentialEnvVars);
|
||||
expect(opengateway?.preset?.apiKeyEnvVars).toEqual(opengateway?.credentialEnvVars);
|
||||
});
|
||||
|
||||
it('keeps first-party pricing on provider catalog routes, not global models', () => {
|
||||
const openai = getProviderDefinition('openai');
|
||||
expect(openai?.costCurrency).toBe('USD');
|
||||
|
||||
const models = getModelsForProvider(openai!);
|
||||
const gpt4o = models.find((model) => model.apiName === 'gpt-4o');
|
||||
const gpt4oMini = models.find((model) => model.apiName === 'gpt-4o-mini');
|
||||
|
||||
expect(gpt4o?.cost).toEqual({
|
||||
inputPerMillion: 2.50,
|
||||
outputPerMillion: 10.00,
|
||||
});
|
||||
expect(gpt4oMini?.cost).toEqual({
|
||||
inputPerMillion: 0.15,
|
||||
outputPerMillion: 0.60,
|
||||
});
|
||||
expect(getGlobalModelDefinition('gpt-4o')).not.toHaveProperty('cost');
|
||||
});
|
||||
|
||||
it('auto-discovers localhost provider definitions', () => {
|
||||
const ollama = getProviderDefinition('ollama');
|
||||
|
||||
expect(ollama).toBeDefined();
|
||||
expect(ollama?.kind).toBe('localhost');
|
||||
expect(ollama?.category).toBe('local');
|
||||
expect(ollama?.apiKeyRequired).toBe(false);
|
||||
expect(ollama?.setup?.authMode).toBe('none');
|
||||
});
|
||||
|
||||
it('lists providers by kind', () => {
|
||||
const gateways = listProvidersByKind('gateway');
|
||||
const localhost = listProvidersByKind('localhost');
|
||||
|
||||
expect(gateways.map((d) => d.id)).toContain('opengateway');
|
||||
expect(localhost.map((d) => d.id)).toContain('ollama');
|
||||
});
|
||||
|
||||
it('auto-discovers global model catalog entries', () => {
|
||||
const models = listModelDefinitions();
|
||||
const gpt4o = getGlobalModelDefinition('gpt-4o');
|
||||
|
||||
expect(models.map((model) => model.id)).toContain('gpt-4o');
|
||||
expect(gpt4o?.tier).toBe('first-party');
|
||||
expect(gpt4o?.contextWindow).toBe(128000);
|
||||
expect(gpt4o?.maxOutputTokens).toBe(16384);
|
||||
expect(gpt4o?.capabilities?.supportsVision).toBe(true);
|
||||
});
|
||||
|
||||
it('lists all global models', () => {
|
||||
const globals = listGlobalModels();
|
||||
const ids = globals.map((m) => m.id);
|
||||
|
||||
expect(ids).toContain('gpt-4o');
|
||||
expect(ids).toContain('gpt-4o-mini');
|
||||
expect(ids).toContain('claude-sonnet-4');
|
||||
expect(ids).toContain('deepseek-v3');
|
||||
});
|
||||
|
||||
it('resolves globalModelId references in gateway catalog models', () => {
|
||||
const opengateway = getProviderDefinition('opengateway');
|
||||
expect(opengateway).toBeDefined();
|
||||
|
||||
const models = getModelsForProvider(opengateway!);
|
||||
// Model inherits id from global catalog when not explicitly set
|
||||
const gpt4oOverride = models.find((m) => m.apiName === 'gpt-4o');
|
||||
|
||||
expect(gpt4oOverride).toBeDefined();
|
||||
expect(gpt4oOverride?.apiName).toBe('gpt-4o');
|
||||
// Gateway overrides reduced limits
|
||||
expect(gpt4oOverride?.contextWindow).toBe(32000);
|
||||
expect(gpt4oOverride?.maxOutputTokens).toBe(4096);
|
||||
// Inherited from global model
|
||||
expect(gpt4oOverride?.capabilities?.supportsVision).toBe(true);
|
||||
expect(gpt4oOverride?.tier).toBe('first-party');
|
||||
// Provider metadata
|
||||
expect(gpt4oOverride?.providerId).toBe('opengateway');
|
||||
expect(gpt4oOverride?.providerKind).toBe('gateway');
|
||||
});
|
||||
|
||||
it('creates runtime providers from provider config', () => {
|
||||
const provider = createProvider({
|
||||
providerId: 'opengateway',
|
||||
apiKey: 'test-key',
|
||||
baseURL: 'https://opengateway.gitlawb.com/v1',
|
||||
model: 'mimo-v2.5-pro',
|
||||
});
|
||||
|
||||
expect(provider).toBeInstanceOf(OpenAIProvider);
|
||||
});
|
||||
|
||||
it('applies custom headers with env var interpolation', () => {
|
||||
const originalEnv = process.env.HICAP_SECRET;
|
||||
process.env.HICAP_SECRET = 'test-secret-value';
|
||||
|
||||
try {
|
||||
const provider = createProvider({
|
||||
providerId: 'opengateway',
|
||||
apiKey: 'test-key',
|
||||
baseURL: 'https://opengateway.gitlawb.com/v1',
|
||||
model: 'mimo-v2.5-pro',
|
||||
});
|
||||
|
||||
expect(provider).toBeInstanceOf(OpenAIProvider);
|
||||
} finally {
|
||||
if (originalEnv === undefined) {
|
||||
delete process.env.HICAP_SECRET;
|
||||
} else {
|
||||
process.env.HICAP_SECRET = originalEnv;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('anthropic-compatible transport is not yet supported', () => {
|
||||
const anthropicDef: ProviderDefinition = {
|
||||
id: 'anthropic-test',
|
||||
name: 'Anthropic Test',
|
||||
kind: 'provider',
|
||||
description: 'Test anthropic-compatible provider',
|
||||
baseURL: 'https://api.anthropic.com',
|
||||
defaultModel: 'claude-sonnet-4',
|
||||
transportConfig: {
|
||||
kind: 'anthropic-compatible',
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: implement anthropic-compatible transport
|
||||
expect(isProviderRuntimeSupported(anthropicDef)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects unsupported transport kinds', () => {
|
||||
const nativeDefinition: ProviderDefinition = {
|
||||
id: 'native-test',
|
||||
name: 'Native Test',
|
||||
kind: 'provider',
|
||||
description: 'Unsupported native test provider',
|
||||
baseURL: 'https://example.com',
|
||||
defaultModel: 'native-model',
|
||||
transportConfig: {
|
||||
kind: 'gemini-native' as any,
|
||||
},
|
||||
};
|
||||
|
||||
expect(isProviderRuntimeSupported(nativeDefinition)).toBe(false);
|
||||
});
|
||||
|
||||
it('uses catalog defaults when provider commands only return a provider id', () => {
|
||||
const config = resolveProviderCommandConfig({
|
||||
provider_id: 'opengateway',
|
||||
api_key: 'ogw_live_test',
|
||||
});
|
||||
|
||||
expect(config).toEqual({
|
||||
providerId: 'opengateway',
|
||||
apiKey: 'ogw_live_test',
|
||||
baseURL: 'https://opengateway.gitlawb.com/v1',
|
||||
model: 'mimo-v2.5-pro',
|
||||
});
|
||||
});
|
||||
|
||||
it('parses quoted provider command arguments', () => {
|
||||
expect(parseProviderCommand('"C:\\Program Files\\Zero\\provider.exe" --json "{\\"provider_id\\":\\"opengateway\\"}"')).toEqual([
|
||||
'C:\\Program Files\\Zero\\provider.exe',
|
||||
'--json',
|
||||
'{"provider_id":"opengateway"}',
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects unterminated provider command quotes', () => {
|
||||
expect(() => parseProviderCommand('provider-command "unterminated')).toThrow('Unterminated');
|
||||
});
|
||||
|
||||
it('discovers OpenAI-compatible models and merges them with static catalog models', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalConfigDir = process.env.ZERO_CONFIG_DIR;
|
||||
const testConfigDir = testCacheDir('provider-catalog');
|
||||
const requests: Array<{ input: RequestInfo | URL; init?: RequestInit }> = [];
|
||||
|
||||
cleanupTestCacheDir(testConfigDir);
|
||||
process.env.ZERO_CONFIG_DIR = testConfigDir;
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
requests.push({ input, init });
|
||||
return new Response(JSON.stringify({
|
||||
data: [
|
||||
{ id: 'discovered-model' },
|
||||
{ id: 'mimo-v2.5-pro' },
|
||||
],
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await discoverModelsForProvider('opengateway', {
|
||||
apiKey: 'ogw_live_test',
|
||||
forceRefresh: true,
|
||||
});
|
||||
|
||||
expect(result?.source).toBe('network');
|
||||
expect(result?.models.map((model) => model.apiName)).toContain('mimo-v2.5-pro');
|
||||
expect(result?.models.map((model) => model.apiName)).toContain('gpt-4o');
|
||||
expect(result?.models.map((model) => model.apiName)).toContain('discovered-model');
|
||||
expect(String(requests[0]?.input)).toBe('https://opengateway.gitlawb.com/v1/models');
|
||||
expect((requests[0]?.init?.headers as Record<string, string>)?.authorization).toBe('Bearer ogw_live_test');
|
||||
expect(readFileSync(join(testConfigDir, 'model-cache.json'), 'utf-8')).not.toContain('ogw_live_test');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (originalConfigDir === undefined) {
|
||||
delete process.env.ZERO_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.ZERO_CONFIG_DIR = originalConfigDir;
|
||||
}
|
||||
cleanupTestCacheDir(testConfigDir);
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves effective transport config with per-model overrides', () => {
|
||||
const opengateway = getProviderDefinition('opengateway');
|
||||
expect(opengateway).toBeDefined();
|
||||
|
||||
const gpt4oModel = {
|
||||
id: 'gpt-4o-og',
|
||||
apiName: 'gpt-4o',
|
||||
globalModelId: 'gpt-4o',
|
||||
contextWindow: 32000,
|
||||
maxOutputTokens: 4096,
|
||||
transportOverrides: {
|
||||
maxTokensField: 'max_tokens' as const,
|
||||
removeBodyFields: ['store'],
|
||||
},
|
||||
};
|
||||
|
||||
const effective = resolveEffectiveTransportConfig(opengateway!, gpt4oModel);
|
||||
expect(effective).toBeDefined();
|
||||
// Per-model override should take precedence
|
||||
expect(effective?.maxTokensField).toBe('max_tokens');
|
||||
// removeBodyFields should be unioned (definition + model)
|
||||
expect(effective?.removeBodyFields).toContain('store');
|
||||
expect(effective?.removeBodyFields).toContain('stream_options');
|
||||
});
|
||||
|
||||
it('first-party providers auto-expose their owned global models', () => {
|
||||
const openai = getProviderDefinition('openai');
|
||||
expect(openai).toBeDefined();
|
||||
expect(openai?.isFirstParty).toBe(true);
|
||||
|
||||
const models = getModelsForProvider(openai!);
|
||||
const modelIds = models.map((m) => m.apiName);
|
||||
|
||||
expect(modelIds).toContain('gpt-4o');
|
||||
expect(modelIds).toContain('gpt-4o-mini');
|
||||
expect(modelIds).toContain('o1');
|
||||
expect(modelIds).toContain('o1-mini');
|
||||
expect(modelIds).not.toContain('claude-sonnet-4');
|
||||
expect(modelIds).not.toContain('deepseek-v3');
|
||||
|
||||
const gpt4o = models.find((m) => m.apiName === 'gpt-4o');
|
||||
expect(gpt4o?.providerId).toBe('openai');
|
||||
expect(gpt4o?.providerKind).toBe('provider');
|
||||
});
|
||||
|
||||
it('non-first-party providers only show their own catalog models', () => {
|
||||
const opengateway = getProviderDefinition('opengateway');
|
||||
expect(opengateway?.isFirstParty).toBeUndefined();
|
||||
|
||||
const models = getModelsForProvider(opengateway!);
|
||||
const modelIds = models.map((m) => m.apiName);
|
||||
|
||||
expect(modelIds).toContain('mimo-v2.5-pro');
|
||||
expect(modelIds).toContain('gpt-4o'); // via globalModelId override
|
||||
expect(modelIds).not.toContain('claude-sonnet-4');
|
||||
expect(modelIds).not.toContain('deepseek-v3');
|
||||
});
|
||||
|
||||
it('returns definition-level transport config when no model overrides exist', () => {
|
||||
const opengateway = getProviderDefinition('opengateway');
|
||||
const effective = resolveEffectiveTransportConfig(opengateway!, undefined);
|
||||
|
||||
expect(effective?.maxTokensField).toBe('max_completion_tokens');
|
||||
expect(effective?.removeBodyFields).toEqual(['store', 'stream_options']);
|
||||
});
|
||||
|
||||
it('does not cache first-time discovery failures as fresh results', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalConfigDir = process.env.ZERO_CONFIG_DIR;
|
||||
const testConfigDir = testCacheDir('provider-catalog-failure');
|
||||
let requestCount = 0;
|
||||
|
||||
cleanupTestCacheDir(testConfigDir);
|
||||
process.env.ZERO_CONFIG_DIR = testConfigDir;
|
||||
|
||||
globalThis.fetch = (async () => {
|
||||
requestCount += 1;
|
||||
return new Response(JSON.stringify({ error: 'temporary failure' }), {
|
||||
status: 503,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const first = await discoverModelsForProvider('opengateway', {
|
||||
apiKey: 'ogw_live_test',
|
||||
});
|
||||
const second = await discoverModelsForProvider('opengateway', {
|
||||
apiKey: 'ogw_live_test',
|
||||
});
|
||||
|
||||
expect(first?.source).toBe('error');
|
||||
expect(second?.source).toBe('error');
|
||||
expect(requestCount).toBe(2);
|
||||
expect(existsSync(join(testConfigDir, 'model-cache.json'))).toBe(false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (originalConfigDir === undefined) {
|
||||
delete process.env.ZERO_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.ZERO_CONFIG_DIR = originalConfigDir;
|
||||
}
|
||||
cleanupTestCacheDir(testConfigDir);
|
||||
}
|
||||
});
|
||||
|
||||
it('discovers Ollama models via /api/tags', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalConfigDir = process.env.ZERO_CONFIG_DIR;
|
||||
const testConfigDir = testCacheDir('ollama-discovery');
|
||||
|
||||
cleanupTestCacheDir(testConfigDir);
|
||||
process.env.ZERO_CONFIG_DIR = testConfigDir;
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url.includes('/api/tags')) {
|
||||
return new Response(JSON.stringify({
|
||||
models: [
|
||||
{ name: 'llama3.2:latest' },
|
||||
{ name: 'mistral:7b' },
|
||||
],
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
return new Response('not found', { status: 404 });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await discoverModelsForProvider('ollama', {
|
||||
forceRefresh: true,
|
||||
});
|
||||
|
||||
expect(result?.source).toBe('network');
|
||||
expect(result?.models.map((m) => m.apiName)).toContain('llama3.2:latest');
|
||||
expect(result?.models.map((m) => m.apiName)).toContain('mistral:7b');
|
||||
// Ollama models should be tagged as local tier
|
||||
const llama = result?.models.find((m) => m.apiName === 'llama3.2:latest');
|
||||
expect(llama?.tier).toBe('local');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (originalConfigDir === undefined) {
|
||||
delete process.env.ZERO_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.ZERO_CONFIG_DIR = originalConfigDir;
|
||||
}
|
||||
cleanupTestCacheDir(testConfigDir);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns stale-cache on network failure when cache exists', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalConfigDir = process.env.ZERO_CONFIG_DIR;
|
||||
const testConfigDir = testCacheDir('stale-cache-recovery');
|
||||
let fetchCount = 0;
|
||||
|
||||
cleanupTestCacheDir(testConfigDir);
|
||||
process.env.ZERO_CONFIG_DIR = testConfigDir;
|
||||
|
||||
// First call succeeds and populates cache
|
||||
globalThis.fetch = (async () => {
|
||||
fetchCount++;
|
||||
return new Response(JSON.stringify({
|
||||
data: [{ id: 'cached-model' }],
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
// Populate cache
|
||||
await discoverModelsForProvider('opengateway', {
|
||||
apiKey: 'ogw_test',
|
||||
forceRefresh: true,
|
||||
});
|
||||
|
||||
// Now make fetch fail
|
||||
globalThis.fetch = (async () => {
|
||||
fetchCount++;
|
||||
return new Response(JSON.stringify({ error: 'down' }), {
|
||||
status: 503,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
// Should return stale-cache, not error
|
||||
const result = await discoverModelsForProvider('opengateway', {
|
||||
apiKey: 'ogw_test',
|
||||
forceRefresh: true,
|
||||
});
|
||||
|
||||
expect(result?.source).toBe('stale-cache');
|
||||
expect(result?.stale).toBe(true);
|
||||
// Should still have the cached models
|
||||
expect(result?.models.map((m) => m.apiName)).toContain('cached-model');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (originalConfigDir === undefined) {
|
||||
delete process.env.ZERO_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.ZERO_CONFIG_DIR = originalConfigDir;
|
||||
}
|
||||
cleanupTestCacheDir(testConfigDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import { ToolRegistry } from '../src/tools/registry';
|
||||
import type { Tool } from '../src/tools/types';
|
||||
|
||||
function makeTool(name: string): Tool {
|
||||
return {
|
||||
name,
|
||||
description: `tool ${name}`,
|
||||
parameters: z.object({ x: z.string() }),
|
||||
async execute() {
|
||||
return `ran ${name}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('ToolRegistry', () => {
|
||||
it('registers and retrieves a tool by name', () => {
|
||||
const registry = new ToolRegistry();
|
||||
const tool = makeTool('alpha');
|
||||
registry.register(tool);
|
||||
|
||||
expect(registry.get('alpha')).toBe(tool);
|
||||
});
|
||||
|
||||
it('returns undefined for an unknown tool', () => {
|
||||
const registry = new ToolRegistry();
|
||||
expect(registry.get('missing')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('getAll returns every registered tool', () => {
|
||||
const registry = new ToolRegistry();
|
||||
registry.register(makeTool('a'));
|
||||
registry.register(makeTool('b'));
|
||||
|
||||
const names = registry.getAll().map((t) => t.name).sort();
|
||||
expect(names).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('re-registering a name overwrites the previous tool', () => {
|
||||
const registry = new ToolRegistry();
|
||||
registry.register(makeTool('dup'));
|
||||
const second = makeTool('dup');
|
||||
registry.register(second);
|
||||
|
||||
expect(registry.getAll()).toHaveLength(1);
|
||||
expect(registry.get('dup')).toBe(second);
|
||||
});
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import { toolRegistry } from '../src/tools';
|
||||
|
||||
// Guards the regression where loop.ts imported a missing `zod-to-json-schema`
|
||||
// package instead of zod v4's built-in `z.toJSONSchema`.
|
||||
describe('tool parameter -> JSON Schema conversion', () => {
|
||||
it('every registered tool converts to a valid object JSON Schema', () => {
|
||||
const tools = toolRegistry.getAll();
|
||||
expect(tools.length).toBeGreaterThan(0);
|
||||
|
||||
for (const tool of tools) {
|
||||
const schema = z.toJSONSchema(tool.parameters, { target: 'draft-7' }) as any;
|
||||
expect(schema.type).toBe('object');
|
||||
expect(schema.properties).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('produces the shape the agent loop sends to providers', () => {
|
||||
const planTool = toolRegistry.get('update_plan');
|
||||
expect(planTool).toBeDefined();
|
||||
|
||||
const schema = z.toJSONSchema(planTool!.parameters, { target: 'draft-7' }) as any;
|
||||
expect(schema.properties.plan.type).toBe('array');
|
||||
expect(schema.required).toContain('plan');
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user