Compare commits
455 Commits
v2.12.1
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31dfef85b8 | ||
|
|
0ce87085db | ||
|
|
753fd809b5 | ||
|
|
6d99b5c1fc | ||
|
|
255f535a50 | ||
|
|
2206d68523 | ||
|
|
b643dd4f19 | ||
|
|
0ed1d183d4 | ||
|
|
d13e8411f0 | ||
|
|
36b665ed89 | ||
|
|
987ae46841 | ||
|
|
74e9834797 | ||
|
|
5657c3aa28 | ||
|
|
c433e7397e | ||
|
|
dec35d28a7 | ||
|
|
1f493cc921 | ||
|
|
ef7276a46a | ||
|
|
a2f64e18f3 | ||
|
|
e37493a6db | ||
|
|
c0be58b2ce | ||
|
|
beab015512 | ||
|
|
638842966f | ||
|
|
1b6037bbdf | ||
|
|
360984abec | ||
|
|
9a273a4ad8 | ||
|
|
b7b5737f9c | ||
|
|
fa9bf4590c | ||
|
|
b4fa31a47a | ||
|
|
ec2cf22449 | ||
|
|
f6d4201d7d | ||
|
|
5cb5dbef42 | ||
|
|
7d796738a2 | ||
|
|
0823dbe4d4 | ||
|
|
8391b8a7a5 | ||
|
|
903a1534a4 | ||
|
|
bbaf78ac70 | ||
|
|
79dab37569 | ||
|
|
374083fa0e | ||
|
|
0b9cf32190 | ||
|
|
a5097a4efe | ||
|
|
15b91f50f6 | ||
|
|
30f3dd2646 | ||
|
|
cf7b23be5e | ||
|
|
0c000596dc | ||
|
|
5ee8996a39 | ||
|
|
7cd59e9c0a | ||
|
|
cb6f1c9f75 | ||
|
|
eeb7eb2be2 | ||
|
|
fd6a33b88f | ||
|
|
e22960d862 | ||
|
|
ea1d604b72 | ||
|
|
d3e3371a77 | ||
|
|
188bbef018 | ||
|
|
6008388a4e | ||
|
|
8402b550df | ||
|
|
880e29e883 | ||
|
|
47e64a4a92 | ||
|
|
e23ce11df9 | ||
|
|
f1cdb3bce1 | ||
|
|
83cbc56709 | ||
|
|
ede9abceb3 | ||
|
|
27ef9fa8df | ||
|
|
333db56172 | ||
|
|
1ecb2bafdf | ||
|
|
d00c2e7439 | ||
|
|
8d545723dc | ||
|
|
e737477fbe | ||
|
|
aa859f8cdd | ||
|
|
c282244439 | ||
|
|
75925d5433 | ||
|
|
c7ca608b38 | ||
|
|
b933992e36 | ||
|
|
bf28b3e711 | ||
|
|
9363324e0e | ||
|
|
8e02cab307 | ||
|
|
f888da8848 | ||
|
|
9fb284d4b5 | ||
|
|
584aecf266 | ||
|
|
848b2e3faa | ||
|
|
33666245d8 | ||
|
|
7b9e20f2fa | ||
|
|
e36385e671 | ||
|
|
ca2f8059a6 | ||
|
|
f9b9b59658 | ||
|
|
837176d947 | ||
|
|
8e2410f1a0 | ||
|
|
bad98e88ec | ||
|
|
e264cd5078 | ||
|
|
0230e71bc6 | ||
|
|
c9f762f980 | ||
|
|
f658544cd6 | ||
|
|
396043a122 | ||
|
|
9854e9f6e5 | ||
|
|
48167a6920 | ||
|
|
207a39b17a | ||
|
|
5de3d4fb7d | ||
|
|
7a9e604b2d | ||
|
|
6670754efe | ||
|
|
37d4aec4d0 | ||
|
|
c38b078c12 | ||
|
|
5e44996746 | ||
|
|
9a152bcebb | ||
|
|
c67ca8275e | ||
|
|
72a3975799 | ||
|
|
747d824cbf | ||
|
|
b8a8cc95e2 | ||
|
|
96630bb0ee | ||
|
|
15e3e16bf2 | ||
|
|
68699330b8 | ||
|
|
49384fa804 | ||
|
|
b056e775f5 | ||
|
|
9bed597e46 | ||
|
|
74f355322a | ||
|
|
1ea304513c | ||
|
|
e925ed0009 | ||
|
|
fc5c2baac0 | ||
|
|
b5bd837025 | ||
|
|
abc4a34ce4 | ||
|
|
7168c2d904 | ||
|
|
7050d447cd | ||
|
|
d6499cbe31 | ||
|
|
a38dc28e40 | ||
|
|
4ac0fa7bb0 | ||
|
|
c1246f61d1 | ||
|
|
89fa9ff167 | ||
|
|
03871262b2 | ||
|
|
4c22d6de76 | ||
|
|
1dd369fda5 | ||
|
|
84e97ba900 | ||
|
|
ef65f405e8 | ||
|
|
3de559ff87 | ||
|
|
acb16bcb27 | ||
|
|
9995b680f7 | ||
|
|
41fa37eb11 | ||
|
|
70bca4a7a6 | ||
|
|
b1f19cbfbd | ||
|
|
8395a6eaac | ||
|
|
abd1ec1092 | ||
|
|
5a8d9f09d9 | ||
|
|
2c4730f094 | ||
|
|
951df07c0f | ||
|
|
4c49299a93 | ||
|
|
00508e9959 | ||
|
|
c9ef648c60 | ||
|
|
8a9ebe1012 | ||
|
|
014bdaeec2 | ||
|
|
570b51d07b | ||
|
|
a91b05d9c6 | ||
|
|
4a892a9809 | ||
|
|
4d4966362f | ||
|
|
0c21c72e05 | ||
|
|
caf50fc4c9 | ||
|
|
3801e42ccb | ||
|
|
306dab41ad | ||
|
|
9f040e020f | ||
|
|
25dbcfe200 | ||
|
|
47a641c415 | ||
|
|
5c4f4fc655 | ||
|
|
2e1b467de4 | ||
|
|
e180d295bb | ||
|
|
93e59da9d6 | ||
|
|
358bd8d7fa | ||
|
|
78d67582d6 | ||
|
|
54575ad259 | ||
|
|
045fa79d92 | ||
|
|
4d966ec99b | ||
|
|
5d99e9ab64 | ||
|
|
129388387b | ||
|
|
c196db2a0e | ||
|
|
6ded689d08 | ||
|
|
45d660176e | ||
|
|
ffbab8f316 | ||
|
|
e203130ed8 | ||
|
|
0631865c16 | ||
|
|
2b036e7476 | ||
|
|
e6a572824c | ||
|
|
4d76f37bfe | ||
|
|
84e1ee09f0 | ||
|
|
3d5319a72d | ||
|
|
75eb82ea32 | ||
|
|
325ce1212b | ||
|
|
66f8946ff1 | ||
|
|
22619d137e | ||
|
|
000a61c961 | ||
|
|
9f07aae0a1 | ||
|
|
d7326e1eeb | ||
|
|
ac80e268d0 | ||
|
|
4d9c664694 | ||
|
|
34863a77ef | ||
|
|
a1b881f38e | ||
|
|
2b556e0f6c | ||
|
|
444b7ce991 | ||
|
|
31c5951dfc | ||
|
|
84dcb32608 | ||
|
|
f2dc61f1a3 | ||
|
|
d99d79aebf | ||
|
|
c78661b1f2 | ||
|
|
1f47ea9937 | ||
|
|
3920f843af | ||
|
|
42de7c3e40 | ||
|
|
1a3fb0035b | ||
|
|
4a722df8be | ||
|
|
6d4cebd17f | ||
|
|
3afdaadaad | ||
|
|
2042a29877 | ||
|
|
c6fb5e58c8 | ||
|
|
2dd9cf7b88 | ||
|
|
d68f90f796 | ||
|
|
e6e25e6d93 | ||
|
|
0c996669b0 | ||
|
|
8916a32ea0 | ||
|
|
cddbd0d945 | ||
|
|
9e8173593f | ||
|
|
d9ab6ab99b | ||
|
|
cf53b2b51a | ||
|
|
cf66a86e16 | ||
|
|
d2a5f47f1c | ||
|
|
9e98cef182 | ||
|
|
2b8853cbac | ||
|
|
f9fce50144 | ||
|
|
d1ffecd887 | ||
|
|
d9aabb33fd | ||
|
|
79bd75b3db | ||
|
|
14dc8ee8df | ||
|
|
6ea63706db | ||
|
|
864656475a | ||
|
|
9048b616e3 | ||
|
|
4fe4fb1adf | ||
|
|
04ae3642d9 | ||
|
|
70d604e0e4 | ||
|
|
8d65748ad3 | ||
|
|
2314a0d371 | ||
|
|
e620b546ab | ||
|
|
0fada4d0fc | ||
|
|
c79235744b | ||
|
|
6bbe69a72a | ||
|
|
5b8c6c70b2 | ||
|
|
179f57fa96 | ||
|
|
f83b22c4de | ||
|
|
965bb2dd10 | ||
|
|
f9dca8d877 | ||
|
|
91c490a358 | ||
|
|
aa44c54068 | ||
|
|
945b090b1b | ||
|
|
05cd133e2a | ||
|
|
8ed3f7e03b | ||
|
|
42e5b5bf44 | ||
|
|
8320c7cf2d | ||
|
|
612e9b3e03 | ||
|
|
f27e93bcc8 | ||
|
|
10a5bab94d | ||
|
|
f615b012e7 | ||
|
|
0809de8262 | ||
|
|
24bdc7ea77 | ||
|
|
8ff159bc2e | ||
|
|
49b0b5e085 | ||
|
|
1132be370c | ||
|
|
f240dbb7ee | ||
|
|
571810f1e7 | ||
|
|
83d958580f | ||
|
|
f1e7b6ab1e | ||
|
|
1a5fdb3338 | ||
|
|
c29e6f0213 | ||
|
|
1bbb61b1c2 | ||
|
|
2a95c91cab | ||
|
|
307d583ad6 | ||
|
|
ce5315fbd0 | ||
|
|
1c262a65fe | ||
|
|
0c127879c0 | ||
|
|
65a6a702ec | ||
|
|
60f4cd4fac | ||
|
|
5f823b0f8e | ||
|
|
e35a488cf6 | ||
|
|
adb1a9fcb9 | ||
|
|
9bfed238b9 | ||
|
|
61abd553fb | ||
|
|
6425d9d97e | ||
|
|
d57744905f | ||
|
|
c7ae2d7be6 | ||
|
|
358f7f439d | ||
|
|
4fde139dd8 | ||
|
|
b10703ec9a | ||
|
|
8b12257729 | ||
|
|
7536a12754 | ||
|
|
0fb765732a | ||
|
|
d4c8ec6690 | ||
|
|
d6416082a2 | ||
|
|
e6aaf57a21 | ||
|
|
5242f3daef | ||
|
|
3f2ded54ee | ||
|
|
aa5018583e | ||
|
|
185d4e1e54 | ||
|
|
79e9fd82c5 | ||
|
|
a50878df51 | ||
|
|
a2c2922d0a | ||
|
|
0581793dfe | ||
|
|
afdc25744f | ||
|
|
e9aa805c3f | ||
|
|
751ac64d39 | ||
|
|
6ef1029bc4 | ||
|
|
ace2098ca0 | ||
|
|
a86b1ffc78 | ||
|
|
a975b23beb | ||
|
|
ef0220e508 | ||
|
|
545b0a2dc8 | ||
|
|
29cee62b47 | ||
|
|
157ed642d6 | ||
|
|
768ecd928b | ||
|
|
8394926fe1 | ||
|
|
500dfaf7bf | ||
|
|
69e542c6db | ||
|
|
c559037f72 | ||
|
|
3d49ee1262 | ||
|
|
68655bf22e | ||
|
|
1570e292fb | ||
|
|
cccd159f7d | ||
|
|
a54d9b17aa | ||
|
|
b0cc1cd26a | ||
|
|
7853f1f4bf | ||
|
|
13d3dc7144 | ||
|
|
2cca1cab29 | ||
|
|
b23241ec4f | ||
|
|
7981c86613 | ||
|
|
115e46517f | ||
|
|
02c1b6cc6f | ||
|
|
677a7aed64 | ||
|
|
6f4649d92a | ||
|
|
947ebea20b | ||
|
|
57bf84536b | ||
|
|
cc4299cdea | ||
|
|
83676b36cf | ||
|
|
398075f5df | ||
|
|
d4347e829d | ||
|
|
980b685393 | ||
|
|
b5c1cfb57f | ||
|
|
cd97572d0a | ||
|
|
b9ec4c7c4a | ||
|
|
2064568124 | ||
|
|
ad44af9d15 | ||
|
|
d331b484f9 | ||
|
|
4a38e70fa8 | ||
|
|
204ea319cb | ||
|
|
a2bfb5e556 | ||
|
|
f25f7ed0f5 | ||
|
|
29dbc0f57b | ||
|
|
544212fa9c | ||
|
|
f3eed731d6 | ||
|
|
6f1cabd3f4 | ||
|
|
15571d3d95 | ||
|
|
556262e791 | ||
|
|
375e7f715d | ||
|
|
5aa0ee125d | ||
|
|
d0b3be72c5 | ||
|
|
a10903def2 | ||
|
|
dc5a24ac3e | ||
|
|
9d13c6cff1 | ||
|
|
b78e564872 | ||
|
|
c709fafa25 | ||
|
|
5914a393ad | ||
|
|
4e5b3566a2 | ||
|
|
898d3e6175 | ||
|
|
21236d88a7 | ||
|
|
ea8ca1a100 | ||
|
|
66acb0e444 | ||
|
|
f7c8763462 | ||
|
|
ee2f390bf6 | ||
|
|
ae6495dc17 | ||
|
|
b8b8d14b1c | ||
|
|
7a10b24bbd | ||
|
|
258463a146 | ||
|
|
0f890c11c2 | ||
|
|
e81002ba43 | ||
|
|
a20f011014 | ||
|
|
48174ec25a | ||
|
|
26e77a0a89 | ||
|
|
a5c71473a5 | ||
|
|
aecfc77fb6 | ||
|
|
5a4261a607 | ||
|
|
6913613398 | ||
|
|
d27a1efd94 | ||
|
|
bc05fb6671 | ||
|
|
7937d72cbf | ||
|
|
fe11ba294c | ||
|
|
6b5a8263f9 | ||
|
|
65b00c9720 | ||
|
|
5ed031db63 | ||
|
|
0553676ab0 | ||
|
|
b80b373230 | ||
|
|
f55046228f | ||
|
|
2992902283 | ||
|
|
b66c8dc1d1 | ||
|
|
8f2209a138 | ||
|
|
6c3ef65aed | ||
|
|
e1e8b24941 | ||
|
|
0d0ddefbfe | ||
|
|
09f72e2902 | ||
|
|
5f63aff01d | ||
|
|
6fd9734337 | ||
|
|
4bf853fc91 | ||
|
|
87134d3390 | ||
|
|
36c42ac92f | ||
|
|
56fe32caab | ||
|
|
09756b8ffc | ||
|
|
9ba9f906c5 | ||
|
|
ce69007fde | ||
|
|
b1f36d61a8 | ||
|
|
97e51c42dc | ||
|
|
91d2705804 | ||
|
|
6575dfcbc4 | ||
|
|
59b0e6943d | ||
|
|
9d64f213ee | ||
|
|
e572c7c321 | ||
|
|
be2adff3ef | ||
|
|
37f4c48183 | ||
|
|
a49fbeec5f | ||
|
|
7a7b16fb62 | ||
|
|
ae781f1e14 | ||
|
|
d7645a4058 | ||
|
|
16927729c7 | ||
|
|
a4ba63cd1c | ||
|
|
063db0d390 | ||
|
|
dc52395ead | ||
|
|
c8e9f90900 | ||
|
|
6fbc5ba582 | ||
|
|
fc76ea9d93 | ||
|
|
2a3b45bea5 | ||
|
|
79b80e5a2f | ||
|
|
e2cbe8c29b | ||
|
|
99c7df5640 | ||
|
|
f61e1a5f2b | ||
|
|
03c51c9321 | ||
|
|
c10994563b | ||
|
|
d188688dd8 | ||
|
|
95645effd7 | ||
|
|
00b8f622d5 | ||
|
|
967e53258c | ||
|
|
c40f562434 | ||
|
|
a9523bc607 | ||
|
|
f26bf24c33 | ||
|
|
bc65fcea7e | ||
|
|
3a8eac751e | ||
|
|
48dc8298dd | ||
|
|
8bc9d6a540 | ||
|
|
6a6e20cf5d | ||
|
|
3a5aea7f4b | ||
|
|
a4812801b4 | ||
|
|
6422ff270b | ||
|
|
3c27206777 | ||
|
|
8510a2273d | ||
|
|
a8ca3ad5fb | ||
|
|
30e0cc6ef1 | ||
|
|
f345101f91 | ||
|
|
d09c994b91 | ||
|
|
8c30974c18 | ||
|
|
c341c156ec |
129
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
129
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
name: Bug Report
|
||||
description: Report a bug or unexpected behavior in oh-my-opencode
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "needs-triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Please write your issue in English.** See our [Language Policy](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/CONTRIBUTING.md#language-policy) for details.
|
||||
|
||||
- type: checkboxes
|
||||
id: prerequisites
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
description: Please confirm the following before submitting
|
||||
options:
|
||||
- label: I have searched existing issues to avoid duplicates
|
||||
required: true
|
||||
- label: I am using the latest version of oh-my-opencode
|
||||
required: true
|
||||
- label: I have read the [documentation](https://github.com/code-yeongyu/oh-my-opencode#readme)
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Bug Description
|
||||
description: A clear and concise description of what the bug is
|
||||
placeholder: Describe the bug in detail...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
placeholder: |
|
||||
1. Configure oh-my-opencode with...
|
||||
2. Run command '...'
|
||||
3. See error...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: What did you expect to happen?
|
||||
placeholder: Describe what should happen...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual Behavior
|
||||
description: What actually happened?
|
||||
placeholder: Describe what actually happened...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: doctor
|
||||
attributes:
|
||||
label: Doctor Output
|
||||
description: |
|
||||
**Required:** Run `bunx oh-my-opencode doctor` and paste the full output below.
|
||||
This helps us diagnose your environment and configuration.
|
||||
placeholder: |
|
||||
Paste the output of: bunx oh-my-opencode doctor
|
||||
|
||||
Example:
|
||||
✓ OpenCode version: 1.0.150
|
||||
✓ oh-my-opencode version: 1.2.3
|
||||
✓ Plugin loaded successfully
|
||||
...
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Error Logs
|
||||
description: If applicable, add any error messages or logs
|
||||
placeholder: Paste error logs here...
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Configuration
|
||||
description: If relevant, share your oh-my-opencode configuration (remove sensitive data)
|
||||
placeholder: |
|
||||
{
|
||||
"agents": { ... },
|
||||
"disabled_hooks": [ ... ]
|
||||
}
|
||||
render: json
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Any other context about the problem
|
||||
placeholder: Add any other context, screenshots, or information...
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: Which operating system are you using?
|
||||
options:
|
||||
- macOS
|
||||
- Linux
|
||||
- Windows
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: opencode-version
|
||||
attributes:
|
||||
label: OpenCode Version
|
||||
description: Run `opencode --version` to get your version
|
||||
placeholder: "1.0.150"
|
||||
validations:
|
||||
required: true
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discord Community
|
||||
url: https://discord.gg/PUwSMR9XNk
|
||||
about: Join our Discord server for real-time discussions and community support
|
||||
- name: Documentation
|
||||
url: https://github.com/code-yeongyu/oh-my-opencode#readme
|
||||
about: Read the comprehensive documentation and guides
|
||||
100
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
100
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or enhancement for oh-my-opencode
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement", "needs-triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Please write your issue in English.** See our [Language Policy](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/CONTRIBUTING.md#language-policy) for details.
|
||||
|
||||
- type: checkboxes
|
||||
id: prerequisites
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
description: Please confirm the following before submitting
|
||||
options:
|
||||
- label: I have searched existing issues and discussions to avoid duplicates
|
||||
required: true
|
||||
- label: This feature request is specific to oh-my-opencode (not OpenCode core)
|
||||
required: true
|
||||
- label: I have read the [documentation](https://github.com/code-yeongyu/oh-my-opencode#readme)
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem Description
|
||||
description: What problem does this feature solve? What's the use case?
|
||||
placeholder: |
|
||||
Describe the problem or limitation you're experiencing...
|
||||
Example: "As a user, I find it difficult to..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: Describe how you'd like this feature to work
|
||||
placeholder: |
|
||||
Describe your proposed solution in detail...
|
||||
Example: "Add a new hook that..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: Have you considered any alternative solutions or workarounds?
|
||||
placeholder: |
|
||||
Describe any alternative solutions you've considered...
|
||||
Example: "I tried using X but it didn't work because..."
|
||||
|
||||
- type: textarea
|
||||
id: doctor
|
||||
attributes:
|
||||
label: Doctor Output (Optional)
|
||||
description: |
|
||||
If relevant to your feature request, run `bunx oh-my-opencode doctor` and paste the output.
|
||||
This helps us understand your environment.
|
||||
placeholder: |
|
||||
Paste the output of: bunx oh-my-opencode doctor
|
||||
(Optional for feature requests)
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: Any other context, mockups, or examples
|
||||
placeholder: |
|
||||
Add any other context, screenshots, code examples, or links...
|
||||
Examples from other tools/projects are helpful!
|
||||
|
||||
- type: dropdown
|
||||
id: feature-type
|
||||
attributes:
|
||||
label: Feature Type
|
||||
description: What type of feature is this?
|
||||
options:
|
||||
- New Agent
|
||||
- New Hook
|
||||
- New Tool
|
||||
- New MCP Integration
|
||||
- Configuration Option
|
||||
- Documentation
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution
|
||||
attributes:
|
||||
label: Contribution
|
||||
description: Are you willing to contribute to this feature?
|
||||
options:
|
||||
- label: I'm willing to submit a PR for this feature
|
||||
- label: I can help with testing
|
||||
- label: I can help with documentation
|
||||
83
.github/ISSUE_TEMPLATE/general.yml
vendored
Normal file
83
.github/ISSUE_TEMPLATE/general.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
name: Question or Discussion
|
||||
description: Ask a question or start a discussion about oh-my-opencode
|
||||
title: "[Question]: "
|
||||
labels: ["question", "needs-triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Please write your issue in English.** See our [Language Policy](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/CONTRIBUTING.md#language-policy) for details.
|
||||
|
||||
- type: checkboxes
|
||||
id: prerequisites
|
||||
attributes:
|
||||
label: Prerequisites
|
||||
description: Please confirm the following before submitting
|
||||
options:
|
||||
- label: I have searched existing issues and discussions
|
||||
required: true
|
||||
- label: I have read the [documentation](https://github.com/code-yeongyu/oh-my-opencode#readme)
|
||||
required: true
|
||||
- label: This is a question (not a bug report or feature request)
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question
|
||||
description: What would you like to know or discuss?
|
||||
placeholder: |
|
||||
Ask your question in detail...
|
||||
|
||||
Examples:
|
||||
- How do I configure agent X to do Y?
|
||||
- What's the best practice for Z?
|
||||
- Why does feature A work differently than B?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Context
|
||||
description: Provide any relevant context or background
|
||||
placeholder: |
|
||||
What have you tried so far?
|
||||
What's your use case?
|
||||
Any relevant configuration or setup details?
|
||||
|
||||
- type: textarea
|
||||
id: doctor
|
||||
attributes:
|
||||
label: Doctor Output (Optional)
|
||||
description: |
|
||||
If your question is about configuration or setup, run `bunx oh-my-opencode doctor` and paste the output.
|
||||
placeholder: |
|
||||
Paste the output of: bunx oh-my-opencode doctor
|
||||
(Optional for questions)
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: Question Category
|
||||
description: What is your question about?
|
||||
options:
|
||||
- Configuration
|
||||
- Agent Usage
|
||||
- Hook Behavior
|
||||
- Tool Usage
|
||||
- Installation/Setup
|
||||
- Best Practices
|
||||
- Performance
|
||||
- Integration
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Any other information that might be helpful
|
||||
placeholder: Links, screenshots, examples, etc.
|
||||
BIN
.github/assets/orchestrator-sisyphus.png
vendored
BIN
.github/assets/orchestrator-sisyphus.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 984 KiB After Width: | Height: | Size: 182 KiB |
BIN
.github/assets/sisyphuslabs.png
vendored
Normal file
BIN
.github/assets/sisyphuslabs.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches: [master, dev]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches: [dev]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
22
.github/workflows/lint-workflows.yml
vendored
Normal file
22
.github/workflows/lint-workflows.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Lint Workflows
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
|
||||
jobs:
|
||||
actionlint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install actionlint
|
||||
run: |
|
||||
bash <(curl -sSL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.10/scripts/download-actionlint.bash)
|
||||
|
||||
- name: Run actionlint
|
||||
run: ./actionlint -color -shellcheck=""
|
||||
28
.github/workflows/publish.yml
vendored
28
.github/workflows/publish.yml
vendored
@@ -8,12 +8,13 @@ on:
|
||||
description: "Bump major, minor, or patch"
|
||||
required: true
|
||||
type: choice
|
||||
default: patch
|
||||
options:
|
||||
- major
|
||||
- minor
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
version:
|
||||
description: "Override version (optional)"
|
||||
description: "Override version (e.g., 3.0.0-beta.6 for beta release). Takes precedence over bump."
|
||||
required: false
|
||||
type: string
|
||||
|
||||
@@ -76,6 +77,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Upgrade npm for OIDC trusted publishing
|
||||
run: npm install -g npm@latest
|
||||
@@ -104,13 +106,16 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
echo "=== Running bun build (main) ==="
|
||||
bun build src/index.ts src/google-auth.ts --outdir dist --target bun --format esm --external @ast-grep/napi
|
||||
bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi
|
||||
echo "=== Running bun build (CLI) ==="
|
||||
bun build src/cli/index.ts --outdir dist/cli --target bun --format esm
|
||||
bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external @ast-grep/napi
|
||||
echo "=== Running tsc ==="
|
||||
tsc --emitDeclarationOnly
|
||||
bunx tsc --emitDeclarationOnly
|
||||
echo "=== Running build:schema ==="
|
||||
bun run build:schema
|
||||
|
||||
- name: Build platform binaries
|
||||
run: bun run build:binaries
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
@@ -120,6 +125,13 @@ jobs:
|
||||
ls -la dist/cli/
|
||||
test -f dist/index.js || (echo "ERROR: dist/index.js not found!" && exit 1)
|
||||
test -f dist/cli/index.js || (echo "ERROR: dist/cli/index.js not found!" && exit 1)
|
||||
echo "=== Platform binaries ==="
|
||||
for platform in darwin-arm64 darwin-x64 linux-x64 linux-arm64 linux-x64-musl linux-arm64-musl; do
|
||||
test -f "packages/${platform}/bin/oh-my-opencode" || (echo "ERROR: packages/${platform}/bin/oh-my-opencode not found!" && exit 1)
|
||||
echo "✓ packages/${platform}/bin/oh-my-opencode"
|
||||
done
|
||||
test -f "packages/windows-x64/bin/oh-my-opencode.exe" || (echo "ERROR: packages/windows-x64/bin/oh-my-opencode.exe not found!" && exit 1)
|
||||
echo "✓ packages/windows-x64/bin/oh-my-opencode.exe"
|
||||
|
||||
- name: Publish
|
||||
run: bun run script/publish.ts
|
||||
@@ -136,10 +148,12 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Merge to master
|
||||
continue-on-error: true
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
VERSION=$(jq -r '.version' package.json)
|
||||
git stash --include-untracked || true
|
||||
git checkout master
|
||||
git reset --hard "v${VERSION}"
|
||||
git push -f origin master
|
||||
git push -f origin master || echo "::warning::Failed to push to master. This can happen when workflow files changed. Manually sync master: git checkout master && git reset --hard v${VERSION} && git push -f"
|
||||
|
||||
154
.github/workflows/sisyphus-agent.yml
vendored
154
.github/workflows/sisyphus-agent.yml
vendored
@@ -15,13 +15,13 @@ jobs:
|
||||
agent:
|
||||
runs-on: ubuntu-latest
|
||||
# @sisyphus-dev-ai mention only (maintainers, exclude self)
|
||||
if: |
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(contains(github.event.comment.body, '@sisyphus-dev-ai') &&
|
||||
github.event.comment.user.login != 'sisyphus-dev-ai' &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association))
|
||||
(github.event_name == 'issue_comment' &&
|
||||
contains(github.event.comment.body || '', '@sisyphus-dev-ai') &&
|
||||
(github.event.comment.user.login || '') != 'sisyphus-dev-ai' &&
|
||||
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association || ''))
|
||||
|
||||
# Minimal default GITHUB_TOKEN permissions
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -89,21 +89,21 @@ jobs:
|
||||
echo "Installing OpenCode..."
|
||||
curl -fsSL https://opencode.ai/install -o /tmp/opencode-install.sh
|
||||
|
||||
# Try default installer first, fallback to pinned version if it fails
|
||||
# Try default installer first, fallback to re-download if it fails
|
||||
if file /tmp/opencode-install.sh | grep -q "shell script\|text"; then
|
||||
if ! bash /tmp/opencode-install.sh 2>&1; then
|
||||
echo "Default installer failed, trying with pinned version..."
|
||||
bash /tmp/opencode-install.sh --version 1.0.204
|
||||
echo "Default installer failed, trying direct install..."
|
||||
bash <(curl -fsSL https://opencode.ai/install)
|
||||
fi
|
||||
else
|
||||
echo "Download corrupted, trying direct install with pinned version..."
|
||||
bash <(curl -fsSL https://opencode.ai/install) --version 1.0.204
|
||||
echo "Download corrupted, trying direct install..."
|
||||
bash <(curl -fsSL https://opencode.ai/install)
|
||||
fi
|
||||
fi
|
||||
opencode --version
|
||||
|
||||
# Run local oh-my-opencode install (uses built dist)
|
||||
bun run dist/cli/index.js install --no-tui --claude=max20 --chatgpt=no --gemini=no
|
||||
bun run dist/cli/index.js install --no-tui --claude=max20 --chatgpt=no --gemini=no --copilot=no
|
||||
|
||||
# Override plugin to use local file reference
|
||||
OPENCODE_JSON=~/.config/opencode/opencode.json
|
||||
@@ -156,6 +156,71 @@ jobs:
|
||||
|
||||
OMO_JSON=~/.config/opencode/oh-my-opencode.json
|
||||
PROMPT_APPEND=$(cat << 'PROMPT_EOF'
|
||||
<ultrawork-mode>
|
||||
[CODE RED] Maximum precision required. Ultrathink before acting.
|
||||
|
||||
YOU MUST LEVERAGE ALL AVAILABLE AGENTS TO THEIR FULLEST POTENTIAL.
|
||||
TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
|
||||
|
||||
## AGENT UTILIZATION PRINCIPLES (by capability, not by name)
|
||||
- **Codebase Exploration**: Spawn exploration agents using BACKGROUND TASKS for file patterns, internal implementations, project structure
|
||||
- **Documentation & References**: Use librarian-type agents via BACKGROUND TASKS for API references, examples, external library docs
|
||||
- **Planning & Strategy**: For implementation tasks, spawn a dedicated planning agent for work breakdown (not needed for simple questions/investigations)
|
||||
- **High-IQ Reasoning**: Leverage specialized agents for architecture decisions, code review, strategic planning
|
||||
- **Frontend/UI Tasks**: Delegate to UI-specialized agents for design and implementation
|
||||
|
||||
## EXECUTION RULES
|
||||
- **TODO**: Track EVERY step. Mark complete IMMEDIATELY after each.
|
||||
- **PARALLEL**: Fire independent agent calls simultaneously via background_task - NEVER wait sequentially.
|
||||
- **BACKGROUND FIRST**: Use background_task for exploration/research agents (10+ concurrent if needed).
|
||||
- **VERIFY**: Re-read request after completion. Check ALL requirements met before reporting done.
|
||||
- **DELEGATE**: Don't do everything yourself - orchestrate specialized agents for their strengths.
|
||||
|
||||
## WORKFLOW
|
||||
1. Analyze the request and identify required capabilities
|
||||
2. Spawn exploration/librarian agents via background_task in PARALLEL (10+ if needed)
|
||||
3. Always Use Plan agent with gathered context to create detailed work breakdown
|
||||
4. Execute with continuous verification against original requirements
|
||||
|
||||
## TDD (if test infrastructure exists)
|
||||
|
||||
1. Write spec (requirements)
|
||||
2. Write tests (failing)
|
||||
3. RED: tests fail
|
||||
4. Implement minimal code
|
||||
5. GREEN: tests pass
|
||||
6. Refactor if needed (must stay green)
|
||||
7. Next feature, repeat
|
||||
|
||||
## ZERO TOLERANCE FAILURES
|
||||
- **NO Scope Reduction**: Never make "demo", "skeleton", "simplified", "basic" versions - deliver FULL implementation
|
||||
- **NO MockUp Work**: When user asked you to do "port A", you must "port A", fully, 100%. No Extra feature, No reduced feature, no mock data, fully working 100% port.
|
||||
- **NO Partial Completion**: Never stop at 60-80% saying "you can extend this..." - finish 100%
|
||||
- **NO Assumed Shortcuts**: Never skip requirements you deem "optional" or "can be added later"
|
||||
- **NO Premature Stopping**: Never declare done until ALL TODOs are completed and verified
|
||||
- **NO TEST DELETION**: Never delete or skip failing tests to make the build pass. Fix the code, not the tests.
|
||||
|
||||
THE USER ASKED FOR X. DELIVER EXACTLY X. NOT A SUBSET. NOT A DEMO. NOT A STARTING POINT.
|
||||
|
||||
</ultrawork-mode>
|
||||
|
||||
---
|
||||
|
||||
|
||||
[analyze-mode]
|
||||
ANALYSIS MODE. Gather context before diving deep:
|
||||
|
||||
CONTEXT GATHERING (parallel):
|
||||
- 1-2 explore agents (codebase patterns, implementations)
|
||||
- 1-2 librarian agents (if external library involved)
|
||||
- Direct tools: Grep, AST-grep, LSP for targeted searches
|
||||
|
||||
IF COMPLEX (architecture, multi-system, debugging after 2+ failures):
|
||||
- Consult oracle for strategic guidance
|
||||
|
||||
SYNTHESIZE findings before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## GitHub Actions Environment
|
||||
|
||||
@@ -244,14 +309,17 @@ jobs:
|
||||
AUTHOR="$COMMENT_AUTHOR"
|
||||
COMMENT_ID="$COMMENT_ID_VAL"
|
||||
|
||||
# Check if PR or Issue
|
||||
if gh api "repos/$REPO/issues/${ISSUE_NUM}" | jq -e '.pull_request' > /dev/null; then
|
||||
# Check if PR or Issue and get title
|
||||
ISSUE_DATA=$(gh api "repos/$REPO/issues/${ISSUE_NUM}")
|
||||
TITLE=$(echo "$ISSUE_DATA" | jq -r '.title')
|
||||
if echo "$ISSUE_DATA" | jq -e '.pull_request' > /dev/null; then
|
||||
echo "type=pr" >> $GITHUB_OUTPUT
|
||||
echo "number=${ISSUE_NUM}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "type=issue" >> $GITHUB_OUTPUT
|
||||
echo "number=${ISSUE_NUM}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "title=${TITLE}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
echo "comment<<EOF" >> $GITHUB_OUTPUT
|
||||
@@ -297,15 +365,32 @@ jobs:
|
||||
COMMENT_AUTHOR: ${{ steps.context.outputs.author }}
|
||||
CONTEXT_TYPE: ${{ steps.context.outputs.type }}
|
||||
CONTEXT_NUMBER: ${{ steps.context.outputs.number }}
|
||||
CONTEXT_TITLE: ${{ steps.context.outputs.title }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
export PATH="$HOME/.opencode/bin:$PATH"
|
||||
|
||||
PROMPT=$(cat <<'PROMPT_EOF'
|
||||
[analyze-mode]
|
||||
ANALYSIS MODE. Gather context before diving deep:
|
||||
|
||||
CONTEXT GATHERING (parallel):
|
||||
- 1-2 explore agents (codebase patterns, implementations)
|
||||
- 1-2 librarian agents (if external library involved)
|
||||
- Direct tools: Grep, AST-grep, LSP for targeted searches
|
||||
|
||||
IF COMPLEX (architecture, multi-system, debugging after 2+ failures):
|
||||
- Consult oracle for strategic guidance
|
||||
|
||||
SYNTHESIZE findings before proceeding.
|
||||
|
||||
---
|
||||
|
||||
Your username is @sisyphus-dev-ai, mentioned by @AUTHOR_PLACEHOLDER in REPO_PLACEHOLDER.
|
||||
|
||||
## Context
|
||||
- Title: TITLE_PLACEHOLDER
|
||||
- Type: TYPE_PLACEHOLDER
|
||||
- Number: #NUMBER_PLACEHOLDER
|
||||
- Repository: REPO_PLACEHOLDER
|
||||
@@ -316,8 +401,46 @@ jobs:
|
||||
|
||||
---
|
||||
|
||||
Write everything using the todo tools.
|
||||
Then investigate and satisfy the request. Only if user requested to you to work explicitely, then use plan agent to plan, todo obsessivley then create a PR to `BRANCH_PLACEHOLDER` branch.
|
||||
## CRITICAL: First Steps (MUST DO BEFORE ANYTHING ELSE)
|
||||
|
||||
### [CODE RED] MANDATORY CONTEXT READING - ZERO EXCEPTIONS
|
||||
|
||||
**YOU MUST READ ALL CONTENT. NOT SOME. NOT MOST. ALL.**
|
||||
|
||||
1. **READ FULL CONVERSATION** - Execute ALL commands below before ANY other action:
|
||||
- **Issues**: `gh issue view NUMBER_PLACEHOLDER --comments`
|
||||
- **PRs**: Use ALL THREE commands to get COMPLETE context:
|
||||
```bash
|
||||
gh pr view NUMBER_PLACEHOLDER --comments
|
||||
gh api repos/REPO_PLACEHOLDER/pulls/NUMBER_PLACEHOLDER/comments
|
||||
gh api repos/REPO_PLACEHOLDER/pulls/NUMBER_PLACEHOLDER/reviews
|
||||
```
|
||||
|
||||
**WHAT TO EXTRACT FROM THE CONVERSATION:**
|
||||
- The ORIGINAL issue/PR description (first message) - this is often the TRUE requirement
|
||||
- ALL previous attempts and their outcomes
|
||||
- ALL decisions made and their reasoning
|
||||
- ALL feedback, criticism, and rejection reasons
|
||||
- ANY linked issues, PRs, or external references
|
||||
- The EXACT ask from the user who mentioned you
|
||||
|
||||
**FAILURE TO READ EVERYTHING = GUARANTEED FAILURE**
|
||||
You WILL make wrong assumptions. You WILL repeat past mistakes. You WILL miss critical context.
|
||||
|
||||
2. **CREATE TODOS IMMEDIATELY**: Right after reading, create your todo list using todo tools.
|
||||
- First todo: "Summarize issue/PR context and requirements"
|
||||
- Break down ALL work into atomic, verifiable steps
|
||||
- **GIT WORKFLOW (MANDATORY for implementation tasks)**: ALWAYS include these final todos:
|
||||
- "Create new branch from origin/BRANCH_PLACEHOLDER (NEVER push directly to BRANCH_PLACEHOLDER)"
|
||||
- "Commit changes"
|
||||
- "Create PR to BRANCH_PLACEHOLDER branch"
|
||||
- Plan everything BEFORE starting any work
|
||||
|
||||
---
|
||||
|
||||
|
||||
Plan everything using todo tools.
|
||||
Then investigate and satisfy the request. Only if user requested to you to work explicitly, then use plan agent to plan, todo obsessively then create a PR to `BRANCH_PLACEHOLDER` branch.
|
||||
When done, report the result to the issue/PR with `gh issue comment NUMBER_PLACEHOLDER` or `gh pr comment NUMBER_PLACEHOLDER`.
|
||||
PROMPT_EOF
|
||||
)
|
||||
@@ -326,6 +449,7 @@ jobs:
|
||||
PROMPT="${PROMPT//REPO_PLACEHOLDER/$REPO_NAME}"
|
||||
PROMPT="${PROMPT//TYPE_PLACEHOLDER/$CONTEXT_TYPE}"
|
||||
PROMPT="${PROMPT//NUMBER_PLACEHOLDER/$CONTEXT_NUMBER}"
|
||||
PROMPT="${PROMPT//TITLE_PLACEHOLDER/$CONTEXT_TITLE}"
|
||||
PROMPT="${PROMPT//BRANCH_PLACEHOLDER/$DEFAULT_BRANCH}"
|
||||
PROMPT="${PROMPT//COMMENT_PLACEHOLDER/$USER_COMMENT}"
|
||||
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,9 +1,14 @@
|
||||
# Dependencies
|
||||
.sisyphus/
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Platform binaries (built, not committed)
|
||||
packages/*/bin/oh-my-opencode
|
||||
packages/*/bin/oh-my-opencode.exe
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
173
AGENTS.md
173
AGENTS.md
@@ -1,29 +1,29 @@
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
**Generated:** 2026-01-02T22:41:22+09:00
|
||||
**Commit:** d0694e5
|
||||
**Generated:** 2026-01-17T21:55:00+09:00
|
||||
**Commit:** 255f535a
|
||||
**Branch:** dev
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
OpenCode plugin: multi-model agent orchestration (Claude Opus 4.5, GPT-5.2, Gemini 3, Grok), 11 LSP tools, AST-Grep, Claude Code compatibility layer. "oh-my-zsh" for OpenCode.
|
||||
OpenCode plugin implementing multi-model agent orchestration (Claude Opus 4.5, GPT-5.2, Gemini 3, Grok, GLM-4.7). 31 lifecycle hooks, 20+ tools (LSP, AST-Grep, delegation), 10 specialized agents, Claude Code compatibility layer. "oh-my-zsh" for OpenCode.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
oh-my-opencode/
|
||||
├── src/
|
||||
│ ├── agents/ # 7 AI agents - see src/agents/AGENTS.md
|
||||
│ ├── hooks/ # 22 lifecycle hooks - see src/hooks/AGENTS.md
|
||||
│ ├── tools/ # LSP, AST-Grep, session mgmt - see src/tools/AGENTS.md
|
||||
│ ├── features/ # Claude Code compat layer - see src/features/AGENTS.md
|
||||
│ ├── auth/ # Google Antigravity OAuth - see src/auth/AGENTS.md
|
||||
│ ├── shared/ # Cross-cutting utilities - see src/shared/AGENTS.md
|
||||
│ ├── cli/ # CLI installer, doctor - see src/cli/AGENTS.md
|
||||
│ ├── mcp/ # MCP configs: context7, websearch_exa, grep_app
|
||||
│ ├── agents/ # 10 AI agents (Sisyphus, oracle, librarian, explore, frontend, etc.) - see src/agents/AGENTS.md
|
||||
│ ├── hooks/ # 31 lifecycle hooks (PreToolUse, PostToolUse, Stop, etc.) - see src/hooks/AGENTS.md
|
||||
│ ├── tools/ # 20+ tools (LSP, AST-Grep, delegation, session) - see src/tools/AGENTS.md
|
||||
│ ├── features/ # Background agents, Claude Code compat layer - see src/features/AGENTS.md
|
||||
│ ├── shared/ # 43 cross-cutting utilities - see src/shared/AGENTS.md
|
||||
│ ├── cli/ # CLI installer, doctor, run - see src/cli/AGENTS.md
|
||||
│ ├── mcp/ # Built-in MCPs: websearch, context7, grep_app
|
||||
│ ├── config/ # Zod schema, TypeScript types
|
||||
│ └── index.ts # Main plugin entry (464 lines)
|
||||
├── script/ # build-schema.ts, publish.ts, generate-changelog.ts
|
||||
│ └── index.ts # Main plugin entry (568 lines)
|
||||
├── script/ # build-schema.ts, publish.ts, build-binaries.ts
|
||||
├── packages/ # 7 platform-specific binaries
|
||||
└── dist/ # Build output (ESM + .d.ts)
|
||||
```
|
||||
|
||||
@@ -31,47 +31,90 @@ oh-my-opencode/
|
||||
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| Add agent | `src/agents/` | Create .ts, add to builtinAgents, update types.ts |
|
||||
| Add hook | `src/hooks/` | Dir with createXXXHook(), export from index.ts |
|
||||
| Add tool | `src/tools/` | Dir with constants/types/tools.ts, add to builtinTools |
|
||||
| Add agent | `src/agents/` | Create .ts with factory, add to `builtinAgents` in index.ts |
|
||||
| Add hook | `src/hooks/` | Create dir with `createXXXHook()`, register in index.ts |
|
||||
| Add tool | `src/tools/` | Dir with index/types/constants/tools.ts, add to `builtinTools` |
|
||||
| Add MCP | `src/mcp/` | Create config, add to index.ts |
|
||||
| Add skill | `src/features/builtin-skills/` | Dir with SKILL.md |
|
||||
| Config schema | `src/config/schema.ts` | Run `bun run build:schema` after |
|
||||
| Add skill | `src/features/builtin-skills/` | Create dir with SKILL.md |
|
||||
| LSP behavior | `src/tools/lsp/` | client.ts (connection), tools.ts (handlers) |
|
||||
| AST-Grep | `src/tools/ast-grep/` | napi.ts for @ast-grep/napi binding |
|
||||
| Config schema | `src/config/schema.ts` | Zod schema, run `bun run build:schema` after changes |
|
||||
| Claude Code compat | `src/features/claude-code-*-loader/` | Command, skill, agent, mcp loaders |
|
||||
| Background agents | `src/features/background-agent/` | manager.ts (1165 lines) for task lifecycle |
|
||||
| Skill MCP | `src/features/skill-mcp-manager/` | MCP servers embedded in skills |
|
||||
| CLI installer | `src/cli/install.ts` | Interactive TUI (462 lines) |
|
||||
| Doctor checks | `src/cli/doctor/checks/` | 14 health checks across 6 categories |
|
||||
| Orchestrator | `src/hooks/sisyphus-orchestrator/` | Main orchestration hook (771 lines) |
|
||||
|
||||
## TDD (Test-Driven Development)
|
||||
|
||||
**MANDATORY for new features and bug fixes.** Follow RED-GREEN-REFACTOR:
|
||||
|
||||
| Phase | Action | Verification |
|
||||
|-------|--------|--------------|
|
||||
| **RED** | Write test describing expected behavior | `bun test` → FAIL (expected) |
|
||||
| **GREEN** | Implement minimum code to pass | `bun test` → PASS |
|
||||
| **REFACTOR** | Improve code quality, remove duplication | `bun test` → PASS (must stay green) |
|
||||
|
||||
**Rules:**
|
||||
- NEVER write implementation before test
|
||||
- NEVER delete failing tests to "pass" - fix the code
|
||||
- Test file naming: `*.test.ts` alongside source
|
||||
- BDD comments: `#given`, `#when`, `#then` (same as AAA)
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
- **Bun only**: `bun run`, `bun test`, `bunx` (NEVER npm/npx)
|
||||
- **Package manager**: Bun only (`bun run`, `bun build`, `bunx`)
|
||||
- **Types**: bun-types (not @types/node)
|
||||
- **Build**: `bun build` (ESM) + `tsc --emitDeclarationOnly`
|
||||
- **Exports**: Barrel pattern in index.ts; explicit named exports for tools/hooks
|
||||
- **Naming**: kebab-case directories, createXXXHook/createXXXTool factories
|
||||
- **Testing**: BDD comments `#given`, `#when`, `#then` (same as AAA)
|
||||
- **Exports**: Barrel pattern in index.ts; explicit named exports
|
||||
- **Naming**: kebab-case directories, `createXXXHook`/`createXXXTool` factories
|
||||
- **Testing**: BDD comments `#given/#when/#then`, 84 test files
|
||||
- **Temperature**: 0.1 for code agents, max 0.3
|
||||
|
||||
## ANTI-PATTERNS
|
||||
## ANTI-PATTERNS (THIS PROJECT)
|
||||
|
||||
| Category | Forbidden |
|
||||
|----------|-----------|
|
||||
| Type Safety | `as any`, `@ts-ignore`, `@ts-expect-error` |
|
||||
| Package Manager | npm, yarn, npx |
|
||||
| File Ops | Bash mkdir/touch/rm for code file creation |
|
||||
| Publishing | Direct `bun publish`, local version bump |
|
||||
| Agent Behavior | High temp (>0.3), broad tool access, sequential agent calls |
|
||||
| Hooks | Heavy PreToolUse logic, blocking without reason |
|
||||
| Year | 2024 in code/prompts (use current year) |
|
||||
| **Package Manager** | npm, yarn - use Bun exclusively |
|
||||
| **Types** | @types/node - use bun-types |
|
||||
| **File Ops** | mkdir/touch/rm/cp/mv in code - agents use bash tool |
|
||||
| **Publishing** | Direct `bun publish` - use GitHub Actions workflow_dispatch |
|
||||
| **Versioning** | Local version bump - managed by CI |
|
||||
| **Date References** | Year 2024 - use current year |
|
||||
| **Type Safety** | `as any`, `@ts-ignore`, `@ts-expect-error` |
|
||||
| **Error Handling** | Empty catch blocks `catch(e) {}` |
|
||||
| **Testing** | Deleting failing tests to "pass" |
|
||||
| **Agent Calls** | Sequential agent calls - use `delegate_task` for parallel |
|
||||
| **Tool Access** | Broad tool access - prefer explicit `include` |
|
||||
| **Hook Logic** | Heavy PreToolUse computation - slows every tool call |
|
||||
| **Commits** | Giant commits (3+ files = 2+ commits), separate test from impl |
|
||||
| **Temperature** | >0.3 for code agents |
|
||||
| **Trust** | Trust agent self-reports - ALWAYS verify independently |
|
||||
|
||||
## UNIQUE STYLES
|
||||
|
||||
- **Platform**: Union type `"darwin" | "linux" | "win32" | "unsupported"`
|
||||
- **Optional props**: Extensive `?` for optional interface properties
|
||||
- **Flexible objects**: `Record<string, unknown>` for dynamic configs
|
||||
- **Agent tools**: `tools: { include: [...] }` or `tools: { exclude: [...] }`
|
||||
- **Hook naming**: `createXXXHook` function convention
|
||||
- **Factory pattern**: Components created via `createXXX()` functions
|
||||
|
||||
## AGENT MODELS
|
||||
|
||||
| Agent | Model | Purpose |
|
||||
|-------|-------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | Primary orchestrator |
|
||||
| oracle | openai/gpt-5.2 | Strategy, code review |
|
||||
| librarian | anthropic/claude-sonnet-4-5 | Docs, OSS research |
|
||||
| explore | opencode/grok-code | Fast codebase grep |
|
||||
| frontend-ui-ux-engineer | google/gemini-3-pro-preview | UI generation |
|
||||
| document-writer | google/gemini-3-pro-preview | Technical docs |
|
||||
| Agent | Default Model | Purpose |
|
||||
|-------|---------------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | Primary orchestrator with extended thinking |
|
||||
| oracle | openai/gpt-5.2 | Read-only consultation, high-IQ debugging |
|
||||
| librarian | opencode/glm-4.7-free | Multi-repo analysis, docs, GitHub search |
|
||||
| explore | opencode/grok-code | Fast codebase exploration (contextual grep) |
|
||||
| frontend-ui-ux-engineer | google/gemini-3-pro-preview | UI generation, visual design |
|
||||
| document-writer | google/gemini-3-flash | Technical documentation |
|
||||
| multimodal-looker | google/gemini-3-flash | PDF/image analysis |
|
||||
| Prometheus (Planner) | anthropic/claude-opus-4-5 | Strategic planning, interview mode |
|
||||
| Metis (Plan Consultant) | anthropic/claude-sonnet-4-5 | Pre-planning analysis |
|
||||
| Momus (Plan Reviewer) | anthropic/claude-sonnet-4-5 | Plan validation |
|
||||
|
||||
## COMMANDS
|
||||
|
||||
@@ -79,7 +122,8 @@ oh-my-opencode/
|
||||
bun run typecheck # Type check
|
||||
bun run build # ESM + declarations + schema
|
||||
bun run rebuild # Clean + Build
|
||||
bun test # Run tests (380+)
|
||||
bun run build:schema # Schema only
|
||||
bun test # Run tests (84 test files)
|
||||
```
|
||||
|
||||
## DEPLOYMENT
|
||||
@@ -87,26 +131,51 @@ bun test # Run tests (380+)
|
||||
**GitHub Actions workflow_dispatch only**
|
||||
|
||||
1. Never modify package.json version locally
|
||||
2. Commit & push to dev
|
||||
3. Trigger: `gh workflow run publish -f bump=patch|minor|major`
|
||||
2. Commit & push changes
|
||||
3. Trigger `publish` workflow: `gh workflow run publish -f bump=patch`
|
||||
|
||||
CI auto-commits schema changes on master, maintains rolling `next` draft release on dev.
|
||||
**Critical**: Never `bun publish` directly. Never bump version locally.
|
||||
|
||||
## CI PIPELINE
|
||||
|
||||
- **ci.yml**: Parallel test/typecheck → build → auto-commit schema on master → rolling `next` draft release
|
||||
- **publish.yml**: Manual workflow_dispatch → version bump → changelog → 8-package OIDC npm publish → force-push master
|
||||
|
||||
## COMPLEXITY HOTSPOTS
|
||||
|
||||
| File | Lines | Description |
|
||||
|------|-------|-------------|
|
||||
| `src/index.ts` | 464 | Main plugin, all hook/tool init |
|
||||
| `src/cli/config-manager.ts` | 669 | JSONC parsing, env detection |
|
||||
| `src/auth/antigravity/fetch.ts` | 621 | Token refresh, URL rewriting |
|
||||
| `src/tools/lsp/client.ts` | 611 | LSP protocol, JSON-RPC |
|
||||
| `src/hooks/anthropic-context-window-limit-recovery/executor.ts` | 564 | Multi-stage recovery |
|
||||
| `src/agents/sisyphus.ts` | 504 | Orchestrator prompt |
|
||||
| `src/agents/orchestrator-sisyphus.ts` | 1531 | Orchestrator agent, 7-section delegation, wisdom accumulation |
|
||||
| `src/features/builtin-skills/skills.ts` | 1203 | Skill definitions (playwright, git-master, frontend-ui-ux) |
|
||||
| `src/agents/prometheus-prompt.ts` | 1196 | Planning agent, interview mode, Momus loop |
|
||||
| `src/features/background-agent/manager.ts` | 1165 | Task lifecycle, concurrency, notification batching |
|
||||
| `src/hooks/sisyphus-orchestrator/index.ts` | 771 | Orchestrator hook implementation |
|
||||
| `src/tools/delegate-task/tools.ts` | 761 | Category-based task delegation |
|
||||
| `src/cli/config-manager.ts` | 730 | JSONC parsing, multi-level config |
|
||||
| `src/agents/sisyphus.ts` | 640 | Main Sisyphus prompt |
|
||||
| `src/features/builtin-commands/templates/refactor.ts` | 619 | Refactoring command template |
|
||||
| `src/tools/lsp/client.ts` | 596 | LSP protocol, JSON-RPC |
|
||||
|
||||
## MCP ARCHITECTURE
|
||||
|
||||
Three-tier MCP system:
|
||||
1. **Built-in**: `websearch` (Exa), `context7` (docs), `grep_app` (GitHub search)
|
||||
2. **Claude Code compatible**: `.mcp.json` files with `${VAR}` expansion
|
||||
3. **Skill-embedded**: YAML frontmatter in skills (e.g., playwright)
|
||||
|
||||
## CONFIG SYSTEM
|
||||
|
||||
- **Zod validation**: `src/config/schema.ts`
|
||||
- **JSONC support**: Comments and trailing commas
|
||||
- **Multi-level**: Project (`.opencode/`) → User (`~/.config/opencode/`)
|
||||
- **CLI doctor**: Validates config and reports errors
|
||||
|
||||
## NOTES
|
||||
|
||||
- **Testing**: Bun native test (`bun test`), BDD-style, 84 test files
|
||||
- **OpenCode**: Requires >= 1.0.150
|
||||
- **Config**: `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`
|
||||
- **JSONC**: Config files support comments and trailing commas
|
||||
- **Claude Code**: Full compat layer for settings.json hooks, commands, skills, agents, MCPs
|
||||
- **Skill MCP**: Skills can embed MCP server configs in YAML frontmatter
|
||||
- **Multi-lang docs**: README.md (EN), README.ko.md (KO), README.ja.md (JA), README.zh-cn.md (ZH-CN)
|
||||
- **Config**: `~/.config/opencode/oh-my-opencode.json` (user) or `.opencode/oh-my-opencode.json` (project)
|
||||
- **Trusted deps**: @ast-grep/cli, @ast-grep/napi, @code-yeongyu/comment-checker
|
||||
- **Claude Code Compat**: Full compatibility layer for settings.json hooks, commands, skills, agents, MCPs
|
||||
- **Flaky tests**: 2 known flaky tests (ralph-loop CI timeout, session-state parallel pollution)
|
||||
|
||||
@@ -26,6 +26,29 @@ First off, thanks for taking the time to contribute! This document provides guid
|
||||
|
||||
Be respectful, inclusive, and constructive. We're all here to make better tools together.
|
||||
|
||||
## Language Policy
|
||||
|
||||
**English is the primary language for all communications in this repository.**
|
||||
|
||||
This includes:
|
||||
- Issues and bug reports
|
||||
- Pull requests and code reviews
|
||||
- Documentation and comments
|
||||
- Discussions and community interactions
|
||||
|
||||
### Why English?
|
||||
|
||||
- **Global Accessibility**: English allows contributors from all regions to collaborate effectively
|
||||
- **Consistency**: A single language keeps discussions organized and searchable
|
||||
- **Open Source Best Practice**: Most successful open-source projects use English as the lingua franca
|
||||
|
||||
### Need Help with English?
|
||||
|
||||
If English isn't your first language, don't worry! We value your contributions regardless of perfect grammar. You can:
|
||||
- Use translation tools to help compose messages
|
||||
- Ask for help from other community members
|
||||
- Focus on clear, simple communication rather than perfect prose
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
@@ -89,7 +112,7 @@ oh-my-opencode/
|
||||
│ ├── agents/ # AI agents (OmO, oracle, librarian, explore, etc.)
|
||||
│ ├── hooks/ # 21 lifecycle hooks
|
||||
│ ├── tools/ # LSP (11), AST-Grep, Grep, Glob, etc.
|
||||
│ ├── mcp/ # MCP server integrations (context7, websearch_exa, grep_app)
|
||||
│ ├── mcp/ # MCP server integrations (context7, grep_app)
|
||||
│ ├── features/ # Claude Code compatibility layers
|
||||
│ ├── config/ # Zod schemas and TypeScript types
|
||||
│ ├── auth/ # Google Antigravity OAuth
|
||||
@@ -199,7 +222,7 @@ export function createMyHook(input: PluginInput) {
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. **Fork** the repository and create your branch from `master`
|
||||
1. **Fork** the repository and create your branch from `dev`
|
||||
2. **Make changes** following the conventions above
|
||||
3. **Build and test** locally:
|
||||
```bash
|
||||
|
||||
333
README.ja.md
333
README.ja.md
@@ -1,13 +1,19 @@
|
||||
> [!NOTE]
|
||||
>
|
||||
> *「私はエージェントが生成したコードと人間が書いたコードを区別できない、しかしはるかに多くのことを達成できる世界を作り、ソフトウェア革命を起こすことを目指しています。私はこの旅に個人的な時間、情熱、そして資金を注ぎ込んできましたし、これからもそうし続けます。」*
|
||||
> [](https://sisyphuslabs.ai)
|
||||
> > **Sisyphusの完全製品化バージョンを構築中です。フロンティアエージェントの未来を定義します。<br />[こちら](https://sisyphuslabs.ai)からウェイトリストに参加してください。**
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.1)
|
||||
> > **オーケストレーターがベータ版で利用可能になりました。`oh-my-opencode@3.0.0-beta.1`を使用してインストールしてください。**
|
||||
>
|
||||
> 一緒に歩みましょう!
|
||||
>
|
||||
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PWpXmbhF) | [Discordコミュニティ](https://discord.gg/PWpXmbhF)に参加して、コントリビューターや`oh-my-opencode`仲間とつながりましょう。 |
|
||||
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PUwSMR9XNk) | [Discordコミュニティ](https://discord.gg/PUwSMR9XNk)に参加して、コントリビューターや`oh-my-opencode`仲間とつながりましょう。 |
|
||||
> | :-----| :----- |
|
||||
> | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | `oh-my-opencode`に関するニュースは私のXアカウントで投稿していましたが、無実の罪で凍結されたため、<br />[@justsisyphus](https://x.com/justsisyphus)が代わりに更新を投稿しています。 |
|
||||
> | [<img alt="Sponsor" src="https://img.shields.io/badge/Sponsor-❤-ff69b4?style=flat-square&logo=github-sponsors&labelColor=black" width="156px" />](https://github.com/sponsors/code-yeongyu) | [スポンサーになって](https://github.com/sponsors/code-yeongyu) `oh-my-opencode` の開発を応援してください。皆さまのご支援がこのプロジェクトを成長させます。 |
|
||||
> | [<img alt="GitHub Follow" src="https://img.shields.io/github/followers/code-yeongyu?style=flat-square&logo=github&labelColor=black&color=24292f" width="156px" />](https://github.com/code-yeongyu) | GitHubで[@code-yeongyu](https://github.com/code-yeongyu)をフォローして、他のプロジェクトもチェックしてください。 |
|
||||
|
||||
<!-- <CENTERED SECTION FOR GITHUB DISPLAY> -->
|
||||
|
||||
@@ -22,7 +28,29 @@
|
||||
|
||||
> `oh-my-opencode` をインストールして、ドーピングしたかのようにコーディングしましょう。バックグラウンドでエージェントを走らせ、oracle、librarian、frontend engineer のような専門エージェントを呼び出してください。丹精込めて作られた LSP/AST ツール、厳選された MCP、そして完全な Claude Code 互換レイヤーを、たった一行で手に入れましょう。
|
||||
|
||||
**今すぐ始めましょう。ChatGPT、Claude、Gemini のサブスクリプションで使えます。**
|
||||
# Claude OAuth アクセスに関するお知らせ
|
||||
|
||||
## TL;DR
|
||||
|
||||
> Q. oh-my-opencodeを使用できますか?
|
||||
|
||||
はい。
|
||||
|
||||
> Q. Claude Codeのサブスクリプションで使用できますか?
|
||||
|
||||
はい、技術的には可能です。ただし、使用を推奨することはできません。
|
||||
|
||||
## 詳細
|
||||
|
||||
> 2026年1月より、AnthropicはToS違反を理由にサードパーティのOAuthアクセスを制限しました。
|
||||
>
|
||||
> [**Anthropicはこのプロジェクト oh-my-opencode を、opencodeをブロックする正当化の根拠として挙げています。**](https://x.com/thdxr/status/2010149530486911014)
|
||||
>
|
||||
> 実際、Claude CodeのOAuthリクエストシグネチャを偽装するプラグインがコミュニティに存在します。
|
||||
>
|
||||
> これらのツールは技術的な検出可能性に関わらず動作する可能性がありますが、ユーザーはToSへの影響を認識すべきであり、私個人としてはそれらの使用を推奨できません。
|
||||
>
|
||||
> このプロジェクトは非公式ツールの使用に起因するいかなる問題についても責任を負いません。また、**私たちはそれらのOAuthシステムのカスタム実装を一切持っていません。**
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -42,21 +70,26 @@
|
||||
|
||||
## ユーザーレビュー
|
||||
|
||||
> "Cursorのサブスクリプションを解約しました。オープンソースコミュニティで信じられないことが起きています。" - [Arthur Guiot](https://x.com/arthur_guiot/status/2008736347092382053?s=20)
|
||||
|
||||
> "人間が3ヶ月かかる仕事をClaude Codeが7日でやるなら、Sisyphusは1時間でやります。タスクが完了するまでただ動き続ける。It is a discipline agent." — B, Quant Researcher
|
||||
|
||||
> "Oh My Opencodeを使って、たった1日で8000個のeslint警告を解消しました" — [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
|
||||
|
||||
> "Ohmyopencodeとralph loopを使って、一晩で45,000行のtauriアプリをSaaSウェブアプリに変換しました。インタビュープロンプトから始めて、質問に対する評価と推奨を求めました。作業する様子を見ているのは驚きでしたし、朝起きたらほぼ完成したウェブサイトがありました!" - [James Hargis](https://x.com/hargabyte/status/2007299688261882202)
|
||||
|
||||
> "oh-my-opencodeを使ってください、もう戻れませんよ" — [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
|
||||
|
||||
> "何どうすごいのかあまり言語化できてないけど、開発体験が異次元に上がった。" - [苔硯:こけすずり](https://x.com/kokesuzuri/status/2008532913961529372?s=20)
|
||||
|
||||
> "今週末はopen code、oh my opencode、supermemoryでマインクラフト/ソウルライクな何かを作る実験をしています。"
|
||||
> "昼食後の散歩に行く間に、しゃがみアニメーションを追加するよう頼みました。[動画]" - [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
|
||||
|
||||
> "これをコアに取り入れて彼を採用すべきです。マジで。本当に、本当に、本当に良いです" — Henning Kilset
|
||||
|
||||
> "@yeon_gyu_kimを説得できるなら雇うべきです。彼はopencodeに革命を起こしました" — [mysticaltech](https://x.com/mysticaltech/status/2001858758608376079)
|
||||
|
||||
> "やばい、これマジで本物だ @androolloyd oh my opencode 最高すぎる" — [z80.eth](https://x.com/0xz80/status/2001815226505924791)
|
||||
|
||||
> "oh-my-opencodeを使ってください、もう戻れませんよ" — [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
|
||||
|
||||
> "Oh My Opencodeは頂点に立っています、敵はいません" — [RyanOnThePath](https://x.com/RyanOnThePath/status/2001438321252118548)
|
||||
|
||||
> "シジフォスという名前自体が美しいじゃないですか?" — Sigrid ([@sigridjin_eth](https://x.com/sigridjin_eth))
|
||||
> "Oh My OpenCode Is Actually Insane" - [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
|
||||
|
||||
---
|
||||
|
||||
@@ -65,17 +98,36 @@
|
||||
- [Oh My OpenCode](#oh-my-opencode)
|
||||
- [この Readme は読まなくていいです](#この-readme-は読まなくていいです)
|
||||
- [エージェントの時代ですから](#エージェントの時代ですから)
|
||||
- [🪄 魔法の言葉:`ultrawork`](#-魔法の言葉ultrawork)
|
||||
- [読みたい方のために:シジフォスに会う](#読みたい方のためにシジフォスに会う)
|
||||
- [インストールするだけで。](#インストールするだけで)
|
||||
- [インストール](#インストール)
|
||||
- [人間の方へ](#人間の方へ)
|
||||
- [LLM エージェントの方へ](#llm-エージェントの方へ)
|
||||
- [ステップ 0: サブスクリプション情報を確認](#ステップ-0-サブスクリプション情報を確認)
|
||||
- [ステップ 1: OpenCode インストール確認](#ステップ-1-opencode-インストール確認)
|
||||
- [ステップ 2: インストーラーを実行](#ステップ-2-インストーラーを実行)
|
||||
- [ステップ 3: 設定確認](#ステップ-3-設定確認)
|
||||
- [ステップ 4: 認証情報の設定](#ステップ-4-認証情報の設定)
|
||||
- [4.1 Anthropic (Claude)](#41-anthropic-claude)
|
||||
- [4.2 Google Gemini (Antigravity OAuth)](#42-google-gemini-antigravity-oauth)
|
||||
- [4.2.1 モデル設定](#421-モデル設定)
|
||||
- [4.2.2 oh-my-opencode エージェントモデルのオーバーライド](#422-oh-my-opencode-エージェントモデルのオーバーライド)
|
||||
|
||||
- [⚠️ 注意](#️-注意)
|
||||
- [セットアップの確認](#セットアップの確認)
|
||||
- [ユーザーに「おめでとうございます!🎉」と伝える](#ユーザーにおめでとうございますと伝える)
|
||||
- [無料広告](#無料広告)
|
||||
- [スターをお願いする ⭐](#スターをお願いする-)
|
||||
- [複雑すぎますか?](#複雑すぎますか)
|
||||
- [アンインストール](#アンインストール)
|
||||
- [機能](#機能)
|
||||
- [Agents: あなたの新しいチームメイト](#agents-あなたの新しいチームメイト)
|
||||
- [バックグラウンドエージェント: 本当のチームのように働く](#バックグラウンドエージェント-本当のチームのように働く)
|
||||
- [ツール: 同僚にはもっと良い道具を](#ツール-同僚にはもっと良い道具を)
|
||||
- [なぜあなただけ IDE を使っているのですか?](#なぜあなただけ-ide-を使っているのですか)
|
||||
- [Context is all you need.](#context-is-all-you-need)
|
||||
- [セッション管理](#セッション管理)
|
||||
- [Context Is All You Need](#context-is-all-you-need)
|
||||
- [マルチモーダルを活用し、トークンは節約する](#マルチモーダルを活用しトークンは節約する)
|
||||
- [止まらないエージェントループ](#止まらないエージェントループ)
|
||||
- [Claude Code 互換性: さらば Claude Code、ようこそ OpenCode](#claude-code-互換性-さらば-claude-codeようこそ-opencode)
|
||||
@@ -85,16 +137,20 @@
|
||||
- [互換性トグル](#互換性トグル)
|
||||
- [エージェントのためだけでなく、あなたのために](#エージェントのためだけでなくあなたのために)
|
||||
- [設定](#設定)
|
||||
- [JSONC のサポート](#jsonc-のサポート)
|
||||
- [Google Auth](#google-auth)
|
||||
- [Agents](#agents)
|
||||
- [Permission オプション](#permission-オプション)
|
||||
- [Sisyphus Agent](#sisyphus-agent)
|
||||
- [Background Tasks](#background-tasks)
|
||||
- [Hooks](#hooks)
|
||||
- [MCPs](#mcps)
|
||||
- [LSP](#lsp)
|
||||
- [Experimental](#experimental)
|
||||
- [作者のノート](#作者のノート)
|
||||
- [注意](#注意)
|
||||
- [こちらの企業の専門家にご愛用いただいています](#こちらの企業の専門家にご愛用いただいています)
|
||||
- [スポンサー](#スポンサー)
|
||||
|
||||
# Oh My OpenCode
|
||||
|
||||
@@ -167,13 +223,15 @@ Windows から Linux に初めて乗り換えた時のこと、自分の思い
|
||||
|
||||
インストールするだけで、エージェントは以下のようなワークフローで働けるようになります:
|
||||
|
||||
1. バックグラウンドタスクとして Gemini 3 Pro にフロントエンドを書かせている間に、Claude Opus 4.5 がバックエンドを作成し、デバッグで詰まったら GPT 5.2 に助けを求めます。フロントエンドの実装完了報告が来たら、それを検証して出荷します。
|
||||
2. 何か調べる必要があれば、公式ドキュメント、コードベースの全履歴、GitHub に公開されている実装例まで徹底的に調査します。単なる grep だけでなく、内蔵された LSP ツールや AST-Grep まで駆使します。
|
||||
3. LLM に仕事を任せる際、コンテキスト管理の心配はもう不要です。私がやります。
|
||||
- OhMyOpenCode は複数のエージェントを積極的に活用し、コンテキストの負荷を軽減します。
|
||||
- **あなたのエージェントは今や開発チームのリードです。あなたは AI マネージャーです。**
|
||||
4. 頼んだ仕事が完了するまで止まりません。
|
||||
5. このプロジェクトについて深く知りたくない?大丈夫です。ただ 'ultrathink' と入力してください。
|
||||
1. Sisyphusは自分自身でファイルを探し回るような時間の無駄はしません。メインエージェントのコンテキストを軽量に保つため、より高速で安価なモデルへ並列でバックグラウンドタスクを飛ばし、自身の代わりに領域の調査を完了させます。
|
||||
1. SisyphusはリファクタリングにLSPを活用します。その方が確実で、安全、かつ的確だからです。
|
||||
1. UIに関わる重い作業が必要な場合、SisyphusはフロントエンドのタスクをGemini 3 Proに直接デリゲートします。
|
||||
1. もしSisyphusがループに陥ったり壁にぶつかったりしても、無駄に悩み続けることはありません。高IQな戦略的バックアップとしてGPT 5.2を呼び出します。
|
||||
1. 複雑なオープンソースフレームワークを扱っていますか?Sisyphusはサブエージェントを生成し、生のソースコードやドキュメントをリアルタイムで消化します。彼は完全なコンテキスト認識を持って動作します。
|
||||
1. Sisyphusがコメントに触れるとき、その存在意義を証明するか、さもなくば削除します。あなたのコードベースを常にクリーンに保ちます。
|
||||
1. Sisyphusは自身のTODOリストに縛られています。もし始めたことを終わらせられなければ、システムは彼を強制的に「bouldering」モードに戻します。あなたのタスクは、何があろうと完了します。
|
||||
1. 正直、ドキュメントなんて読む必要はありません。ただプロンプトを書いてください。「ultrawork」というキーワードを含めるだけで十分です。Sisyphusが構造を分析し、コンテキストを集め、外部のソースコードまで掘り下げ、仕事が100%完了するまでboulderingを続けます。
|
||||
1. ぶっちゃけ、「ultrawork」と打つのすら面倒ですよね。それなら「ulw」だけでOKです。ただulwと打ち、コーヒーでも飲んでいてください。仕事は終わっています。
|
||||
|
||||
このような機能が不要であれば、前述の通り、特定の機能だけを選んで使うことができます。
|
||||
|
||||
@@ -183,6 +241,9 @@ Windows から Linux に初めて乗り換えた時のこと、自分の思い
|
||||
### 人間の方へ
|
||||
|
||||
面倒な作業は LLM エージェントに任せましょう。このリンクをコピーして貼り付け、インストールと設定をお願いしてください。
|
||||
```
|
||||
Install and configure by following the instructions here https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/refs/heads/master/README.md
|
||||
```
|
||||
|
||||
### LLM エージェントの方へ
|
||||
|
||||
@@ -275,7 +336,7 @@ opencode auth login
|
||||
{
|
||||
"plugin": [
|
||||
"oh-my-opencode",
|
||||
"opencode-antigravity-auth@1.1.2"
|
||||
"opencode-antigravity-auth@1.2.8"
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -287,20 +348,19 @@ opencode auth login
|
||||
|
||||
#### 4.2.2 oh-my-opencode エージェントモデルのオーバーライド
|
||||
|
||||
`opencode-antigravity-auth` プラグインは内蔵 Google auth と異なるモデル名を使用します。`oh-my-opencode.json`(または `.opencode/oh-my-opencode.json`)でエージェントモデルをオーバーライドし、内蔵 `google_auth` を無効化してください:
|
||||
`opencode-antigravity-auth` プラグインは特定のモデル名を使用します。エージェントモデルを `oh-my-opencode.json`(または `.opencode/oh-my-opencode.json`)でオーバーライドしてください:
|
||||
|
||||
```json
|
||||
{
|
||||
"google_auth": false,
|
||||
"agents": {
|
||||
"frontend-ui-ux-engineer": { "model": "google/gemini-3-pro-high" },
|
||||
"document-writer": { "model": "google/gemini-3-flash" },
|
||||
"multimodal-looker": { "model": "google/gemini-3-flash" }
|
||||
"frontend-ui-ux-engineer": { "model": "google/antigravity-gemini-3-pro-high" },
|
||||
"document-writer": { "model": "google/antigravity-gemini-3-flash" },
|
||||
"multimodal-looker": { "model": "google/antigravity-gemini-3-flash" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**利用可能なモデル名**: `google/gemini-3-pro-high`, `google/gemini-3-pro-medium`, `google/gemini-3-pro-low`, `google/gemini-3-flash`, `google/gemini-3-flash`, `google/gemini-3-flash-lite`, `google/claude-sonnet-4-5`, `google/claude-sonnet-4-5-thinking`, `google/claude-opus-4-5-thinking`, `google/gpt-oss-120b-medium`
|
||||
**利用可能なモデル名**: `google/antigravity-gemini-3-pro-high`, `google/antigravity-gemini-3-pro-low`, `google/antigravity-gemini-3-flash`, `google/antigravity-claude-sonnet-4-5`, `google/antigravity-claude-sonnet-4-5-thinking-low`, `google/antigravity-claude-sonnet-4-5-thinking-medium`, `google/antigravity-claude-sonnet-4-5-thinking-high`, `google/antigravity-claude-opus-4-5-thinking-low`, `google/antigravity-claude-opus-4-5-thinking-medium`, `google/antigravity-claude-opus-4-5-thinking-high`, `google/gemini-3-pro-preview`, `google/gemini-3-flash-preview`, `google/gemini-2.5-pro`, `google/gemini-2.5-flash`
|
||||
|
||||
その後、認証を行います:
|
||||
|
||||
@@ -315,44 +375,46 @@ opencode auth login
|
||||
|
||||
**マルチアカウントロードバランシング**: プラグインは最大10個の Google アカウントをサポートします。1つのアカウントがレートリミットに達すると、自動的に次のアカウントに切り替わります。
|
||||
|
||||
#### 4.3 OpenAI (ChatGPT Plus/Pro)
|
||||
#### 4.3 GitHub Copilot(フォールバックプロバイダー)
|
||||
|
||||
まず、opencode-openai-codex-auth プラグインを追加します:
|
||||
GitHub Copilot は、ネイティブプロバイダー(Claude、ChatGPT、Gemini)が利用できない場合の**フォールバックプロバイダー**としてサポートされています。インストーラーは、Copilot をネイティブプロバイダーより低い優先度で構成します。
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": [
|
||||
"oh-my-opencode",
|
||||
"opencode-openai-codex-auth@4.1.1"
|
||||
]
|
||||
}
|
||||
**優先度**: ネイティブプロバイダー (Claude/ChatGPT/Gemini) > GitHub Copilot > 無料モデル
|
||||
|
||||
##### モデルマッピング
|
||||
|
||||
GitHub Copilot が有効な場合、oh-my-opencode は以下のモデル割り当てを使用します:
|
||||
|
||||
| エージェント | モデル |
|
||||
|--------------|--------|
|
||||
| **Sisyphus** | `github-copilot/claude-opus-4.5` |
|
||||
| **Oracle** | `github-copilot/gpt-5.2` |
|
||||
| **Explore** | `grok code`(デフォルト) |
|
||||
| **Librarian** | `glm 4.7 free`(デフォルト) |
|
||||
|
||||
GitHub Copilot はプロキシプロバイダーとして機能し、サブスクリプションに基づいて基盤となるモデルにリクエストをルーティングします。
|
||||
|
||||
##### セットアップ
|
||||
|
||||
インストーラーを実行し、GitHub Copilot で「はい」を選択します:
|
||||
|
||||
```bash
|
||||
bunx oh-my-opencode install
|
||||
# サブスクリプション(Claude、ChatGPT、Gemini)を選択
|
||||
# プロンプトが表示されたら: "Do you have a GitHub Copilot subscription?" → 「はい」を選択
|
||||
```
|
||||
|
||||
**重要**: 現在、公式 npm パッケージに 400 エラー (`"No tool call found for function call output with call_id"`) を引き起こすバグがあります。修正版がリリースされるまでは、**ホットフィックスブランチの使用を推奨します**。`~/.config/opencode/package.json` を修正してください:
|
||||
または、非対話モードを使用します:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"opencode-openai-codex-auth": "code-yeongyu/opencode-openai-codex-auth#fix/orphaned-function-call-output-with-tools"
|
||||
}
|
||||
}
|
||||
```bash
|
||||
bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no --copilot=yes
|
||||
```
|
||||
|
||||
その後、`cd ~/.config/opencode && bun i` を実行してください。`opencode.json` ではバージョン指定なしで `"opencode-openai-codex-auth"` として使用します(`@4.1.0` は除外)。
|
||||
|
||||
#### 4.3.1 モデル設定
|
||||
`opencode.json` に完全なモデル設定も構成する必要があります。
|
||||
[opencode-openai-codex-auth ドキュメント](https://github.com/numman-ali/opencode-openai-codex-auth)を読み、[`config/full-opencode.json`](https://github.com/numman-ali/opencode-openai-codex-auth/blob/main/config/full-opencode.json) から provider/models 設定をコピーして、ユーザーの既存のセットアップを壊さないよう慎重にマージしてください。
|
||||
|
||||
その後、認証を行います:
|
||||
その後、GitHub で認証します:
|
||||
|
||||
```bash
|
||||
opencode auth login
|
||||
# Provider: OpenAI を選択
|
||||
# Login method: ChatGPT Plus/Pro (Codex Subscription) を選択
|
||||
# ユーザーにブラウザでの OAuth フロー完了を案内
|
||||
# 完了まで待機
|
||||
# 成功を確認し、ユーザーに報告
|
||||
# 選択: GitHub → OAuth 経由で認証
|
||||
```
|
||||
|
||||
|
||||
@@ -360,7 +422,7 @@ opencode auth login
|
||||
|
||||
**ユーザーが明示的に要求しない限り、モデル設定を変更したり、機能(エージェント、フック、MCP)を無効化しないでください。**
|
||||
|
||||
プラグインはデフォルトで完璧に動作します。推奨される `google_auth: true` 設定を除き、明示的な要求なしに他の設定を変更したり機能をオフにしないでください。
|
||||
プラグインはデフォルトで完璧に動作します。`opencode-antigravity-auth` を使用する場合は、上記の通りエージェントモデルをオーバーライドしてください。明示的な要求なしに他の設定を変更したり機能をオフにしないでください。
|
||||
|
||||
### セットアップの確認
|
||||
|
||||
@@ -439,7 +501,7 @@ oh-my-opencode を削除するには:
|
||||
|
||||
- **Sisyphus** (`anthropic/claude-opus-4-5`): **デフォルトエージェントです。** OpenCode のための強力な AI オーケストレーターです。専門のサブエージェントを活用して、複雑なタスクを計画、委任、実行します。バックグラウンドタスクへの委任と Todo ベースのワークフローを重視します。最大の推論能力を発揮するため、Claude Opus 4.5 と拡張思考 (32k token budget) を使用します。
|
||||
- **oracle** (`openai/gpt-5.2`): アーキテクチャ、コードレビュー、戦略立案のための専門アドバイザー。GPT-5.2 の卓越した論理的推論と深い分析能力を活用します。AmpCode からインスピレーションを得ました。
|
||||
- **librarian** (`anthropic/claude-sonnet-4-5` または `google/gemini-3-flash`): マルチリポジトリ分析、ドキュメント検索、実装例の調査を担当。Antigravity 認証が設定されている場合は Gemini 3 Flash を使用し、それ以外は Claude Sonnet 4.5 を使用して、深いコードベース理解と GitHub リサーチ、根拠に基づいた回答を提供します。AmpCode からインスピレーションを得ました。
|
||||
- **librarian** (`opencode/glm-4.7-free`): マルチリポジトリ分析、ドキュメント検索、実装例の調査を担当。GLM-4.7 Free を使用して、深いコードベース理解と GitHub リサーチ、根拠に基づいた回答を提供します。AmpCode からインスピレーションを得ました。
|
||||
- **explore** (`opencode/grok-code`、`google/gemini-3-flash`、または `anthropic/claude-haiku-4-5`): 高速なコードベース探索、ファイルパターンマッチング。Antigravity 認証が設定されている場合は Gemini 3 Flash を使用し、Claude max20 が利用可能な場合は Haiku を使用し、それ以外は Grok を使います。Claude Code からインスピレーションを得ました。
|
||||
- **frontend-ui-ux-engineer** (`google/gemini-3-pro-preview`): 開発者に転身したデザイナーという設定です。素晴らしい UI を作ります。美しく独創的な UI コードを生成することに長けた Gemini を使用します。
|
||||
- **document-writer** (`google/gemini-3-pro-preview`): テクニカルライティングの専門家という設定です。Gemini は文筆家であり、流れるような文章を書きます。
|
||||
@@ -486,17 +548,9 @@ Ask @explore for the policy on this feature
|
||||
あなたがエディタで使っているその機能、他のエージェントは触ることができません。
|
||||
最高の同僚に最高の道具を渡してください。これでリファクタリングも、ナビゲーションも、分析も、エージェントが適切に行えるようになります。
|
||||
|
||||
- **lsp_hover**: その位置の型情報、ドキュメント、シグネチャを取得
|
||||
- **lsp_goto_definition**: シンボル定義へジャンプ
|
||||
- **lsp_find_references**: ワークスペース全体で使用箇所を検索
|
||||
- **lsp_document_symbols**: ファイルのシンボルアウトラインを取得
|
||||
- **lsp_workspace_symbols**: プロジェクト全体から名前でシンボルを検索
|
||||
- **lsp_diagnostics**: ビルド前にエラー/警告を取得
|
||||
- **lsp_servers**: 利用可能な LSP サーバー一覧
|
||||
- **lsp_prepare_rename**: 名前変更操作の検証
|
||||
- **lsp_rename**: ワークスペース全体でシンボル名を変更
|
||||
- **lsp_code_actions**: 利用可能なクイックフィックス/リファクタリングを取得
|
||||
- **lsp_code_action_resolve**: コードアクションを適用
|
||||
- **ast_grep_search**: AST 認識コードパターン検索 (25言語対応)
|
||||
- **ast_grep_replace**: AST 認識コード置換
|
||||
|
||||
@@ -541,7 +595,6 @@ OpenCode セッション履歴をナビゲートおよび検索するための
|
||||
```
|
||||
- **Online**: プロジェクトのルールがすべてではありません。拡張機能のための内蔵 MCP を提供します:
|
||||
- **context7**: ライブラリの最新公式ドキュメントを取得
|
||||
- **websearch_exa**: Exa AI を活用したリアルタイムウェブ検索
|
||||
- **grep_app**: 数百万の公開 GitHub リポジトリから超高速コード検索(実装例を探すのに最適)
|
||||
|
||||
#### マルチモーダルを活用し、トークンは節約する
|
||||
@@ -626,21 +679,38 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
"commands": false,
|
||||
"skills": false,
|
||||
"agents": false,
|
||||
"hooks": false
|
||||
"hooks": false,
|
||||
"plugins": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| トグル | `false` の場合、ロードが無効になるパス | 影響を受けないもの |
|
||||
| ---------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------- |
|
||||
| `mcp` | `~/.claude/.mcp.json`, `./.mcp.json`, `./.claude/.mcp.json` | 内蔵 MCP (context7, websearch_exa) |
|
||||
| `mcp` | `~/.claude/.mcp.json`, `./.mcp.json`, `./.claude/.mcp.json` | 内蔵 MCP (context7, grep_app) |
|
||||
| `commands` | `~/.claude/commands/*.md`, `./.claude/commands/*.md` | `~/.config/opencode/command/`, `./.opencode/command/` |
|
||||
| `skills` | `~/.claude/skills/*/SKILL.md`, `./.claude/skills/*/SKILL.md` | - |
|
||||
| `agents` | `~/.claude/agents/*.md`, `./.claude/agents/*.md` | 内蔵エージェント (oracle, librarian 等) |
|
||||
| `hooks` | `~/.claude/settings.json`, `./.claude/settings.json`, `./.claude/settings.local.json` | - |
|
||||
| `plugins` | `~/.claude/plugins/` (Claude Code マーケットプレイスプラグイン) | - |
|
||||
|
||||
すべてのトグルはデフォルトで `true` (有効) です。完全な Claude Code 互換性を望む場合は `claude_code` オブジェクトを省略してください。
|
||||
|
||||
**特定のプラグインだけを無効化** するには `plugins_override` を使用します:
|
||||
|
||||
```json
|
||||
{
|
||||
"claude_code": {
|
||||
"plugins_override": {
|
||||
"claude-mem@thedotmack": false,
|
||||
"some-other-plugin@marketplace": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
プラグインシステム自体は有効にしたまま、特定のプラグインだけをその完全な識別子 (`plugin-name@marketplace-name`) で無効化できます。
|
||||
|
||||
### エージェントのためだけでなく、あなたのために
|
||||
|
||||
エージェントが活躍すれば、あなたも幸せになります。ですが、私はあなた自身も助けたいのです。
|
||||
@@ -683,10 +753,10 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
1. `.opencode/oh-my-opencode.json` (プロジェクト)
|
||||
2. ユーザー設定(プラットフォーム別):
|
||||
|
||||
| プラットフォーム | ユーザー設定パス |
|
||||
|------------------|------------------|
|
||||
| **Windows** | `~/.config/opencode/oh-my-opencode.json` (推奨) または `%APPDATA%\opencode\oh-my-opencode.json` (fallback) |
|
||||
| **macOS/Linux** | `~/.config/opencode/oh-my-opencode.json` |
|
||||
| プラットフォーム | ユーザー設定パス |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| **Windows** | `~/.config/opencode/oh-my-opencode.json` (推奨) または `%APPDATA%\opencode\oh-my-opencode.json` (fallback) |
|
||||
| **macOS/Linux** | `~/.config/opencode/oh-my-opencode.json` |
|
||||
|
||||
スキーマ自動補完がサポートされています:
|
||||
|
||||
@@ -710,10 +780,7 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
|
||||
// Antigravity OAuth 経由で Google Gemini を有効にする
|
||||
"google_auth": false,
|
||||
|
||||
|
||||
/* エージェントのオーバーライド - 特定のタスクに合わせてモデルをカスタマイズ */
|
||||
"agents": {
|
||||
"oracle": {
|
||||
@@ -730,27 +797,18 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
|
||||
**推奨**: 外部の [`opencode-antigravity-auth`](https://github.com/NoeFabris/opencode-antigravity-auth) プラグインを使用してください。マルチアカウントロードバランシング、より多くのモデル(Antigravity 経由の Claude を含む)、活発なメンテナンスを提供します。[インストール > Google Gemini](#42-google-gemini-antigravity-oauth) を参照。
|
||||
|
||||
`opencode-antigravity-auth` 使用時は内蔵 auth を無効化し、`oh-my-opencode.json` でエージェントモデルをオーバーライドしてください:
|
||||
`opencode-antigravity-auth` 使用時は `oh-my-opencode.json` でエージェントモデルをオーバーライドしてください:
|
||||
|
||||
```json
|
||||
{
|
||||
"google_auth": false,
|
||||
"agents": {
|
||||
"frontend-ui-ux-engineer": { "model": "google/gemini-3-pro-high" },
|
||||
"document-writer": { "model": "google/gemini-3-flash" },
|
||||
"multimodal-looker": { "model": "google/gemini-3-flash" }
|
||||
"frontend-ui-ux-engineer": { "model": "google/antigravity-gemini-3-pro-high" },
|
||||
"document-writer": { "model": "google/antigravity-gemini-3-flash" },
|
||||
"multimodal-looker": { "model": "google/antigravity-gemini-3-flash" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**代替案**: 内蔵 Antigravity OAuth を有効化(単一アカウント、Gemini モデルのみ):
|
||||
|
||||
```json
|
||||
{
|
||||
"google_auth": true
|
||||
}
|
||||
```
|
||||
|
||||
### Agents
|
||||
|
||||
内蔵エージェント設定をオーバーライドできます:
|
||||
@@ -803,13 +861,13 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
}
|
||||
```
|
||||
|
||||
| Permission | 説明 | 値 |
|
||||
|------------|------|----|
|
||||
| `edit` | ファイル編集権限 | `ask` / `allow` / `deny` |
|
||||
| `bash` | Bash コマンド実行権限 | `ask` / `allow` / `deny` またはコマンド別: `{ "git": "allow", "rm": "deny" }` |
|
||||
| `webfetch` | ウェブアクセス権限 | `ask` / `allow` / `deny` |
|
||||
| `doom_loop` | 無限ループ検知のオーバーライド許可 | `ask` / `allow` / `deny` |
|
||||
| `external_directory` | プロジェクトルート外へのファイルアクセス | `ask` / `allow` / `deny` |
|
||||
| Permission | 説明 | 値 |
|
||||
| -------------------- | ---------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `edit` | ファイル編集権限 | `ask` / `allow` / `deny` |
|
||||
| `bash` | Bash コマンド実行権限 | `ask` / `allow` / `deny` またはコマンド別: `{ "git": "allow", "rm": "deny" }` |
|
||||
| `webfetch` | ウェブアクセス権限 | `ask` / `allow` / `deny` |
|
||||
| `doom_loop` | 無限ループ検知のオーバーライド許可 | `ask` / `allow` / `deny` |
|
||||
| `external_directory` | プロジェクトルート外へのファイルアクセス | `ask` / `allow` / `deny` |
|
||||
|
||||
または `~/.config/opencode/oh-my-opencode.json` か `.opencode/oh-my-opencode.json` の `disabled_agents` を使用して無効化できます:
|
||||
|
||||
@@ -826,8 +884,9 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
有効時(デフォルト)、Sisyphus はオプションの特殊エージェントを備えた強力なオーケストレーターを提供します:
|
||||
|
||||
- **Sisyphus**: プライマリオーケストレーターエージェント (Claude Opus 4.5)
|
||||
- **Builder-Sisyphus**: OpenCode のデフォルトビルドエージェント(SDK 制限により名前変更、デフォルトで無効)
|
||||
- **Planner-Sisyphus**: OpenCode のデフォルトプランエージェント(SDK 制限により名前変更、デフォルトで有効)
|
||||
- **OpenCode-Builder**: OpenCode のデフォルトビルドエージェント(SDK 制限により名前変更、デフォルトで無効)
|
||||
- **Prometheus (Planner)**: OpenCode のデフォルトプランエージェント + work-planner 方法論(デフォルトで有効)
|
||||
- **Metis (Plan Consultant)**: 隠された要件と AI 失敗ポイントを特定する事前計画分析エージェント
|
||||
|
||||
**設定オプション:**
|
||||
|
||||
@@ -842,7 +901,7 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
}
|
||||
```
|
||||
|
||||
**例:Builder-Sisyphus を有効化:**
|
||||
**例:OpenCode-Builder を有効化:**
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -852,7 +911,7 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
}
|
||||
```
|
||||
|
||||
これにより、Sisyphus と並行して Builder-Sisyphus エージェントを有効化できます。Sisyphus が有効な場合、デフォルトのビルドエージェントは常にサブエージェントモードに降格されます。
|
||||
これにより、Sisyphus と並行して OpenCode-Builder エージェントを有効化できます。Sisyphus が有効な場合、デフォルトのビルドエージェントは常にサブエージェントモードに降格されます。
|
||||
|
||||
**例:すべての Sisyphus オーケストレーションを無効化:**
|
||||
|
||||
@@ -873,22 +932,59 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"temperature": 0.3
|
||||
},
|
||||
"Builder-Sisyphus": {
|
||||
"OpenCode-Builder": {
|
||||
"model": "anthropic/claude-opus-4"
|
||||
},
|
||||
"Planner-Sisyphus": {
|
||||
"Prometheus (Planner)": {
|
||||
"model": "openai/gpt-5.2"
|
||||
},
|
||||
"Metis (Plan Consultant)": {
|
||||
"model": "anthropic/claude-sonnet-4-5"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| オプション | デフォルト | 説明 |
|
||||
| --------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `disabled` | `false` | `true` の場合、すべての Sisyphus オーケストレーションを無効化し、元の build/plan をプライマリとして復元します。 |
|
||||
| `default_builder_enabled` | `false` | `true` の場合、Builder-Sisyphus エージェントを有効化します(OpenCode build と同じ、SDK 制限により名前変更)。デフォルトでは無効です。 |
|
||||
| `planner_enabled` | `true` | `true` の場合、Planner-Sisyphus エージェントを有効化します(OpenCode plan と同じ、SDK 制限により名前変更)。デフォルトで有効です。 |
|
||||
| `replace_plan` | `true` | `true` の場合、デフォルトのプランエージェントをサブエージェントモードに降格させます。`false` に設定すると、Planner-Sisyphus とデフォルトのプランの両方を利用できます。 |
|
||||
| オプション | デフォルト | 説明 |
|
||||
| ------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `disabled` | `false` | `true` の場合、すべての Sisyphus オーケストレーションを無効化し、元の build/plan をプライマリとして復元します。 |
|
||||
| `default_builder_enabled` | `false` | `true` の場合、OpenCode-Builder エージェントを有効化します(OpenCode build と同じ、SDK 制限により名前変更)。デフォルトでは無効です。 |
|
||||
| `planner_enabled` | `true` | `true` の場合、Prometheus (Planner) エージェントを有効化します(work-planner 方法論を含む)。デフォルトで有効です。 |
|
||||
| `replace_plan` | `true` | `true` の場合、デフォルトのプランエージェントをサブエージェントモードに降格させます。`false` に設定すると、Prometheus (Planner) とデフォルトのプランの両方を利用できます。 |
|
||||
|
||||
### Background Tasks
|
||||
|
||||
バックグラウンドエージェントタスクの同時実行数を設定します。並列で実行できるバックグラウンドエージェントの数を制御します。
|
||||
|
||||
```json
|
||||
{
|
||||
"background_task": {
|
||||
"defaultConcurrency": 5,
|
||||
"providerConcurrency": {
|
||||
"anthropic": 3,
|
||||
"openai": 5,
|
||||
"google": 10
|
||||
},
|
||||
"modelConcurrency": {
|
||||
"anthropic/claude-opus-4-5": 2,
|
||||
"google/gemini-3-flash": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| オプション | デフォルト | 説明 |
|
||||
| --------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `defaultConcurrency` | - | すべてのプロバイダー/モデルに対するデフォルトの最大同時バックグラウンドタスク数 |
|
||||
| `providerConcurrency` | - | プロバイダーごとの同時実行制限。キーはプロバイダー名(例:`anthropic`、`openai`、`google`) |
|
||||
| `modelConcurrency` | - | モデルごとの同時実行制限。キーは完全なモデル名(例:`anthropic/claude-opus-4-5`)。プロバイダー制限より優先されます。 |
|
||||
|
||||
**優先順位**: `modelConcurrency` > `providerConcurrency` > `defaultConcurrency`
|
||||
|
||||
**ユースケース**:
|
||||
- 高価なモデル(例:Opus)を制限してコストの急増を防ぐ
|
||||
- 高速で安価なモデル(例:Gemini Flash)により多くの同時タスクを許可する
|
||||
- プロバイダーレベルの上限を設定してプロバイダーのレートリミットを遵守する
|
||||
|
||||
### Hooks
|
||||
|
||||
@@ -900,23 +996,22 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
|
||||
}
|
||||
```
|
||||
|
||||
利用可能なフック:`todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `empty-message-sanitizer`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`, `preemptive-compaction`
|
||||
利用可能なフック:`todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`, `preemptive-compaction`
|
||||
|
||||
**`auto-update-checker`と`startup-toast`について**: `startup-toast` フックは `auto-update-checker` のサブ機能です。アップデートチェックは有効なまま起動トースト通知のみを無効化するには、`disabled_hooks` に `"startup-toast"` を追加してください。すべてのアップデートチェック機能(トーストを含む)を無効化するには、`"auto-update-checker"` を追加してください。
|
||||
|
||||
### MCPs
|
||||
|
||||
コンテキスト7、Exa、grep.app MCP がデフォルトで有効になっています。
|
||||
Context7、grep.app MCP がデフォルトで有効になっています。
|
||||
|
||||
- **context7**: ライブラリの最新公式ドキュメントを取得
|
||||
- **websearch_exa**: Exa AI を活用したリアルタイムウェブ検索
|
||||
- **grep_app**: [grep.app](https://grep.app) を通じて数百万の公開 GitHub リポジトリから超高速コード検索
|
||||
|
||||
不要であれば、`~/.config/opencode/oh-my-opencode.json` または `.opencode/oh-my-opencode.json` の `disabled_mcps` を使用して無効化できます:
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_mcps": ["context7", "websearch_exa", "grep_app"]
|
||||
"disabled_mcps": ["context7", "grep_app"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -952,7 +1047,6 @@ OpenCode でサポートされるすべての LSP 構成およびカスタム設
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"preemptive_compaction_threshold": 0.85,
|
||||
"truncate_all_tool_outputs": true,
|
||||
"aggressive_truncation": true,
|
||||
"auto_resume": true
|
||||
@@ -960,13 +1054,11 @@ OpenCode でサポートされるすべての LSP 構成およびカスタム設
|
||||
}
|
||||
```
|
||||
|
||||
| オプション | デフォルト | 説明 |
|
||||
| --------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `preemptive_compaction_threshold` | `0.85` | プリエンプティブコンパクションをトリガーする閾値(0.5-0.95)。`preemptive-compaction` フックはデフォルトで有効です。このオプションで閾値をカスタマイズできます。 |
|
||||
| `truncate_all_tool_outputs` | `false` | ホワイトリストのツール(Grep、Glob、LSP、AST-grep)だけでなく、すべてのツール出力を切り詰めます。Tool output truncator はデフォルトで有効です - `disabled_hooks`で無効化できます。 |
|
||||
| `aggressive_truncation` | `false` | トークン制限を超えた場合、ツール出力を積極的に切り詰めて制限内に収めます。デフォルトの切り詰めより積極的です。不十分な場合は要約/復元にフォールバックします。 |
|
||||
| `auto_resume` | `false` | thinking block エラーや thinking disabled violation からの回復成功後、自動的にセッションを再開します。最後のユーザーメッセージを抽出して続行します。 |
|
||||
| `dcp_for_compaction` | `false` | コンパクション用DCP(動的コンテキスト整理)を有効化 - トークン制限超過時に最初に実行されます。コンパクション前に重複したツール呼び出しと古いツール出力を整理します。 |
|
||||
| オプション | デフォルト | 説明 |
|
||||
| --------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `truncate_all_tool_outputs` | `false` | ホワイトリストのツール(Grep、Glob、LSP、AST-grep)だけでなく、すべてのツール出力を切り詰めます。Tool output truncator はデフォルトで有効です - `disabled_hooks`で無効化できます。 |
|
||||
| `aggressive_truncation` | `false` | トークン制限を超えた場合、ツール出力を積極的に切り詰めて制限内に収めます。デフォルトの切り詰めより積極的です。不十分な場合は要約/復元にフォールバックします。 |
|
||||
| `auto_resume` | `false` | thinking block エラーや thinking disabled violation からの回復成功後、自動的にセッションを再開します。最後のユーザーメッセージを抽出して続行します。 |
|
||||
|
||||
**警告**:これらの機能は実験的であり、予期しない動作を引き起こす可能性があります。影響を理解した場合にのみ有効にしてください。
|
||||
|
||||
@@ -1025,5 +1117,8 @@ OpenCode が Debian / ArchLinux だとしたら、Oh My OpenCode は Ubuntu / [O
|
||||
- **Numman Ali** [GitHub](https://github.com/numman-ali) [X](https://x.com/nummanali)
|
||||
- 最初のスポンサー
|
||||
- **Aaron Iker** [GitHub](https://github.com/aaroniker) [X](https://x.com/aaroniker)
|
||||
- **Suyeol Jeon (devxoul)** [GitHub](https://github.com/devxoul)
|
||||
- 私のキャリアをスタートさせてくださった方であり、優れたエージェンティックワークフローをどのように構築できるかについて多大なインスピレーションを与えてくださった方です。優れたチームを作るために優れたシステムをどう設計すべきか多くのことを学び、その学びがこのharnessを作る上で大きな助けとなりました。
|
||||
- **Hyerin Won (devwon)** [GitHub](https://github.com/devwon)
|
||||
|
||||
*素晴らしいヒーロー画像を作成してくれた [@junhoyeo](https://github.com/junhoyeo) に感謝します*
|
||||
|
||||
1022
README.ko.md
1022
README.ko.md
File diff suppressed because it is too large
Load Diff
413
README.md
413
README.md
@@ -1,16 +1,19 @@
|
||||
> [!NOTE]
|
||||
>
|
||||
> *"I aim to spark a software revolution by creating a world where agent-generated code is indistinguishable from human code, yet capable of achieving vastly more. I have poured my personal time, passion, and funds into this journey, and I will continue to do so."*
|
||||
> [](https://sisyphuslabs.ai)
|
||||
> > **We're building a fully productized version of Sisyphus to define the future of frontier agents. <br />Join the waitlist [here](https://sisyphuslabs.ai).**
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://x.com/justsisyphus/status/2006250634354548963)
|
||||
> > **The Orchestrator is coming. This Week. [Get notified on X](https://x.com/justsisyphus/status/2006250634354548963)**
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.7)
|
||||
> > **The Orchestrator is now available in beta. Use `oh-my-opencode@3.0.0-beta.7` to install it.**
|
||||
>
|
||||
> Be with us!
|
||||
>
|
||||
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PWpXmbhF) | Join our [Discord community](https://discord.gg/PWpXmbhF) to connect with contributors and fellow `oh-my-opencode` users. |
|
||||
> | [<img alt="Discord link" src="https://img.shields.io/discord/1452487457085063218?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square" width="156px" />](https://discord.gg/PUwSMR9XNk) | Join our [Discord community](https://discord.gg/PUwSMR9XNk) to connect with contributors and fellow `oh-my-opencode` users. |
|
||||
> | :-----| :----- |
|
||||
> | [<img alt="X link" src="https://img.shields.io/badge/Follow-%40justsisyphus-00CED1?style=flat-square&logo=x&labelColor=black" width="156px" />](https://x.com/justsisyphus) | News and updates for `oh-my-opencode` used to be posted on my X account. <br /> Since it was suspended mistakenly, [@justsisyphus](https://x.com/justsisyphus) now posts updates on my behalf. |
|
||||
> | [<img alt="Sponsor" src="https://img.shields.io/badge/Sponsor-❤-ff69b4?style=flat-square&logo=github-sponsors&labelColor=black" width="156px" />](https://github.com/sponsors/code-yeongyu) | Support the development of `oh-my-opencode` by [becoming a sponsor](https://github.com/sponsors/code-yeongyu). Your contribution helps keep this project alive and growing. |
|
||||
> | [<img alt="GitHub Follow" src="https://img.shields.io/github/followers/code-yeongyu?style=flat-square&logo=github&labelColor=black&color=24292f" width="156px" />](https://github.com/code-yeongyu) | Follow [@code-yeongyu](https://github.com/code-yeongyu) on GitHub for more projects. |
|
||||
|
||||
<!-- <CENTERED SECTION FOR GITHUB DISPLAY> -->
|
||||
|
||||
@@ -25,11 +28,29 @@
|
||||
|
||||
> This is coding on steroids—`oh-my-opencode` in action. Run background agents, call specialized agents like oracle, librarian, and frontend engineer. Use crafted LSP/AST tools, curated MCPs, and a full Claude Code compatibility layer.
|
||||
|
||||
# Claude OAuth Access Notice
|
||||
|
||||
No stupid token consumption massive subagents here. No bloat tools here.
|
||||
## TL;DR
|
||||
|
||||
**Certified, Verified, Tested, Actually Useful Harness in Production, after $24,000 worth of tokens spent.**
|
||||
**START WITH YOUR ChatGPT, Claude, Gemini SUBSCRIPTIONS. WE ALL COVER THEM.**
|
||||
> Q. Can I use oh-my-opencode?
|
||||
|
||||
Yes.
|
||||
|
||||
> Q. Can I use it with my Claude Code subscription?
|
||||
|
||||
Yes, technically possible. But I cannot recommend using it.
|
||||
|
||||
## FULL
|
||||
|
||||
> As of January 2026, Anthropic has restricted third-party OAuth access citing ToS violations.
|
||||
>
|
||||
> [**Anthropic has cited this project, oh-my-opencode as justification for blocking opencode.**](https://x.com/thdxr/status/2010149530486911014)
|
||||
>
|
||||
> Indeed, some plugins that spoof Claude Code's oauth request signatures exist in the community.
|
||||
>
|
||||
> These tools may work regardless of technical detectability, but users should be aware of ToS implications, and I personally cannot recommend to use those.
|
||||
>
|
||||
> This project is not responsible for any issues arising from the use of unofficial tools, and **we do not have any custom implementations of those oauth systems.**
|
||||
|
||||
|
||||
<div align="center">
|
||||
@@ -41,8 +62,9 @@ No stupid token consumption massive subagents here. No bloat tools here.
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/stargazers)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/issues)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/blob/master/LICENSE.md)
|
||||
[](https://deepwiki.com/code-yeongyu/oh-my-opencode)
|
||||
|
||||
[English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
[English](README.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -50,48 +72,61 @@ No stupid token consumption massive subagents here. No bloat tools here.
|
||||
|
||||
## Reviews
|
||||
|
||||
> "It made me cancel my Cursor subscription. Unbelievable things are happening in the open source community." - [Arthur Guiot](https://x.com/arthur_guiot/status/2008736347092382053?s=20)
|
||||
|
||||
> "If Claude Code does in 7 days what a human does in 3 months, Sisyphus does it in 1 hour. It just works until the task is done. It is a discipline agent." — B, Quant Researcher
|
||||
|
||||
> "Knocked out 8000 eslint warnings with Oh My Opencode, just in a day" — [Jacob Ferrari](https://x.com/jacobferrari_/status/2003258761952289061)
|
||||
|
||||
> "I converted a 45k line tauri app into a SaaS web app overnight using Ohmyopencode and ralph loop. Started with interview me prompt, asked it for ratings and recommendations on the questions. It was amazing to watch it work and to wake up this morning to a mostly working website!" - [James Hargis](https://x.com/hargabyte/status/2007299688261882202)
|
||||
|
||||
> "use oh-my-opencode, you will never go back" — [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
|
||||
|
||||
> "I haven't really been able to articulate exactly what makes it so great yet, but the development experience has reached a completely different dimension." - [
|
||||
苔硯:こけすずり](https://x.com/kokesuzuri/status/2008532913961529372?s=20)
|
||||
|
||||
> "Experimenting with open code, oh my opencode and supermemory this weekend to build some minecraft/souls-like abomination."
|
||||
> "Asking it to add crouch animations while I go take my post-lunch walk. [Video]" - [MagiMetal](https://x.com/MagiMetal/status/2005374704178373023)
|
||||
|
||||
> "You guys should pull this into core and recruit him. Seriously. It's really, really, really good." — Henning Kilset
|
||||
|
||||
> "Hire @yeon_gyu_kim if you can convince him, this dude has revolutionized opencode." — [mysticaltech](https://x.com/mysticaltech/status/2001858758608376079)
|
||||
|
||||
> "ok yeah holy shit @androolloyd this thing is legit oh my opencode is sick" — [z80.eth](https://x.com/0xz80/status/2001815226505924791)
|
||||
|
||||
> "use oh-my-opencode, you will never go back" — [d0t3ch](https://x.com/d0t3ch/status/2001685618200580503)
|
||||
|
||||
> "Oh My Opencode is king of the hill and has no contenders" — [RyanOnThePath](https://x.com/RyanOnThePath/status/2001438321252118548)
|
||||
|
||||
> "Isn't the name Sisyphus itself beautiful?" — Sigrid ([@sigridjin_eth](https://x.com/sigridjin_eth))
|
||||
> "Oh My OpenCode Is Actually Insane" - [YouTube - Darren Builds AI](https://www.youtube.com/watch?v=G_Snfh2M41M)
|
||||
|
||||
---
|
||||
|
||||
## Contents
|
||||
|
||||
- [Claude OAuth Access Notice](#claude-oauth-access-notice)
|
||||
- [Reviews](#reviews)
|
||||
- [Contents](#contents)
|
||||
- [Oh My OpenCode](#oh-my-opencode)
|
||||
- [Just Skip Reading This Readme](#just-skip-reading-this-readme)
|
||||
- [It's the Age of Agents](#its-the-age-of-agents)
|
||||
- [🪄 The Magic Word: `ultrawork`](#-the-magic-word-ultrawork)
|
||||
- [For Those Who Want to Read: Meet Sisyphus](#for-those-who-want-to-read-meet-sisyphus)
|
||||
- [Just Install It.](#just-install-it)
|
||||
- [Installation](#installation)
|
||||
- [For Humans](#for-humans)
|
||||
- [For LLM Agents](#for-llm-agents)
|
||||
- [Step 0: Ask user about the setup.](#step-0-ask-user-about-the-setup)
|
||||
- [Step 1: Install OpenCode, if not](#step-1-install-opencode-if-not)
|
||||
- [Step 2: Configure oh-my-opencode Plugin](#step-2-configure-oh-my-opencode-plugin)
|
||||
- [JSONC](#jsonc)
|
||||
- [JSON](#json)
|
||||
- [Step 0: Ask user about subscriptions](#step-0-ask-user-about-subscriptions)
|
||||
- [Step 1: Install OpenCode (if not installed)](#step-1-install-opencode-if-not-installed)
|
||||
- [Step 2: Run the installer](#step-2-run-the-installer)
|
||||
- [Step 3: Verify Setup](#step-3-verify-setup)
|
||||
- [Step 4: Configure Authentication](#step-4-configure-authentication)
|
||||
- [Anthropic (Claude)](#anthropic-claude)
|
||||
- [Google Gemini (Antigravity OAuth)](#google-gemini-antigravity-oauth)
|
||||
- [OpenAI (ChatGPT Plus/Pro)](#openai-chatgpt-pluspro)
|
||||
- [Model Configuration](#model-configuration)
|
||||
- [oh-my-opencode Agent Model Override](#oh-my-opencode-agent-model-override)
|
||||
- [GitHub Copilot (Fallback Provider)](#github-copilot-fallback-provider)
|
||||
- [Model Mappings](#model-mappings)
|
||||
- [Setup](#setup)
|
||||
- [⚠️ Warning](#️-warning)
|
||||
- [Verify the setup](#verify-the-setup)
|
||||
- [Say 'Congratulations! 🎉' to the user](#say-congratulations--to-the-user)
|
||||
- [Free advertising](#free-advertising)
|
||||
- [Ask for a Star ⭐](#ask-for-a-star-)
|
||||
- [Too Complicated?](#too-complicated)
|
||||
- [Uninstallation](#uninstallation)
|
||||
- [Features](#features)
|
||||
@@ -99,9 +134,11 @@ No stupid token consumption massive subagents here. No bloat tools here.
|
||||
- [Background Agents: Work Like a Team](#background-agents-work-like-a-team)
|
||||
- [The Tools: Your Teammates Deserve Better](#the-tools-your-teammates-deserve-better)
|
||||
- [Why Are You the Only One Using an IDE?](#why-are-you-the-only-one-using-an-ide)
|
||||
- [Session Management](#session-management)
|
||||
- [Context Is All You Need](#context-is-all-you-need)
|
||||
- [Be Multimodal. Save Tokens.](#be-multimodal-save-tokens)
|
||||
- [I Removed Their Blockers](#i-removed-their-blockers)
|
||||
- [Skill-Embedded MCP Support](#skill-embedded-mcp-support)
|
||||
- [Goodbye Claude Code. Hello Oh My OpenCode.](#goodbye-claude-code-hello-oh-my-opencode)
|
||||
- [Hooks Integration](#hooks-integration)
|
||||
- [Config Loaders](#config-loaders)
|
||||
@@ -109,21 +146,28 @@ No stupid token consumption massive subagents here. No bloat tools here.
|
||||
- [Compatibility Toggles](#compatibility-toggles)
|
||||
- [Not Just for the Agents](#not-just-for-the-agents)
|
||||
- [Configuration](#configuration)
|
||||
- [JSONC Support](#jsonc-support)
|
||||
- [Google Auth](#google-auth)
|
||||
- [Agents](#agents)
|
||||
- [Permission Options](#permission-options)
|
||||
- [Built-in Skills](#built-in-skills)
|
||||
- [Git Master](#git-master)
|
||||
- [Sisyphus Agent](#sisyphus-agent)
|
||||
- [Background Tasks](#background-tasks)
|
||||
- [Categories](#categories)
|
||||
- [Hooks](#hooks)
|
||||
- [MCPs](#mcps)
|
||||
- [LSP](#lsp)
|
||||
- [Experimental](#experimental)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Author's Note](#authors-note)
|
||||
- [Warnings](#warnings)
|
||||
- [Loved by professionals at](#loved-by-professionals-at)
|
||||
- [Sponsors](#sponsors)
|
||||
|
||||
# Oh My OpenCode
|
||||
|
||||
oMoMoMoMoMo···
|
||||
|
||||
Meet Sisyphus: The Batteries-Included Agent that codes like you.
|
||||
|
||||
[Claude Code](https://www.claude.com/product/claude-code) is great.
|
||||
But if you're a hacker, you'll fall head over heels for [OpenCode](https://github.com/sst/opencode).
|
||||
@@ -191,8 +235,17 @@ Meet our main agent: Sisyphus (Opus 4.5 High). Below are the tools Sisyphus uses
|
||||
|
||||
Just by installing this, you make your agents to work like:
|
||||
|
||||
1. While Gemini 3 Pro writes the frontend as a background task, Claude Opus 4.5 handles the backend. Stuck debugging? Call GPT 5.2 for help. When the frontend reports done, verify and ship.
|
||||
2. Need to look something up? It scours official docs, your entire codebase history, and public GitHub implementations—using not just grep but built-in LSP tools and AST-Grep.
|
||||
1. Sisyphus doesn't waste time hunting for files himself; he keeps the main agent's context lean. Instead, he fires off background tasks to faster, cheaper models in parallel to map the territory for him.
|
||||
1. Sisyphus leverages LSP for refactoring; it's more deterministic, safer, and surgical.
|
||||
1. When the heavy lifting requires a UI touch, Sisyphus delegates frontend tasks directly to Gemini 3 Pro.
|
||||
1. If Sisyphus gets stuck in a loop or hits a wall, he doesn't keep banging his head—he calls GPT 5.2 for high-IQ strategic backup.
|
||||
1. Working with a complex open-source framework? Sisyphus spawns subagents to digest the raw source code and documentation in real-time. He operates with total contextual awareness.
|
||||
1. When Sisyphus touches comments, he either justifies their existence or nukes them. He keeps your codebase clean.
|
||||
1. Sisyphus is bound by his TODO list. If he doesn't finish what he started, the system forces him back into "bouldering" mode. Your task gets done, period.
|
||||
1. Honestly, don't even bother reading the docs. Just write your prompt. Include the 'ultrawork' keyword. Sisyphus will analyze the structure, gather the context, dig through external source code, and just keep bouldering until the job is 100% complete.
|
||||
1. Actually, typing 'ultrawork' is too much effort. Just type 'ulw'. Just ulw. Sip your coffee. Your work is done.
|
||||
|
||||
Need to look something up? It scours official docs, your entire codebase history, and public GitHub implementations—using not just grep but built-in LSP tools and AST-Grep.
|
||||
3. Stop worrying about context management when delegating to LLMs. I've got it covered.
|
||||
- OhMyOpenCode aggressively leverages multiple agents to lighten the context load.
|
||||
- **Your agent is now the dev team lead. You're the AI Manager.**
|
||||
@@ -209,12 +262,14 @@ If you don't want all this, as mentioned, you can just pick and choose specific
|
||||
Run the interactive installer:
|
||||
|
||||
```bash
|
||||
bunx oh-my-opencode install
|
||||
# or use npx if bunx doesn't work
|
||||
npx oh-my-opencode install
|
||||
# or with bun
|
||||
bunx oh-my-opencode install
|
||||
```
|
||||
|
||||
> **Note for Ubuntu/Debian users**: If you installed Bun via Snap (`/snap/bin/bun`), `bunx` will fail with "script not found" due to Snap's sandboxing. Either use `npx` instead, or reinstall Bun via the official installer: `curl -fsSL https://bun.sh/install | bash`
|
||||
> **Note**: The CLI ships with standalone binaries for all major platforms. No runtime (Bun/Node.js) is required for CLI execution after installation.
|
||||
>
|
||||
> **Supported platforms**: macOS (ARM64, x64), Linux (x64, ARM64, Alpine/musl), Windows (x64)
|
||||
|
||||
Follow the prompts to configure your Claude, ChatGPT, and Gemini subscriptions. After installation, authenticate your providers as instructed.
|
||||
|
||||
@@ -315,7 +370,7 @@ First, add the opencode-antigravity-auth plugin:
|
||||
{
|
||||
"plugin": [
|
||||
"oh-my-opencode",
|
||||
"opencode-antigravity-auth@1.1.2"
|
||||
"opencode-antigravity-auth@1.2.8"
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -327,20 +382,19 @@ Read the [opencode-antigravity-auth documentation](https://github.com/NoeFabris/
|
||||
|
||||
##### oh-my-opencode Agent Model Override
|
||||
|
||||
The `opencode-antigravity-auth` plugin uses different model names than the built-in Google auth. Override the agent models in `oh-my-opencode.json` (or `.opencode/oh-my-opencode.json`) and disable the built-in `google_auth`:
|
||||
The `opencode-antigravity-auth` plugin uses different model names than the built-in Google auth. Override the agent models in `oh-my-opencode.json` (or `.opencode/oh-my-opencode.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"google_auth": false,
|
||||
"agents": {
|
||||
"frontend-ui-ux-engineer": { "model": "google/gemini-3-pro-high" },
|
||||
"document-writer": { "model": "google/gemini-3-flash" },
|
||||
"multimodal-looker": { "model": "google/gemini-3-flash" }
|
||||
"frontend-ui-ux-engineer": { "model": "google/antigravity-gemini-3-pro-high" },
|
||||
"document-writer": { "model": "google/antigravity-gemini-3-flash" },
|
||||
"multimodal-looker": { "model": "google/antigravity-gemini-3-flash" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available model names**: `google/gemini-3-pro-high`, `google/gemini-3-pro-medium`, `google/gemini-3-pro-low`, `google/gemini-3-flash`, `google/gemini-3-flash`, `google/gemini-3-flash-lite`, `google/claude-sonnet-4-5`, `google/claude-sonnet-4-5-thinking`, `google/claude-opus-4-5-thinking`, `google/gpt-oss-120b-medium`
|
||||
**Available model names**: `google/antigravity-gemini-3-pro-high`, `google/antigravity-gemini-3-pro-low`, `google/antigravity-gemini-3-flash`, `google/antigravity-claude-sonnet-4-5`, `google/antigravity-claude-sonnet-4-5-thinking-low`, `google/antigravity-claude-sonnet-4-5-thinking-medium`, `google/antigravity-claude-sonnet-4-5-thinking-high`, `google/antigravity-claude-opus-4-5-thinking-low`, `google/antigravity-claude-opus-4-5-thinking-medium`, `google/antigravity-claude-opus-4-5-thinking-high`, `google/gemini-3-pro-preview`, `google/gemini-3-flash-preview`, `google/gemini-2.5-pro`, `google/gemini-2.5-flash`
|
||||
|
||||
Then authenticate:
|
||||
|
||||
@@ -355,44 +409,46 @@ opencode auth login
|
||||
|
||||
**Multi-Account Load Balancing**: The plugin supports up to 10 Google accounts. When one account hits rate limits, it automatically switches to the next available account.
|
||||
|
||||
#### OpenAI (ChatGPT Plus/Pro)
|
||||
#### GitHub Copilot (Fallback Provider)
|
||||
|
||||
First, add the opencode-openai-codex-auth plugin:
|
||||
GitHub Copilot is supported as a **fallback provider** when native providers (Claude, ChatGPT, Gemini) are unavailable. The installer configures Copilot with lower priority than native providers.
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": [
|
||||
"oh-my-opencode",
|
||||
"opencode-openai-codex-auth@4.1.1"
|
||||
]
|
||||
}
|
||||
**Priority**: Native providers (Claude/ChatGPT/Gemini) > GitHub Copilot > Free models
|
||||
|
||||
##### Model Mappings
|
||||
|
||||
When GitHub Copilot is enabled, oh-my-opencode uses these model assignments:
|
||||
|
||||
| Agent | Model |
|
||||
| ------------- | -------------------------------- |
|
||||
| **Sisyphus** | `github-copilot/claude-opus-4.5` |
|
||||
| **Oracle** | `github-copilot/gpt-5.2` |
|
||||
| **Explore** | `grok code` (default) |
|
||||
| **Librarian** | `glm 4.7 free` (default) |
|
||||
|
||||
GitHub Copilot acts as a proxy provider, routing requests to underlying models based on your subscription.
|
||||
|
||||
##### Setup
|
||||
|
||||
Run the installer and select "Yes" for GitHub Copilot:
|
||||
|
||||
```bash
|
||||
bunx oh-my-opencode install
|
||||
# Select your subscriptions (Claude, ChatGPT, Gemini)
|
||||
# When prompted: "Do you have a GitHub Copilot subscription?" → Select "Yes"
|
||||
```
|
||||
|
||||
**Important**: The official npm package currently has a bug causing 400 errors (`"No tool call found for function call output with call_id"`). **Use the hotfix branch** until fixed. Edit `~/.config/opencode/package.json`:
|
||||
Or use non-interactive mode:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"opencode-openai-codex-auth": "code-yeongyu/opencode-openai-codex-auth#fix/orphaned-function-call-output-with-tools"
|
||||
}
|
||||
}
|
||||
```bash
|
||||
bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no --copilot=yes
|
||||
```
|
||||
|
||||
Then run `cd ~/.config/opencode && bun i`. In `opencode.json`, use `"opencode-openai-codex-auth"` without the version suffix.
|
||||
|
||||
##### Model Configuration
|
||||
You'll also need full model settings in `opencode.json`.
|
||||
Read the [opencode-openai-codex-auth documentation](https://github.com/numman-ali/opencode-openai-codex-auth), copy provider/models config from [`config/full-opencode.json`](https://github.com/numman-ali/opencode-openai-codex-auth/blob/main/config/full-opencode.json), and merge carefully to avoid breaking the user's existing setup.
|
||||
|
||||
Then authenticate:
|
||||
Then authenticate with GitHub:
|
||||
|
||||
```bash
|
||||
opencode auth login
|
||||
# Interactive Terminal: Provider: Select OpenAI
|
||||
# Interactive Terminal: Login method: Select ChatGPT Plus/Pro (Codex Subscription)
|
||||
# Interactive Terminal: Guide user through OAuth flow in browser
|
||||
# Wait for completion
|
||||
# Verify success and confirm with user
|
||||
# Select: GitHub → Authenticate via OAuth
|
||||
```
|
||||
|
||||
|
||||
@@ -400,7 +456,7 @@ opencode auth login
|
||||
|
||||
**Unless the user explicitly requests it, do not change model settings or disable features (agents, hooks, MCPs).**
|
||||
|
||||
The plugin works perfectly by default. Except for the recommended `google_auth: true` setting, do not change other settings or turn off features without an explicit request.
|
||||
The plugin works perfectly by default. Do not change settings or turn off features without an explicit request.
|
||||
|
||||
### Verify the setup
|
||||
|
||||
@@ -478,9 +534,9 @@ To remove oh-my-opencode:
|
||||
|
||||
- **Sisyphus** (`anthropic/claude-opus-4-5`): **The default agent.** A powerful AI orchestrator for OpenCode. Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Emphasizes background task delegation and todo-driven workflow. Uses Claude Opus 4.5 with extended thinking (32k budget) for maximum reasoning capability.
|
||||
- **oracle** (`openai/gpt-5.2`): Architecture, code review, strategy. Uses GPT-5.2 for its stellar logical reasoning and deep analysis. Inspired by AmpCode.
|
||||
- **librarian** (`anthropic/claude-sonnet-4-5` or `google/gemini-3-flash`): Multi-repo analysis, doc lookup, implementation examples. Uses Gemini 3 Flash when Antigravity auth is configured, otherwise Claude Sonnet 4.5 for deep codebase understanding and GitHub research with evidence-based answers. Inspired by AmpCode.
|
||||
- **librarian** (`opencode/glm-4.7-free`): Multi-repo analysis, doc lookup, implementation examples. Uses GLM-4.7 Free for deep codebase understanding and GitHub research with evidence-based answers. Inspired by AmpCode.
|
||||
- **explore** (`opencode/grok-code`, `google/gemini-3-flash`, or `anthropic/claude-haiku-4-5`): Fast codebase exploration and pattern matching. Uses Gemini 3 Flash when Antigravity auth is configured, Haiku when Claude max20 is available, otherwise Grok. Inspired by Claude Code.
|
||||
- **frontend-ui-ux-engineer** (`google/gemini-3-pro-high`): A designer turned developer. Builds gorgeous UIs. Gemini excels at creative, beautiful UI code.
|
||||
- **frontend-ui-ux-engineer** (`google/gemini-3-pro-preview`): A designer turned developer. Builds gorgeous UIs. Gemini excels at creative, beautiful UI code.
|
||||
- **document-writer** (`google/gemini-3-flash`): Technical writing expert. Gemini is a wordsmith—writes prose that flows.
|
||||
- **multimodal-looker** (`google/gemini-3-flash`): Visual content specialist. Analyzes PDFs, images, diagrams to extract information.
|
||||
|
||||
@@ -522,20 +578,13 @@ Syntax highlighting, autocomplete, refactoring, navigation, analysis—and now a
|
||||
The features in your editor? Other agents can't touch them.
|
||||
Hand your best tools to your best colleagues. Now they can properly refactor, navigate, and analyze.
|
||||
|
||||
- **lsp_hover**: Type info, docs, signatures at position
|
||||
- **lsp_goto_definition**: Jump to symbol definition
|
||||
- **lsp_find_references**: Find all usages across workspace
|
||||
- **lsp_document_symbols**: Get file symbol outline
|
||||
- **lsp_workspace_symbols**: Search symbols by name across project
|
||||
- **lsp_diagnostics**: Get errors/warnings before build
|
||||
- **lsp_servers**: List available LSP servers
|
||||
- **lsp_prepare_rename**: Validate rename operation
|
||||
- **lsp_rename**: Rename symbol across workspace
|
||||
- **lsp_code_actions**: Get available quick fixes/refactorings
|
||||
- **lsp_code_action_resolve**: Apply code action
|
||||
- **ast_grep_search**: AST-aware code pattern search (25 languages)
|
||||
- **ast_grep_replace**: AST-aware code replacement
|
||||
- **call_omo_agent**: Spawn specialized explore/librarian agents. Supports `run_in_background` parameter for async execution.
|
||||
- **delegate_task**: Category-based task delegation with specialized agents. Supports pre-configured categories (visual, business-logic) or direct agent targeting. Use `background_output` to retrieve results and `background_cancel` to cancel tasks. See [Categories](#categories).
|
||||
|
||||
#### Session Management
|
||||
|
||||
@@ -575,8 +624,8 @@ These tools enable agents to reference previous conversations and maintain conti
|
||||
- Use camelCase for function names
|
||||
```
|
||||
- **Online**: Project rules aren't everything. Built-in MCPs for extended capabilities:
|
||||
- **websearch**: Real-time web search powered by [Exa AI](https://exa.ai)
|
||||
- **context7**: Official documentation lookup
|
||||
- **websearch_exa**: Real-time web search
|
||||
- **grep_app**: Ultra-fast code search across public GitHub repos (great for finding implementation examples)
|
||||
|
||||
#### Be Multimodal. Save Tokens.
|
||||
@@ -681,21 +730,38 @@ Disable specific Claude Code compatibility features with the `claude_code` confi
|
||||
"commands": false,
|
||||
"skills": false,
|
||||
"agents": false,
|
||||
"hooks": false
|
||||
"hooks": false,
|
||||
"plugins": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Toggle | When `false`, stops loading from... | Unaffected |
|
||||
| ---------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------- |
|
||||
| `mcp` | `~/.claude/.mcp.json`, `./.mcp.json`, `./.claude/.mcp.json` | Built-in MCP (context7, websearch_exa) |
|
||||
| `mcp` | `~/.claude/.mcp.json`, `./.mcp.json`, `./.claude/.mcp.json` | Built-in MCP (context7, grep_app) |
|
||||
| `commands` | `~/.claude/commands/*.md`, `./.claude/commands/*.md` | `~/.config/opencode/command/`, `./.opencode/command/` |
|
||||
| `skills` | `~/.claude/skills/*/SKILL.md`, `./.claude/skills/*/SKILL.md` | - |
|
||||
| `agents` | `~/.claude/agents/*.md`, `./.claude/agents/*.md` | Built-in agents (oracle, librarian, etc.) |
|
||||
| `hooks` | `~/.claude/settings.json`, `./.claude/settings.json`, `./.claude/settings.local.json` | - |
|
||||
| `plugins` | `~/.claude/plugins/` (Claude Code marketplace plugins) | - |
|
||||
|
||||
All toggles default to `true` (enabled). Omit the `claude_code` object for full Claude Code compatibility.
|
||||
|
||||
**Selectively disable specific plugins** using `plugins_override`:
|
||||
|
||||
```json
|
||||
{
|
||||
"claude_code": {
|
||||
"plugins_override": {
|
||||
"claude-mem@thedotmack": false,
|
||||
"some-other-plugin@marketplace": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This allows you to keep the plugin system enabled while disabling specific plugins by their full identifier (`plugin-name@marketplace-name`).
|
||||
|
||||
### Not Just for the Agents
|
||||
|
||||
When agents thrive, you thrive. But I want to help you directly too.
|
||||
@@ -738,10 +804,10 @@ Config file locations (priority order):
|
||||
1. `.opencode/oh-my-opencode.json` (project)
|
||||
2. User config (platform-specific):
|
||||
|
||||
| Platform | User Config Path |
|
||||
|----------|------------------|
|
||||
| **Windows** | `~/.config/opencode/oh-my-opencode.json` (preferred) or `%APPDATA%\opencode\oh-my-opencode.json` (fallback) |
|
||||
| **macOS/Linux** | `~/.config/opencode/oh-my-opencode.json` |
|
||||
| Platform | User Config Path |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| **Windows** | `~/.config/opencode/oh-my-opencode.json` (preferred) or `%APPDATA%\opencode\oh-my-opencode.json` (fallback) |
|
||||
| **macOS/Linux** | `~/.config/opencode/oh-my-opencode.json` |
|
||||
|
||||
Schema autocomplete supported:
|
||||
|
||||
@@ -765,10 +831,7 @@ When both `oh-my-opencode.jsonc` and `oh-my-opencode.json` files exist, `.jsonc`
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
|
||||
// Enable Google Gemini via Antigravity OAuth
|
||||
"google_auth": false,
|
||||
|
||||
|
||||
/* Agent overrides - customize models for specific tasks */
|
||||
"agents": {
|
||||
"oracle": {
|
||||
@@ -783,28 +846,7 @@ When both `oh-my-opencode.jsonc` and `oh-my-opencode.json` files exist, `.jsonc`
|
||||
|
||||
### Google Auth
|
||||
|
||||
**Recommended**: Use the external [`opencode-antigravity-auth`](https://github.com/NoeFabris/opencode-antigravity-auth) plugin. It provides multi-account load balancing, more models (including Claude via Antigravity), and active maintenance. See [Installation > Google Gemini](#google-gemini-antigravity-oauth).
|
||||
|
||||
When using `opencode-antigravity-auth`, disable the built-in auth and override agent models in `oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"google_auth": false,
|
||||
"agents": {
|
||||
"frontend-ui-ux-engineer": { "model": "google/gemini-3-pro-high" },
|
||||
"document-writer": { "model": "google/gemini-3-flash" },
|
||||
"multimodal-looker": { "model": "google/gemini-3-flash" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative**: Enable built-in Antigravity OAuth (single account, Gemini models only):
|
||||
|
||||
```json
|
||||
{
|
||||
"google_auth": true
|
||||
}
|
||||
```
|
||||
**Recommended**: For Google Gemini authentication, install the [`opencode-antigravity-auth`](https://github.com/NoeFabris/opencode-antigravity-auth) plugin. It provides multi-account load balancing, more models (including Claude via Antigravity), and active maintenance. See [Installation > Google Gemini](#google-gemini-antigravity-oauth).
|
||||
|
||||
### Agents
|
||||
|
||||
@@ -881,6 +923,7 @@ Available agents: `oracle`, `librarian`, `explore`, `frontend-ui-ux-engineer`, `
|
||||
Oh My OpenCode includes built-in skills that provide additional capabilities:
|
||||
|
||||
- **playwright**: Browser automation with Playwright MCP. Use for web scraping, testing, screenshots, and browser interactions.
|
||||
- **git-master**: Git expert for atomic commits, rebase/squash, and history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with `delegate_task(category='quick', skills=['git-master'], ...)` to save context.
|
||||
|
||||
Disable built-in skills via `disabled_skills` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
@@ -890,15 +933,34 @@ Disable built-in skills via `disabled_skills` in `~/.config/opencode/oh-my-openc
|
||||
}
|
||||
```
|
||||
|
||||
Available built-in skills: `playwright`
|
||||
Available built-in skills: `playwright`, `git-master`
|
||||
|
||||
### Git Master
|
||||
|
||||
Configure git-master skill behavior:
|
||||
|
||||
```json
|
||||
{
|
||||
"git_master": {
|
||||
"commit_footer": true,
|
||||
"include_co_authored_by": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------------------ | ------- | -------------------------------------------------------------------------------- |
|
||||
| `commit_footer` | `true` | Adds "Ultraworked with Sisyphus" footer to commit messages. |
|
||||
| `include_co_authored_by` | `true` | Adds `Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>` trailer to commits. |
|
||||
|
||||
### Sisyphus Agent
|
||||
|
||||
When enabled (default), Sisyphus provides a powerful orchestrator with optional specialized agents:
|
||||
|
||||
- **Sisyphus**: Primary orchestrator agent (Claude Opus 4.5)
|
||||
- **Builder-Sisyphus**: OpenCode's default build agent, renamed due to SDK limitations (disabled by default)
|
||||
- **Planner-Sisyphus**: OpenCode's default plan agent, renamed due to SDK limitations (enabled by default)
|
||||
- **OpenCode-Builder**: OpenCode's default build agent, renamed due to SDK limitations (disabled by default)
|
||||
- **Prometheus (Planner)**: OpenCode's default plan agent with work-planner methodology (enabled by default)
|
||||
- **Metis (Plan Consultant)**: Pre-planning analysis agent that identifies hidden requirements and AI failure points
|
||||
|
||||
**Configuration Options:**
|
||||
|
||||
@@ -913,7 +975,7 @@ When enabled (default), Sisyphus provides a powerful orchestrator with optional
|
||||
}
|
||||
```
|
||||
|
||||
**Example: Enable Builder-Sisyphus:**
|
||||
**Example: Enable OpenCode-Builder:**
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -923,7 +985,7 @@ When enabled (default), Sisyphus provides a powerful orchestrator with optional
|
||||
}
|
||||
```
|
||||
|
||||
This enables Builder-Sisyphus agent alongside Sisyphus. The default build agent is always demoted to subagent mode when Sisyphus is enabled.
|
||||
This enables OpenCode-Builder agent alongside Sisyphus. The default build agent is always demoted to subagent mode when Sisyphus is enabled.
|
||||
|
||||
**Example: Disable all Sisyphus orchestration:**
|
||||
|
||||
@@ -944,22 +1006,103 @@ You can also customize Sisyphus agents like other agents:
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"temperature": 0.3
|
||||
},
|
||||
"Builder-Sisyphus": {
|
||||
"OpenCode-Builder": {
|
||||
"model": "anthropic/claude-opus-4"
|
||||
},
|
||||
"Planner-Sisyphus": {
|
||||
"Prometheus (Planner)": {
|
||||
"model": "openai/gpt-5.2"
|
||||
},
|
||||
"Metis (Plan Consultant)": {
|
||||
"model": "anthropic/claude-sonnet-4-5"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| --------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `disabled` | `false` | When `true`, disables all Sisyphus orchestration and restores original build/plan as primary. |
|
||||
| `default_builder_enabled` | `false` | When `true`, enables Builder-Sisyphus agent (same as OpenCode build, renamed due to SDK limitations). Disabled by default. |
|
||||
| `planner_enabled` | `true` | When `true`, enables Planner-Sisyphus agent (same as OpenCode plan, renamed due to SDK limitations). Enabled by default. |
|
||||
| `replace_plan` | `true` | When `true`, demotes default plan agent to subagent mode. Set to `false` to keep both Planner-Sisyphus and default plan available. |
|
||||
| Option | Default | Description |
|
||||
| ------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `disabled` | `false` | When `true`, disables all Sisyphus orchestration and restores original build/plan as primary. |
|
||||
| `default_builder_enabled` | `false` | When `true`, enables OpenCode-Builder agent (same as OpenCode build, renamed due to SDK limitations). Disabled by default. |
|
||||
| `planner_enabled` | `true` | When `true`, enables Prometheus (Planner) agent with work-planner methodology. Enabled by default. |
|
||||
| `replace_plan` | `true` | When `true`, demotes default plan agent to subagent mode. Set to `false` to keep both Prometheus (Planner) and default plan available. |
|
||||
|
||||
### Background Tasks
|
||||
|
||||
Configure concurrency limits for background agent tasks. This controls how many parallel background agents can run simultaneously.
|
||||
|
||||
```json
|
||||
{
|
||||
"background_task": {
|
||||
"defaultConcurrency": 5,
|
||||
"providerConcurrency": {
|
||||
"anthropic": 3,
|
||||
"openai": 5,
|
||||
"google": 10
|
||||
},
|
||||
"modelConcurrency": {
|
||||
"anthropic/claude-opus-4-5": 2,
|
||||
"google/gemini-3-flash": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `defaultConcurrency` | - | Default maximum concurrent background tasks for all providers/models |
|
||||
| `providerConcurrency` | - | Per-provider concurrency limits. Keys are provider names (e.g., `anthropic`, `openai`, `google`) |
|
||||
| `modelConcurrency` | - | Per-model concurrency limits. Keys are full model names (e.g., `anthropic/claude-opus-4-5`). Overrides provider limits. |
|
||||
|
||||
**Priority Order**: `modelConcurrency` > `providerConcurrency` > `defaultConcurrency`
|
||||
|
||||
**Use Cases**:
|
||||
- Limit expensive models (e.g., Opus) to prevent cost spikes
|
||||
- Allow more concurrent tasks for fast/cheap models (e.g., Gemini Flash)
|
||||
- Respect provider rate limits by setting provider-level caps
|
||||
|
||||
### Categories
|
||||
|
||||
Categories enable domain-specific task delegation via the `delegate_task` tool. Each category applies runtime presets (model, temperature, prompt additions) when calling the `Sisyphus-Junior` agent.
|
||||
|
||||
**Default Categories:**
|
||||
|
||||
| Category | Model | Description |
|
||||
| ---------------- | ----------------------------- | ---------------------------------------------------------------------------- |
|
||||
| `visual` | `google/gemini-3-pro-preview` | Frontend, UI/UX, design-focused tasks. High creativity (temp 0.7). |
|
||||
| `business-logic` | `openai/gpt-5.2` | Backend logic, architecture, strategic reasoning. Low creativity (temp 0.1). |
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
// Via delegate_task tool
|
||||
delegate_task(category="visual", prompt="Create a responsive dashboard component")
|
||||
delegate_task(category="business-logic", prompt="Design the payment processing flow")
|
||||
|
||||
// Or target a specific agent directly
|
||||
delegate_task(agent="oracle", prompt="Review this architecture")
|
||||
```
|
||||
|
||||
**Custom Categories:**
|
||||
|
||||
Add custom categories in `oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"categories": {
|
||||
"data-science": {
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"temperature": 0.2,
|
||||
"prompt_append": "Focus on data analysis, ML pipelines, and statistical methods."
|
||||
},
|
||||
"visual": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"prompt_append": "Use shadcn/ui components and Tailwind CSS."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each category supports: `model`, `temperature`, `top_p`, `maxTokens`, `thinking`, `reasoningEffort`, `textVerbosity`, `tools`, `prompt_append`.
|
||||
|
||||
### Hooks
|
||||
|
||||
@@ -971,23 +1114,23 @@ Disable specific built-in hooks via `disabled_hooks` in `~/.config/opencode/oh-m
|
||||
}
|
||||
```
|
||||
|
||||
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `empty-message-sanitizer`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`, `preemptive-compaction`
|
||||
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-context-window-limit-recovery`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`, `interactive-bash-session`, `compaction-context-injector`, `thinking-block-validator`, `claude-code-hooks`, `ralph-loop`, `preemptive-compaction`
|
||||
|
||||
**Note on `auto-update-checker` and `startup-toast`**: The `startup-toast` hook is a sub-feature of `auto-update-checker`. To disable only the startup toast notification while keeping update checking enabled, add `"startup-toast"` to `disabled_hooks`. To disable all update checking features (including the toast), add `"auto-update-checker"` to `disabled_hooks`.
|
||||
|
||||
### MCPs
|
||||
|
||||
Context7, Exa, and grep.app MCP enabled by default.
|
||||
Exa, Context7 and grep.app MCP enabled by default.
|
||||
|
||||
- **websearch**: Real-time web search powered by [Exa AI](https://exa.ai) - searches the web and returns relevant content
|
||||
- **context7**: Fetches up-to-date official documentation for libraries
|
||||
- **websearch_exa**: Real-time web search powered by Exa AI
|
||||
- **grep_app**: Ultra-fast code search across millions of public GitHub repositories via [grep.app](https://grep.app)
|
||||
|
||||
Don't want them? Disable via `disabled_mcps` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_mcps": ["context7", "websearch_exa", "grep_app"]
|
||||
"disabled_mcps": ["websearch", "context7", "grep_app"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1023,7 +1166,6 @@ Opt-in experimental features that may change or be removed in future versions. U
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"preemptive_compaction_threshold": 0.85,
|
||||
"truncate_all_tool_outputs": true,
|
||||
"aggressive_truncation": true,
|
||||
"auto_resume": true
|
||||
@@ -1031,16 +1173,20 @@ Opt-in experimental features that may change or be removed in future versions. U
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| --------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `preemptive_compaction_threshold` | `0.85` | Threshold percentage (0.5-0.95) to trigger preemptive compaction. The `preemptive-compaction` hook is enabled by default; this option customizes the threshold. |
|
||||
| `truncate_all_tool_outputs` | `false` | Truncates ALL tool outputs instead of just whitelisted tools (Grep, Glob, LSP, AST-grep). Tool output truncator is enabled by default - disable via `disabled_hooks`. |
|
||||
| Option | Default | Description |
|
||||
| --------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `truncate_all_tool_outputs` | `false` | Truncates ALL tool outputs instead of just whitelisted tools (Grep, Glob, LSP, AST-grep). Tool output truncator is enabled by default - disable via `disabled_hooks`. |
|
||||
| `aggressive_truncation` | `false` | When token limit is exceeded, aggressively truncates tool outputs to fit within limits. More aggressive than the default truncation behavior. Falls back to summarize/revert if insufficient. |
|
||||
| `auto_resume` | `false` | Automatically resumes session after successful recovery from thinking block errors or thinking disabled violations. Extracts the last user message and continues. |
|
||||
| `dcp_for_compaction` | `false` | Enable DCP (Dynamic Context Pruning) for compaction - runs first when token limit exceeded. Prunes duplicate tool calls and old tool outputs before running compaction. |
|
||||
| `auto_resume` | `false` | Automatically resumes session after successful recovery from thinking block errors or thinking disabled violations. Extracts the last user message and continues. |
|
||||
|
||||
**Warning**: These features are experimental and may cause unexpected behavior. Enable only if you understand the implications.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `OPENCODE_CONFIG_DIR` | Override the OpenCode configuration directory. Useful for profile isolation with tools like [OCX](https://github.com/kdcokenny/ocx) ghost mode. |
|
||||
|
||||
|
||||
## Author's Note
|
||||
|
||||
@@ -1096,5 +1242,8 @@ I have no affiliation with any project or model mentioned here. This is purely p
|
||||
- **Numman Ali** [GitHub](https://github.com/numman-ali) [X](https://x.com/nummanali)
|
||||
- The first sponsor
|
||||
- **Aaron Iker** [GitHub](https://github.com/aaroniker) [X](https://x.com/aaroniker)
|
||||
- **Suyeol Jeon (devxoul)** [GitHub](https://github.com/devxoul)
|
||||
- The person who launched my career and inspired me deeply on how to build great agentic workflows. I learned so much about designing great systems to build great teams, and those lessons were instrumental in creating this harness.
|
||||
- **Hyerin Won (devwon)** [GitHub](https://github.com/devwon)
|
||||
|
||||
*Special thanks to [@junhoyeo](https://github.com/junhoyeo) for this amazing hero image.*
|
||||
|
||||
1209
README.zh-cn.md
1209
README.zh-cn.md
File diff suppressed because it is too large
Load Diff
@@ -12,11 +12,7 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"websearch_exa",
|
||||
"context7",
|
||||
"grep_app"
|
||||
]
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"disabled_agents": {
|
||||
@@ -30,7 +26,10 @@
|
||||
"explore",
|
||||
"frontend-ui-ux-engineer",
|
||||
"document-writer",
|
||||
"multimodal-looker"
|
||||
"multimodal-looker",
|
||||
"Metis (Plan Consultant)",
|
||||
"Momus (Plan Reviewer)",
|
||||
"orchestrator-sisyphus"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -39,7 +38,9 @@
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"playwright"
|
||||
"playwright",
|
||||
"frontend-ui-ux",
|
||||
"git-master"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -68,14 +69,16 @@
|
||||
"agent-usage-reminder",
|
||||
"non-interactive-env",
|
||||
"interactive-bash-session",
|
||||
"empty-message-sanitizer",
|
||||
"thinking-block-validator",
|
||||
"ralph-loop",
|
||||
"preemptive-compaction",
|
||||
"compaction-context-injector",
|
||||
"claude-code-hooks",
|
||||
"auto-slash-command",
|
||||
"edit-error-recovery"
|
||||
"edit-error-recovery",
|
||||
"delegate-task-retry",
|
||||
"prometheus-md-only",
|
||||
"start-work",
|
||||
"sisyphus-orchestrator"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -84,7 +87,8 @@
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"init-deep"
|
||||
"init-deep",
|
||||
"start-work"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -97,6 +101,18 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
@@ -211,6 +227,18 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
@@ -325,6 +353,144 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 2
|
||||
},
|
||||
"top_p": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"prompt_append": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"disable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"subagent",
|
||||
"primary",
|
||||
"all"
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"pattern": "^#[0-9A-Fa-f]{6}$"
|
||||
},
|
||||
"permission": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"edit": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"bash": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"webfetch": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"doom_loop": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"external_directory": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Sisyphus-Junior": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
@@ -439,6 +605,18 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
@@ -547,12 +725,276 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Planner-Sisyphus": {
|
||||
"Prometheus (Planner)": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 2
|
||||
},
|
||||
"top_p": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"prompt_append": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"disable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"subagent",
|
||||
"primary",
|
||||
"all"
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"pattern": "^#[0-9A-Fa-f]{6}$"
|
||||
},
|
||||
"permission": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"edit": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"bash": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"webfetch": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"doom_loop": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"external_directory": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Metis (Plan Consultant)": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 2
|
||||
},
|
||||
"top_p": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"prompt_append": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"disable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"subagent",
|
||||
"primary",
|
||||
"all"
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"pattern": "^#[0-9A-Fa-f]{6}$"
|
||||
},
|
||||
"permission": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"edit": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"bash": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"webfetch": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"doom_loop": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"external_directory": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Momus (Plan Reviewer)": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
@@ -667,6 +1109,18 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
@@ -781,6 +1235,18 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
@@ -895,6 +1361,18 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
@@ -1009,6 +1487,18 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
@@ -1123,6 +1613,18 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
@@ -1237,6 +1739,144 @@
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 2
|
||||
},
|
||||
"top_p": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"prompt_append": {
|
||||
"type": "string"
|
||||
},
|
||||
"tools": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"disable": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"subagent",
|
||||
"primary",
|
||||
"all"
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"pattern": "^#[0-9A-Fa-f]{6}$"
|
||||
},
|
||||
"permission": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"edit": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"bash": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"webfetch": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"doom_loop": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
},
|
||||
"external_directory": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ask",
|
||||
"allow",
|
||||
"deny"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"orchestrator-sisyphus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
@@ -1347,6 +1987,85 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"categories": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 2
|
||||
},
|
||||
"top_p": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"maxTokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"thinking": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"enabled",
|
||||
"disabled"
|
||||
]
|
||||
},
|
||||
"budgetTokens": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"reasoningEffort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"prompt_append": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"model"
|
||||
]
|
||||
}
|
||||
},
|
||||
"claude_code": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1379,9 +2098,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"google_auth": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sisyphus_agent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1416,14 +2132,6 @@
|
||||
"auto_resume": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"preemptive_compaction": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"preemptive_compaction_threshold": {
|
||||
"type": "number",
|
||||
"minimum": 0.5,
|
||||
"maximum": 0.95
|
||||
},
|
||||
"truncate_all_tool_outputs": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1464,7 +2172,6 @@
|
||||
"todowrite",
|
||||
"todoread",
|
||||
"lsp_rename",
|
||||
"lsp_code_action_resolve",
|
||||
"session_read",
|
||||
"session_write",
|
||||
"session_search"
|
||||
@@ -1517,9 +2224,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dcp_for_compaction": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1662,6 +2366,60 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"background_task": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"defaultConcurrency": {
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
},
|
||||
"providerConcurrency": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"modelConcurrency": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"staleTimeoutMs": {
|
||||
"type": "number",
|
||||
"minimum": 60000
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"force_enable": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"git_master": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"commit_footer": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"include_co_authored_by": {
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
bin/oh-my-opencode.js
Normal file
80
bin/oh-my-opencode.js
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
// bin/oh-my-opencode.js
|
||||
// Wrapper script that detects platform and spawns the correct binary
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { getPlatformPackage, getBinaryPath } from "./platform.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/**
|
||||
* Detect libc family on Linux
|
||||
* @returns {string | null} 'glibc', 'musl', or null if detection fails
|
||||
*/
|
||||
function getLibcFamily() {
|
||||
if (process.platform !== "linux") {
|
||||
return undefined; // Not needed on non-Linux
|
||||
}
|
||||
|
||||
try {
|
||||
const detectLibc = require("detect-libc");
|
||||
return detectLibc.familySync();
|
||||
} catch {
|
||||
// detect-libc not available
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const { platform, arch } = process;
|
||||
const libcFamily = getLibcFamily();
|
||||
|
||||
// Get platform package name
|
||||
let pkg;
|
||||
try {
|
||||
pkg = getPlatformPackage({ platform, arch, libcFamily });
|
||||
} catch (error) {
|
||||
console.error(`\noh-my-opencode: ${error.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Resolve binary path
|
||||
const binRelPath = getBinaryPath(pkg, platform);
|
||||
|
||||
let binPath;
|
||||
try {
|
||||
binPath = require.resolve(binRelPath);
|
||||
} catch {
|
||||
console.error(`\noh-my-opencode: Platform binary not installed.`);
|
||||
console.error(`\nYour platform: ${platform}-${arch}${libcFamily === "musl" ? "-musl" : ""}`);
|
||||
console.error(`Expected package: ${pkg}`);
|
||||
console.error(`\nTo fix, run:`);
|
||||
console.error(` npm install ${pkg}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Spawn the binary
|
||||
const result = spawnSync(binPath, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
// Handle spawn errors
|
||||
if (result.error) {
|
||||
console.error(`\noh-my-opencode: Failed to execute binary.`);
|
||||
console.error(`Error: ${result.error.message}\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Handle signals
|
||||
if (result.signal) {
|
||||
const signalNum = result.signal === "SIGTERM" ? 15 :
|
||||
result.signal === "SIGKILL" ? 9 :
|
||||
result.signal === "SIGINT" ? 2 : 1;
|
||||
process.exit(128 + signalNum);
|
||||
}
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
|
||||
main();
|
||||
38
bin/platform.js
Normal file
38
bin/platform.js
Normal file
@@ -0,0 +1,38 @@
|
||||
// bin/platform.js
|
||||
// Shared platform detection module - used by wrapper and postinstall
|
||||
|
||||
/**
|
||||
* Get the platform-specific package name
|
||||
* @param {{ platform: string, arch: string, libcFamily?: string | null }} options
|
||||
* @returns {string} Package name like "oh-my-opencode-darwin-arm64"
|
||||
* @throws {Error} If libc cannot be detected on Linux
|
||||
*/
|
||||
export function getPlatformPackage({ platform, arch, libcFamily }) {
|
||||
let suffix = "";
|
||||
if (platform === "linux") {
|
||||
if (libcFamily === null || libcFamily === undefined) {
|
||||
throw new Error(
|
||||
"Could not detect libc on Linux. " +
|
||||
"Please ensure detect-libc is installed or report this issue."
|
||||
);
|
||||
}
|
||||
if (libcFamily === "musl") {
|
||||
suffix = "-musl";
|
||||
}
|
||||
}
|
||||
|
||||
// Map platform names: win32 -> windows (for package name)
|
||||
const os = platform === "win32" ? "windows" : platform;
|
||||
return `oh-my-opencode-${os}-${arch}${suffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the binary within a platform package
|
||||
* @param {string} pkg Package name
|
||||
* @param {string} platform Process platform
|
||||
* @returns {string} Relative path like "oh-my-opencode-darwin-arm64/bin/oh-my-opencode"
|
||||
*/
|
||||
export function getBinaryPath(pkg, platform) {
|
||||
const ext = platform === "win32" ? ".exe" : "";
|
||||
return `${pkg}/bin/oh-my-opencode${ext}`;
|
||||
}
|
||||
148
bin/platform.test.ts
Normal file
148
bin/platform.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
// bin/platform.test.ts
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { getPlatformPackage, getBinaryPath } from "./platform.js";
|
||||
|
||||
describe("getPlatformPackage", () => {
|
||||
// #region Darwin platforms
|
||||
test("returns darwin-arm64 for macOS ARM64", () => {
|
||||
// #given macOS ARM64 platform
|
||||
const input = { platform: "darwin", arch: "arm64" };
|
||||
|
||||
// #when getting platform package
|
||||
const result = getPlatformPackage(input);
|
||||
|
||||
// #then returns correct package name
|
||||
expect(result).toBe("oh-my-opencode-darwin-arm64");
|
||||
});
|
||||
|
||||
test("returns darwin-x64 for macOS Intel", () => {
|
||||
// #given macOS x64 platform
|
||||
const input = { platform: "darwin", arch: "x64" };
|
||||
|
||||
// #when getting platform package
|
||||
const result = getPlatformPackage(input);
|
||||
|
||||
// #then returns correct package name
|
||||
expect(result).toBe("oh-my-opencode-darwin-x64");
|
||||
});
|
||||
// #endregion
|
||||
|
||||
// #region Linux glibc platforms
|
||||
test("returns linux-x64 for Linux x64 with glibc", () => {
|
||||
// #given Linux x64 with glibc
|
||||
const input = { platform: "linux", arch: "x64", libcFamily: "glibc" };
|
||||
|
||||
// #when getting platform package
|
||||
const result = getPlatformPackage(input);
|
||||
|
||||
// #then returns correct package name
|
||||
expect(result).toBe("oh-my-opencode-linux-x64");
|
||||
});
|
||||
|
||||
test("returns linux-arm64 for Linux ARM64 with glibc", () => {
|
||||
// #given Linux ARM64 with glibc
|
||||
const input = { platform: "linux", arch: "arm64", libcFamily: "glibc" };
|
||||
|
||||
// #when getting platform package
|
||||
const result = getPlatformPackage(input);
|
||||
|
||||
// #then returns correct package name
|
||||
expect(result).toBe("oh-my-opencode-linux-arm64");
|
||||
});
|
||||
// #endregion
|
||||
|
||||
// #region Linux musl platforms
|
||||
test("returns linux-x64-musl for Alpine x64", () => {
|
||||
// #given Linux x64 with musl (Alpine)
|
||||
const input = { platform: "linux", arch: "x64", libcFamily: "musl" };
|
||||
|
||||
// #when getting platform package
|
||||
const result = getPlatformPackage(input);
|
||||
|
||||
// #then returns correct package name with musl suffix
|
||||
expect(result).toBe("oh-my-opencode-linux-x64-musl");
|
||||
});
|
||||
|
||||
test("returns linux-arm64-musl for Alpine ARM64", () => {
|
||||
// #given Linux ARM64 with musl (Alpine)
|
||||
const input = { platform: "linux", arch: "arm64", libcFamily: "musl" };
|
||||
|
||||
// #when getting platform package
|
||||
const result = getPlatformPackage(input);
|
||||
|
||||
// #then returns correct package name with musl suffix
|
||||
expect(result).toBe("oh-my-opencode-linux-arm64-musl");
|
||||
});
|
||||
// #endregion
|
||||
|
||||
// #region Windows platform
|
||||
test("returns windows-x64 for Windows", () => {
|
||||
// #given Windows x64 platform (win32 is Node's platform name)
|
||||
const input = { platform: "win32", arch: "x64" };
|
||||
|
||||
// #when getting platform package
|
||||
const result = getPlatformPackage(input);
|
||||
|
||||
// #then returns correct package name with 'windows' not 'win32'
|
||||
expect(result).toBe("oh-my-opencode-windows-x64");
|
||||
});
|
||||
// #endregion
|
||||
|
||||
// #region Error cases
|
||||
test("throws error for Linux with null libcFamily", () => {
|
||||
// #given Linux platform with null libc detection
|
||||
const input = { platform: "linux", arch: "x64", libcFamily: null };
|
||||
|
||||
// #when getting platform package
|
||||
// #then throws descriptive error
|
||||
expect(() => getPlatformPackage(input)).toThrow("Could not detect libc");
|
||||
});
|
||||
|
||||
test("throws error for Linux with undefined libcFamily", () => {
|
||||
// #given Linux platform with undefined libc
|
||||
const input = { platform: "linux", arch: "x64", libcFamily: undefined };
|
||||
|
||||
// #when getting platform package
|
||||
// #then throws descriptive error
|
||||
expect(() => getPlatformPackage(input)).toThrow("Could not detect libc");
|
||||
});
|
||||
// #endregion
|
||||
});
|
||||
|
||||
describe("getBinaryPath", () => {
|
||||
test("returns path without .exe for Unix platforms", () => {
|
||||
// #given Unix platform package
|
||||
const pkg = "oh-my-opencode-darwin-arm64";
|
||||
const platform = "darwin";
|
||||
|
||||
// #when getting binary path
|
||||
const result = getBinaryPath(pkg, platform);
|
||||
|
||||
// #then returns path without extension
|
||||
expect(result).toBe("oh-my-opencode-darwin-arm64/bin/oh-my-opencode");
|
||||
});
|
||||
|
||||
test("returns path with .exe for Windows", () => {
|
||||
// #given Windows platform package
|
||||
const pkg = "oh-my-opencode-windows-x64";
|
||||
const platform = "win32";
|
||||
|
||||
// #when getting binary path
|
||||
const result = getBinaryPath(pkg, platform);
|
||||
|
||||
// #then returns path with .exe extension
|
||||
expect(result).toBe("oh-my-opencode-windows-x64/bin/oh-my-opencode.exe");
|
||||
});
|
||||
|
||||
test("returns path without .exe for Linux", () => {
|
||||
// #given Linux platform package
|
||||
const pkg = "oh-my-opencode-linux-x64";
|
||||
const platform = "linux";
|
||||
|
||||
// #when getting binary path
|
||||
const result = getBinaryPath(pkg, platform);
|
||||
|
||||
// #then returns path without extension
|
||||
expect(result).toBe("oh-my-opencode-linux-x64/bin/oh-my-opencode");
|
||||
});
|
||||
});
|
||||
45
bun.lock
45
bun.lock
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "oh-my-opencode",
|
||||
@@ -11,12 +11,14 @@
|
||||
"@code-yeongyu/comment-checker": "^0.6.1",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@openauthjs/openauth": "^0.4.3",
|
||||
"@opencode-ai/plugin": "^1.0.162",
|
||||
"@opencode-ai/sdk": "^1.0.162",
|
||||
"@opencode-ai/plugin": "^1.1.19",
|
||||
"@opencode-ai/sdk": "^1.1.19",
|
||||
"commander": "^14.0.2",
|
||||
"detect-libc": "^2.0.0",
|
||||
"hono": "^4.10.4",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"open": "^11.0.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.2",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
@@ -28,6 +30,15 @@
|
||||
"bun-types": "latest",
|
||||
"typescript": "^5.7.3",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-darwin-x64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-linux-arm64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.0.0-beta.8",
|
||||
"oh-my-opencode-linux-x64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-linux-x64-musl": "3.0.0-beta.8",
|
||||
"oh-my-opencode-windows-x64": "3.0.0-beta.8",
|
||||
},
|
||||
},
|
||||
},
|
||||
"trustedDependencies": [
|
||||
@@ -84,9 +95,9 @@
|
||||
|
||||
"@openauthjs/openauth": ["@openauthjs/openauth@0.4.3", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw=="],
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.0.162", "", { "dependencies": { "@opencode-ai/sdk": "1.0.162", "zod": "4.1.8" } }, "sha512-tiJw7SCfSlG/3tY2O0J2UT06OLuazOzsv1zYlFbLxLy/EVedtW0pzxYalO20a4e//vInvOXFkhd2jLyB5vNEVA=="],
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.1.19", "", { "dependencies": { "@opencode-ai/sdk": "1.1.19", "zod": "4.1.8" } }, "sha512-Q6qBEjHb/dJMEw4BUqQxEswTMxCCHUpFMMb6jR8HTTs8X/28XRkKt5pHNPA82GU65IlSoPRph+zd8LReBDN53Q=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.0.162", "", {}, "sha512-+XqRErBUt9eb1m3i/7WkZc/QCKCCjTaGV3MvhLhs/CUwbUn767D/ugzcG/i2ec8j/4nQmjJbjPDRmrQfvF1Qjw=="],
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.19", "", {}, "sha512-XhZhFuvlLCqDpvNtUEjOsi/wvFj3YCXb1dySp+OONQRMuHlorNYnNa7P2A2ntKuhRdGT1Xt5na0nFzlUyNw+4A=="],
|
||||
|
||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||
|
||||
@@ -122,6 +133,8 @@
|
||||
|
||||
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
@@ -144,6 +157,12 @@
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="],
|
||||
|
||||
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
|
||||
|
||||
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
@@ -204,8 +223,16 @@
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
|
||||
|
||||
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
@@ -240,6 +267,8 @@
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
@@ -252,6 +281,8 @@
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
|
||||
@@ -264,6 +295,8 @@
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
@@ -304,6 +337,8 @@
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
|
||||
|
||||
"xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="],
|
||||
|
||||
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||
|
||||
2
bunfig.toml
Normal file
2
bunfig.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[test]
|
||||
preload = ["./test-setup.ts"]
|
||||
200
docs/category-skill-guide.md
Normal file
200
docs/category-skill-guide.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Category & Skill System Guide
|
||||
|
||||
This document provides a comprehensive guide to the **Category** and **Skill** systems, which form the extensibility core of Oh-My-OpenCode.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Instead of delegating everything to a single AI agent, it's far more efficient to invoke **specialists** tailored to the nature of the task.
|
||||
|
||||
- **Category**: "What kind of work is this?" (determines model, temperature, prompt mindset)
|
||||
- **Skill**: "What tools and knowledge are needed?" (injects specialized knowledge, MCP tools, workflows)
|
||||
|
||||
By combining these two concepts, you can generate optimal agents through `delegate_task`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Category System
|
||||
|
||||
A Category is an agent configuration preset optimized for specific domains.
|
||||
|
||||
### Available Built-in Categories
|
||||
|
||||
| Category | Optimal Model | Characteristics | Use Cases |
|
||||
|----------|---------------|-----------------|-----------|
|
||||
| `visual-engineering` | `gemini-3-pro` | High creativity (Temp 0.7) | Frontend, UI/UX, animations, styling |
|
||||
| `ultrabrain` | `gpt-5.2` | Maximum logical reasoning (Temp 0.1) | Architecture design, complex business logic, debugging |
|
||||
| `artistry` | `gemini-3-pro` | Artistic (Temp 0.9) | Creative ideation, design concepts, storytelling |
|
||||
| `quick` | `claude-haiku` | Fast (Temp 0.3) | Simple tasks, refactoring, script writing |
|
||||
| `writing` | `gemini-3-flash` | Natural flow (Temp 0.5) | Documentation, technical blogs, README writing |
|
||||
| `most-capable` | `claude-opus` | High performance (Temp 0.1) | Extremely difficult complex tasks |
|
||||
|
||||
### Usage
|
||||
|
||||
Specify the `category` parameter when invoking the `delegate_task` tool.
|
||||
|
||||
```typescript
|
||||
delegate_task(
|
||||
category="visual-engineering",
|
||||
prompt="Add a responsive chart component to the dashboard page"
|
||||
)
|
||||
```
|
||||
|
||||
### Sisyphus-Junior (Delegated Executor)
|
||||
|
||||
When you use a Category, a special agent called **Sisyphus-Junior** performs the work.
|
||||
- **Characteristic**: Cannot **re-delegate** tasks to other agents.
|
||||
- **Purpose**: Prevents infinite delegation loops and ensures focus on the assigned task.
|
||||
|
||||
---
|
||||
|
||||
## 3. Skill System
|
||||
|
||||
A Skill is a mechanism that injects **specialized knowledge (Context)** and **tools (MCP)** for specific domains into agents.
|
||||
|
||||
### Built-in Skills
|
||||
|
||||
1. **`git-master`**
|
||||
- **Capabilities**: Git expert. Detects commit styles, splits atomic commits, formulates rebase strategies.
|
||||
- **MCP**: None (uses Git commands)
|
||||
- **Usage**: Essential for commits, history searches, branch management.
|
||||
|
||||
2. **`playwright`**
|
||||
- **Capabilities**: Browser automation. Web page testing, screenshots, scraping.
|
||||
- **MCP**: `@playwright/mcp` (auto-executed)
|
||||
- **Usage**: For post-implementation UI verification, E2E test writing.
|
||||
|
||||
3. **`frontend-ui-ux`**
|
||||
- **Capabilities**: Injects designer mindset. Color, typography, motion guidelines.
|
||||
- **Usage**: For aesthetic UI work beyond simple implementation.
|
||||
|
||||
### Usage
|
||||
|
||||
Add desired skill names to the `skills` array.
|
||||
|
||||
```typescript
|
||||
delegate_task(
|
||||
category="quick",
|
||||
skills=["git-master"],
|
||||
prompt="Commit current changes. Follow commit message style."
|
||||
)
|
||||
```
|
||||
|
||||
### Skill Customization (SKILL.md)
|
||||
|
||||
You can add custom skills directly to `.opencode/skills/` in your project root or `~/.claude/skills/` in your home directory.
|
||||
|
||||
**Example: `.opencode/skills/my-skill/SKILL.md`**
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-skill
|
||||
description: My special custom skill
|
||||
mcp:
|
||||
my-mcp:
|
||||
command: npx
|
||||
args: ["-y", "my-mcp-server"]
|
||||
---
|
||||
|
||||
# My Skill Prompt
|
||||
|
||||
This content will be injected into the agent's system prompt.
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Combination Strategies (Combos)
|
||||
|
||||
You can create powerful specialized agents by combining Categories and Skills.
|
||||
|
||||
### 🎨 The Designer (UI Implementation)
|
||||
- **Category**: `visual-engineering`
|
||||
- **Skills**: `["frontend-ui-ux", "playwright"]`
|
||||
- **Effect**: Implements aesthetic UI and verifies rendering results directly in browser.
|
||||
|
||||
### 🏗️ The Architect (Design Review)
|
||||
- **Category**: `ultrabrain`
|
||||
- **Skills**: `[]` (pure reasoning)
|
||||
- **Effect**: Leverages GPT-5.2's logical reasoning for in-depth system architecture analysis.
|
||||
|
||||
### ⚡ The Maintainer (Quick Fixes)
|
||||
- **Category**: `quick`
|
||||
- **Skills**: `["git-master"]`
|
||||
- **Effect**: Uses cost-effective models to quickly fix code and generate clean commits.
|
||||
|
||||
---
|
||||
|
||||
## 5. delegate_task Prompt Guide
|
||||
|
||||
When delegating, **clear and specific** prompts are essential. Include these 7 elements:
|
||||
|
||||
1. **TASK**: What needs to be done? (single objective)
|
||||
2. **EXPECTED OUTCOME**: What is the deliverable?
|
||||
3. **REQUIRED SKILLS**: Which skills should be used?
|
||||
4. **REQUIRED TOOLS**: Which tools must be used? (whitelist)
|
||||
5. **MUST DO**: What must be done (constraints)
|
||||
6. **MUST NOT DO**: What must never be done
|
||||
7. **CONTEXT**: File paths, existing patterns, reference materials
|
||||
|
||||
**Bad Example**:
|
||||
> "Fix this"
|
||||
|
||||
**Good Example**:
|
||||
> **TASK**: Fix mobile layout breaking issue in `LoginButton.tsx`
|
||||
> **CONTEXT**: `src/components/LoginButton.tsx`, using Tailwind CSS
|
||||
> **MUST DO**: Change flex-direction at `md:` breakpoint
|
||||
> **MUST NOT DO**: Modify existing desktop layout
|
||||
> **EXPECTED**: Buttons align vertically on mobile
|
||||
|
||||
---
|
||||
|
||||
## 6. Configuration Guide (oh-my-opencode.json)
|
||||
|
||||
You can fine-tune categories in `oh-my-opencode.json`.
|
||||
|
||||
### Category Configuration Schema (CategoryConfig)
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `model` | string | AI model ID to use (e.g., `anthropic/claude-opus-4-5`) |
|
||||
| `temperature` | number | Creativity level (0.0 ~ 2.0). Lower is more deterministic. |
|
||||
| `prompt_append` | string | Content to append to system prompt when this category is selected |
|
||||
| `thinking` | object | Thinking model configuration (`{ type: "enabled", budgetTokens: 16000 }`) |
|
||||
| `tools` | object | Tool usage control (disable with `{ "tool_name": false }`) |
|
||||
| `maxTokens` | number | Maximum response token count |
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"categories": {
|
||||
// 1. Define new custom category
|
||||
"korean-writer": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"temperature": 0.5,
|
||||
"prompt_append": "You are a Korean technical writer. Maintain a friendly and clear tone."
|
||||
},
|
||||
|
||||
// 2. Override existing category (change model)
|
||||
"visual-engineering": {
|
||||
"model": "openai/gpt-5.2", // Can change model
|
||||
"temperature": 0.8
|
||||
},
|
||||
|
||||
// 3. Configure thinking model and restrict tools
|
||||
"deep-reasoning": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
"budgetTokens": 32000
|
||||
},
|
||||
"tools": {
|
||||
"websearch_web_search_exa": false // Disable web search
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Disable skills
|
||||
"disabled_skills": ["playwright"]
|
||||
}
|
||||
```
|
||||
272
docs/cli-guide.md
Normal file
272
docs/cli-guide.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# Oh-My-OpenCode CLI Guide
|
||||
|
||||
This document provides a comprehensive guide to using the Oh-My-OpenCode CLI tools.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Oh-My-OpenCode provides CLI tools accessible via the `bunx oh-my-opencode` command. The CLI supports various features including plugin installation, environment diagnostics, and session execution.
|
||||
|
||||
```bash
|
||||
# Basic execution (displays help)
|
||||
bunx oh-my-opencode
|
||||
|
||||
# Or run with npx
|
||||
npx oh-my-opencode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Available Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `install` | Interactive Setup Wizard |
|
||||
| `doctor` | Environment diagnostics and health checks |
|
||||
| `run` | OpenCode session runner |
|
||||
| `auth` | Google Antigravity authentication management |
|
||||
| `version` | Display version information |
|
||||
|
||||
---
|
||||
|
||||
## 3. `install` - Interactive Setup Wizard
|
||||
|
||||
An interactive installation tool for initial Oh-My-OpenCode setup. Provides a beautiful TUI (Text User Interface) based on `@clack/prompts`.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
bunx oh-my-opencode install
|
||||
```
|
||||
|
||||
### Installation Process
|
||||
|
||||
1. **Provider Selection**: Choose your AI provider from Claude, ChatGPT, or Gemini.
|
||||
2. **API Key Input**: Enter the API key for your selected provider.
|
||||
3. **Configuration File Creation**: Generates `opencode.json` or `oh-my-opencode.json` files.
|
||||
4. **Plugin Registration**: Automatically registers the oh-my-opencode plugin in OpenCode settings.
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--no-tui` | Run in non-interactive mode without TUI (for CI/CD environments) |
|
||||
| `--verbose` | Display detailed logs |
|
||||
|
||||
---
|
||||
|
||||
## 4. `doctor` - Environment Diagnostics
|
||||
|
||||
Diagnoses your environment to ensure Oh-My-OpenCode is functioning correctly. Performs 17+ health checks.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
bunx oh-my-opencode doctor
|
||||
```
|
||||
|
||||
### Diagnostic Categories
|
||||
|
||||
| Category | Check Items |
|
||||
|----------|-------------|
|
||||
| **Installation** | OpenCode version (>= 1.0.150), plugin registration status |
|
||||
| **Configuration** | Configuration file validity, JSONC parsing |
|
||||
| **Authentication** | Anthropic, OpenAI, Google API key validity |
|
||||
| **Dependencies** | Bun, Node.js, Git installation status |
|
||||
| **Tools** | LSP server status, MCP server status |
|
||||
| **Updates** | Latest version check |
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--category <name>` | Check specific category only (e.g., `--category authentication`) |
|
||||
| `--json` | Output results in JSON format |
|
||||
| `--verbose` | Include detailed information |
|
||||
|
||||
### Example Output
|
||||
|
||||
```
|
||||
oh-my-opencode doctor
|
||||
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ Oh-My-OpenCode Doctor │
|
||||
└──────────────────────────────────────────────────┘
|
||||
|
||||
Installation
|
||||
✓ OpenCode version: 1.0.155 (>= 1.0.150)
|
||||
✓ Plugin registered in opencode.json
|
||||
|
||||
Configuration
|
||||
✓ oh-my-opencode.json is valid
|
||||
⚠ categories.visual-engineering: using default model
|
||||
|
||||
Authentication
|
||||
✓ Anthropic API key configured
|
||||
✓ OpenAI API key configured
|
||||
✗ Google API key not found
|
||||
|
||||
Dependencies
|
||||
✓ Bun 1.2.5 installed
|
||||
✓ Node.js 22.0.0 installed
|
||||
✓ Git 2.45.0 installed
|
||||
|
||||
Summary: 10 passed, 1 warning, 1 failed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. `run` - OpenCode Session Runner
|
||||
|
||||
Executes OpenCode sessions and monitors task completion.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
bunx oh-my-opencode run [prompt]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--enforce-completion` | Keep session active until all TODOs are completed |
|
||||
| `--timeout <seconds>` | Set maximum execution time |
|
||||
|
||||
---
|
||||
|
||||
## 6. `auth` - Authentication Management
|
||||
|
||||
Manages Google Antigravity OAuth authentication. Required for using Gemini models.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Login
|
||||
bunx oh-my-opencode auth login
|
||||
|
||||
# Logout
|
||||
bunx oh-my-opencode auth logout
|
||||
|
||||
# Check current status
|
||||
bunx oh-my-opencode auth status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Configuration Files
|
||||
|
||||
The CLI searches for configuration files in the following locations (in priority order):
|
||||
|
||||
1. **Project Level**: `.opencode/oh-my-opencode.json`
|
||||
2. **User Level**: `~/.config/opencode/oh-my-opencode.json`
|
||||
|
||||
### JSONC Support
|
||||
|
||||
Configuration files support **JSONC (JSON with Comments)** format. You can use comments and trailing commas.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
// Agent configuration
|
||||
"sisyphus_agent": {
|
||||
"disabled": false,
|
||||
"planner_enabled": true,
|
||||
},
|
||||
|
||||
/* Category customization */
|
||||
"categories": {
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Troubleshooting
|
||||
|
||||
### "OpenCode version too old" Error
|
||||
|
||||
```bash
|
||||
# Update OpenCode
|
||||
npm install -g opencode@latest
|
||||
# or
|
||||
bun install -g opencode@latest
|
||||
```
|
||||
|
||||
### "Plugin not registered" Error
|
||||
|
||||
```bash
|
||||
# Reinstall plugin
|
||||
bunx oh-my-opencode install
|
||||
```
|
||||
|
||||
### Doctor Check Failures
|
||||
|
||||
```bash
|
||||
# Diagnose with detailed information
|
||||
bunx oh-my-opencode doctor --verbose
|
||||
|
||||
# Check specific category only
|
||||
bunx oh-my-opencode doctor --category authentication
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Non-Interactive Mode
|
||||
|
||||
Use the `--no-tui` option for CI/CD environments.
|
||||
|
||||
```bash
|
||||
# Run doctor in CI environment
|
||||
bunx oh-my-opencode doctor --no-tui --json
|
||||
|
||||
# Save results to file
|
||||
bunx oh-my-opencode doctor --json > doctor-report.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Developer Information
|
||||
|
||||
### CLI Structure
|
||||
|
||||
```
|
||||
src/cli/
|
||||
├── index.ts # Commander.js-based main entry
|
||||
├── install.ts # @clack/prompts-based TUI installer
|
||||
├── config-manager.ts # JSONC parsing, multi-source config management
|
||||
├── doctor/ # Health check system
|
||||
│ ├── index.ts # Doctor command entry
|
||||
│ └── checks/ # 17+ individual check modules
|
||||
├── run/ # Session runner
|
||||
└── commands/auth.ts # Authentication management
|
||||
```
|
||||
|
||||
### Adding New Doctor Checks
|
||||
|
||||
1. Create `src/cli/doctor/checks/my-check.ts`:
|
||||
|
||||
```typescript
|
||||
import type { DoctorCheck } from "../types"
|
||||
|
||||
export const myCheck: DoctorCheck = {
|
||||
name: "my-check",
|
||||
category: "environment",
|
||||
check: async () => {
|
||||
// Check logic
|
||||
const isOk = await someValidation()
|
||||
|
||||
return {
|
||||
status: isOk ? "pass" : "fail",
|
||||
message: isOk ? "Everything looks good" : "Something is wrong",
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
2. Register in `src/cli/doctor/checks/index.ts`:
|
||||
|
||||
```typescript
|
||||
export { myCheck } from "./my-check"
|
||||
```
|
||||
152
docs/orchestration-guide.md
Normal file
152
docs/orchestration-guide.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Oh-My-OpenCode Orchestration Guide
|
||||
|
||||
## TL;DR - When to Use What
|
||||
|
||||
| Complexity | Approach | When to Use |
|
||||
|------------|----------|-------------|
|
||||
| **Simple** | Just prompt | Simple tasks, quick fixes, single-file changes |
|
||||
| **Complex + Lazy** | Just type `ulw` or `ultrawork` | Complex tasks where explaining context is tedious. Agent figures it out. |
|
||||
| **Complex + Precise** | `@plan` → `/start-work` | Precise, multi-step work requiring true orchestration. Prometheus plans, Sisyphus executes. |
|
||||
|
||||
**Decision Flow:**
|
||||
```
|
||||
Is it a quick fix or simple task?
|
||||
└─ YES → Just prompt normally
|
||||
└─ NO → Is explaining the full context tedious?
|
||||
└─ YES → Type "ulw" and let the agent figure it out
|
||||
└─ NO → Do you need precise, verifiable execution?
|
||||
└─ YES → Use @plan for Prometheus planning, then /start-work
|
||||
└─ NO → Just use "ulw"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
This document provides a comprehensive guide to the orchestration system that implements Oh-My-OpenCode's core philosophy: **"Separation of Planning and Execution"**.
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Traditional AI agents often mix planning and execution, leading to context pollution, goal drift, and AI slop (low-quality code).
|
||||
|
||||
Oh-My-OpenCode solves this by clearly separating two roles:
|
||||
|
||||
1. **Prometheus (Planner)**: A pure strategist who never writes code. Establishes perfect plans through interviews and analysis.
|
||||
2. **Sisyphus (Executor)**: An orchestrator who executes plans. Delegates work to specialized agents and never stops until completion.
|
||||
|
||||
---
|
||||
|
||||
## 2. Overall Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
User[User Request] --> Prometheus
|
||||
|
||||
subgraph Planning Phase
|
||||
Prometheus[Prometheus<br>Planner] --> Metis[Metis<br>Consultant]
|
||||
Metis --> Prometheus
|
||||
Prometheus --> Momus[Momus<br>Reviewer]
|
||||
Momus --> Prometheus
|
||||
Prometheus --> PlanFile["/.sisyphus/plans/{name}.md"]
|
||||
end
|
||||
|
||||
PlanFile --> StartWork[//start-work/]
|
||||
StartWork --> BoulderState[boulder.json]
|
||||
|
||||
subgraph Execution Phase
|
||||
BoulderState --> Sisyphus[Sisyphus<br>Orchestrator]
|
||||
Sisyphus --> Oracle[Oracle]
|
||||
Sisyphus --> Frontend[Frontend<br>Engineer]
|
||||
Sisyphus --> Explore[Explore]
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Key Components
|
||||
|
||||
### 🔮 Prometheus (The Planner)
|
||||
- **Model**: `anthropic/claude-opus-4-5`
|
||||
- **Role**: Strategic planning, requirements interviews, work plan creation
|
||||
- **Constraint**: **READ-ONLY**. Can only create/modify markdown files within `.sisyphus/` directory.
|
||||
- **Characteristic**: Never writes code directly, focuses solely on "how to do it".
|
||||
|
||||
### 🦉 Metis (The Consultant)
|
||||
- **Role**: Pre-analysis and gap detection
|
||||
- **Function**: Identifies hidden user intent, prevents AI over-engineering, eliminates ambiguity.
|
||||
- **Workflow**: Metis consultation is mandatory before plan creation.
|
||||
|
||||
### ⚖️ Momus (The Reviewer)
|
||||
- **Role**: High-precision plan validation (High Accuracy Mode)
|
||||
- **Function**: Rejects and demands revisions until the plan is perfect.
|
||||
- **Trigger**: Activated when user requests "high accuracy".
|
||||
|
||||
### 🪨 Sisyphus (The Orchestrator)
|
||||
- **Model**: `anthropic/claude-opus-4-5` (Extended Thinking 32k)
|
||||
- **Role**: Execution and delegation
|
||||
- **Characteristic**: Doesn't do everything directly, actively delegates to specialized agents (Frontend, Librarian, etc.).
|
||||
|
||||
---
|
||||
|
||||
## 4. Workflow
|
||||
|
||||
### Phase 1: Interview and Planning (Interview Mode)
|
||||
Prometheus starts in **interview mode** by default. Instead of immediately creating a plan, it collects sufficient context.
|
||||
|
||||
1. **Intent Identification**: Classifies whether the user's request is Refactoring or New Feature.
|
||||
2. **Context Collection**: Investigates codebase and external documentation through `explore` and `librarian` agents.
|
||||
3. **Draft Creation**: Continuously records discussion content in `.sisyphus/drafts/`.
|
||||
|
||||
### Phase 2: Plan Generation
|
||||
When the user requests "Make it a plan", plan generation begins.
|
||||
|
||||
1. **Metis Consultation**: Confirms any missed requirements or risk factors.
|
||||
2. **Plan Creation**: Writes a single plan in `.sisyphus/plans/{name}.md` file.
|
||||
3. **Handoff**: Once plan creation is complete, guides user to use `/start-work` command.
|
||||
|
||||
### Phase 3: Execution
|
||||
When the user enters `/start-work`, the execution phase begins.
|
||||
|
||||
1. **State Management**: Creates `boulder.json` file to track current plan and session ID.
|
||||
2. **Task Execution**: Sisyphus reads the plan and processes TODOs one by one.
|
||||
3. **Delegation**: UI work is delegated to Frontend agent, complex logic to Oracle.
|
||||
4. **Continuity**: Even if the session is interrupted, work continues in the next session through `boulder.json`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Commands and Usage
|
||||
|
||||
### `@plan [request]`
|
||||
Invokes Prometheus to start a planning session.
|
||||
- Example: `@plan "I want to refactor the authentication system to NextAuth"`
|
||||
|
||||
### `/start-work`
|
||||
Executes the generated plan.
|
||||
- Function: Finds plan in `.sisyphus/plans/` and enters execution mode.
|
||||
- If there's interrupted work, automatically resumes from where it left off.
|
||||
|
||||
---
|
||||
|
||||
## 6. Configuration Guide
|
||||
|
||||
You can control related features in `oh-my-opencode.json`.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"sisyphus_agent": {
|
||||
"disabled": false, // Enable Sisyphus orchestration (default: false)
|
||||
"planner_enabled": true, // Enable Prometheus (default: true)
|
||||
"replace_plan": true // Replace default plan agent with Prometheus (default: true)
|
||||
},
|
||||
|
||||
// Hook settings (add to disable)
|
||||
"disabled_hooks": [
|
||||
// "start-work", // Disable execution trigger
|
||||
// "prometheus-md-only" // Remove Prometheus write restrictions (not recommended)
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Best Practices
|
||||
|
||||
1. **Don't Rush**: Invest sufficient time in the interview with Prometheus. The more perfect the plan, the faster the execution.
|
||||
2. **Single Plan Principle**: No matter how large the task, contain all TODOs in one plan file (`.md`). This prevents context fragmentation.
|
||||
3. **Active Delegation**: During execution, delegate to specialized agents via `delegate_task` rather than modifying code directly.
|
||||
36
package.json
36
package.json
@@ -1,31 +1,32 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "2.12.1",
|
||||
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
|
||||
"version": "3.0.0-beta.8",
|
||||
"description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"oh-my-opencode": "./dist/cli/index.js"
|
||||
"oh-my-opencode": "./bin/oh-my-opencode.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"bin",
|
||||
"postinstall.mjs"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./google-auth": {
|
||||
"types": "./dist/google-auth.d.ts",
|
||||
"import": "./dist/google-auth.js"
|
||||
},
|
||||
"./schema.json": "./dist/oh-my-opencode.schema.json"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun build src/index.ts src/google-auth.ts --outdir dist --target bun --format esm --external @ast-grep/napi && tsc --emitDeclarationOnly && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external @ast-grep/napi && bun run build:schema",
|
||||
"build": "bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi && tsc --emitDeclarationOnly && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external @ast-grep/napi && bun run build:schema",
|
||||
"build:all": "bun run build && bun run build:binaries",
|
||||
"build:binaries": "bun run script/build-binaries.ts",
|
||||
"build:schema": "bun run script/build-schema.ts",
|
||||
"clean": "rm -rf dist",
|
||||
"postinstall": "node postinstall.mjs",
|
||||
"prepublishOnly": "bun run clean && bun run build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "bun test"
|
||||
@@ -55,16 +56,14 @@
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"@code-yeongyu/comment-checker": "^0.6.1",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@openauthjs/openauth": "^0.4.3",
|
||||
"@opencode-ai/plugin": "^1.0.162",
|
||||
"@opencode-ai/sdk": "^1.0.162",
|
||||
"@opencode-ai/plugin": "^1.1.19",
|
||||
"@opencode-ai/sdk": "^1.1.19",
|
||||
"commander": "^14.0.2",
|
||||
"hono": "^4.10.4",
|
||||
"detect-libc": "^2.0.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.2",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -73,6 +72,15 @@
|
||||
"bun-types": "latest",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-darwin-x64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-linux-arm64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.0.0-beta.8",
|
||||
"oh-my-opencode-linux-x64": "3.0.0-beta.8",
|
||||
"oh-my-opencode-linux-x64-musl": "3.0.0-beta.8",
|
||||
"oh-my-opencode-windows-x64": "3.0.0-beta.8"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@ast-grep/cli",
|
||||
"@ast-grep/napi",
|
||||
|
||||
0
packages/darwin-arm64/bin/.gitkeep
Normal file
0
packages/darwin-arm64/bin/.gitkeep
Normal file
22
packages/darwin-arm64/package.json
Normal file
22
packages/darwin-arm64/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-arm64",
|
||||
"version": "3.0.0-beta.9",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-arm64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/code-yeongyu/oh-my-opencode"
|
||||
},
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"oh-my-opencode": "./bin/oh-my-opencode"
|
||||
}
|
||||
}
|
||||
0
packages/darwin-x64/bin/.gitkeep
Normal file
0
packages/darwin-x64/bin/.gitkeep
Normal file
22
packages/darwin-x64/package.json
Normal file
22
packages/darwin-x64/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-x64",
|
||||
"version": "3.0.0-beta.9",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/code-yeongyu/oh-my-opencode"
|
||||
},
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"oh-my-opencode": "./bin/oh-my-opencode"
|
||||
}
|
||||
}
|
||||
0
packages/linux-arm64-musl/bin/.gitkeep
Normal file
0
packages/linux-arm64-musl/bin/.gitkeep
Normal file
25
packages/linux-arm64-musl/package.json
Normal file
25
packages/linux-arm64-musl/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-arm64-musl",
|
||||
"version": "3.0.0-beta.9",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64-musl)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/code-yeongyu/oh-my-opencode"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"oh-my-opencode": "./bin/oh-my-opencode"
|
||||
}
|
||||
}
|
||||
0
packages/linux-arm64/bin/.gitkeep
Normal file
0
packages/linux-arm64/bin/.gitkeep
Normal file
25
packages/linux-arm64/package.json
Normal file
25
packages/linux-arm64/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-arm64",
|
||||
"version": "3.0.0-beta.9",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/code-yeongyu/oh-my-opencode"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"oh-my-opencode": "./bin/oh-my-opencode"
|
||||
}
|
||||
}
|
||||
0
packages/linux-x64-musl/bin/.gitkeep
Normal file
0
packages/linux-x64-musl/bin/.gitkeep
Normal file
25
packages/linux-x64-musl/package.json
Normal file
25
packages/linux-x64-musl/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64-musl",
|
||||
"version": "3.0.0-beta.9",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/code-yeongyu/oh-my-opencode"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"oh-my-opencode": "./bin/oh-my-opencode"
|
||||
}
|
||||
}
|
||||
0
packages/linux-x64/bin/.gitkeep
Normal file
0
packages/linux-x64/bin/.gitkeep
Normal file
25
packages/linux-x64/package.json
Normal file
25
packages/linux-x64/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64",
|
||||
"version": "3.0.0-beta.9",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/code-yeongyu/oh-my-opencode"
|
||||
},
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"oh-my-opencode": "./bin/oh-my-opencode"
|
||||
}
|
||||
}
|
||||
0
packages/windows-x64/bin/.gitkeep
Normal file
0
packages/windows-x64/bin/.gitkeep
Normal file
22
packages/windows-x64/package.json
Normal file
22
packages/windows-x64/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "oh-my-opencode-windows-x64",
|
||||
"version": "3.0.0-beta.9",
|
||||
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/code-yeongyu/oh-my-opencode"
|
||||
},
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"files": [
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"oh-my-opencode": "./bin/oh-my-opencode.exe"
|
||||
}
|
||||
}
|
||||
43
postinstall.mjs
Normal file
43
postinstall.mjs
Normal file
@@ -0,0 +1,43 @@
|
||||
// postinstall.mjs
|
||||
// Runs after npm install to verify platform binary is available
|
||||
|
||||
import { createRequire } from "node:module";
|
||||
import { getPlatformPackage, getBinaryPath } from "./bin/platform.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/**
|
||||
* Detect libc family on Linux
|
||||
*/
|
||||
function getLibcFamily() {
|
||||
if (process.platform !== "linux") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const detectLibc = require("detect-libc");
|
||||
return detectLibc.familySync();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const { platform, arch } = process;
|
||||
const libcFamily = getLibcFamily();
|
||||
|
||||
try {
|
||||
const pkg = getPlatformPackage({ platform, arch, libcFamily });
|
||||
const binPath = getBinaryPath(pkg, platform);
|
||||
|
||||
// Try to resolve the binary
|
||||
require.resolve(binPath);
|
||||
console.log(`✓ oh-my-opencode binary installed for ${platform}-${arch}`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠ oh-my-opencode: ${error.message}`);
|
||||
console.warn(` The CLI may not work on this platform.`);
|
||||
// Don't fail installation - let user try anyway
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
103
script/build-binaries.ts
Normal file
103
script/build-binaries.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bun
|
||||
// script/build-binaries.ts
|
||||
// Build platform-specific binaries for CLI distribution
|
||||
|
||||
import { $ } from "bun";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
interface PlatformTarget {
|
||||
dir: string;
|
||||
target: string;
|
||||
binary: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const PLATFORMS: PlatformTarget[] = [
|
||||
{ dir: "darwin-arm64", target: "bun-darwin-arm64", binary: "oh-my-opencode", description: "macOS ARM64" },
|
||||
{ dir: "darwin-x64", target: "bun-darwin-x64", binary: "oh-my-opencode", description: "macOS x64" },
|
||||
{ dir: "linux-x64", target: "bun-linux-x64", binary: "oh-my-opencode", description: "Linux x64 (glibc)" },
|
||||
{ dir: "linux-arm64", target: "bun-linux-arm64", binary: "oh-my-opencode", description: "Linux ARM64 (glibc)" },
|
||||
{ dir: "linux-x64-musl", target: "bun-linux-x64-musl", binary: "oh-my-opencode", description: "Linux x64 (musl)" },
|
||||
{ dir: "linux-arm64-musl", target: "bun-linux-arm64-musl", binary: "oh-my-opencode", description: "Linux ARM64 (musl)" },
|
||||
{ dir: "windows-x64", target: "bun-windows-x64", binary: "oh-my-opencode.exe", description: "Windows x64" },
|
||||
];
|
||||
|
||||
const ENTRY_POINT = "src/cli/index.ts";
|
||||
|
||||
async function buildPlatform(platform: PlatformTarget): Promise<boolean> {
|
||||
const outfile = join("packages", platform.dir, "bin", platform.binary);
|
||||
|
||||
console.log(`\n📦 Building ${platform.description}...`);
|
||||
console.log(` Target: ${platform.target}`);
|
||||
console.log(` Output: ${outfile}`);
|
||||
|
||||
try {
|
||||
await $`bun build --compile --minify --sourcemap --bytecode --target=${platform.target} ${ENTRY_POINT} --outfile=${outfile}`;
|
||||
|
||||
// Verify binary exists
|
||||
if (!existsSync(outfile)) {
|
||||
console.error(` ❌ Binary not found after build: ${outfile}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify binary with file command (skip on Windows host for non-Windows targets)
|
||||
if (process.platform !== "win32") {
|
||||
const fileInfo = await $`file ${outfile}`.text();
|
||||
console.log(` ✓ ${fileInfo.trim()}`);
|
||||
} else {
|
||||
console.log(` ✓ Binary created successfully`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(` ❌ Build failed: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("🔨 Building oh-my-opencode platform binaries");
|
||||
console.log(` Entry point: ${ENTRY_POINT}`);
|
||||
console.log(` Platforms: ${PLATFORMS.length}`);
|
||||
|
||||
// Verify entry point exists
|
||||
if (!existsSync(ENTRY_POINT)) {
|
||||
console.error(`\n❌ Entry point not found: ${ENTRY_POINT}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const results: { platform: string; success: boolean }[] = [];
|
||||
|
||||
for (const platform of PLATFORMS) {
|
||||
const success = await buildPlatform(platform);
|
||||
results.push({ platform: platform.description, success });
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log("\n" + "=".repeat(50));
|
||||
console.log("Build Summary:");
|
||||
console.log("=".repeat(50));
|
||||
|
||||
const succeeded = results.filter(r => r.success).length;
|
||||
const failed = results.filter(r => !r.success).length;
|
||||
|
||||
for (const result of results) {
|
||||
const icon = result.success ? "✓" : "✗";
|
||||
console.log(` ${icon} ${result.platform}`);
|
||||
}
|
||||
|
||||
console.log("=".repeat(50));
|
||||
console.log(`Total: ${succeeded} succeeded, ${failed} failed`);
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("\n✅ All platform binaries built successfully!\n");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,12 +1,24 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { $ } from "bun"
|
||||
import { existsSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
|
||||
const PACKAGE_NAME = "oh-my-opencode"
|
||||
const bump = process.env.BUMP as "major" | "minor" | "patch" | undefined
|
||||
const versionOverride = process.env.VERSION
|
||||
|
||||
console.log("=== Publishing oh-my-opencode ===\n")
|
||||
const PLATFORM_PACKAGES = [
|
||||
"darwin-arm64",
|
||||
"darwin-x64",
|
||||
"linux-x64",
|
||||
"linux-arm64",
|
||||
"linux-x64-musl",
|
||||
"linux-arm64-musl",
|
||||
"windows-x64",
|
||||
]
|
||||
|
||||
console.log("=== Publishing oh-my-opencode (multi-package) ===\n")
|
||||
|
||||
async function fetchPreviousVersion(): Promise<string> {
|
||||
try {
|
||||
@@ -22,7 +34,9 @@ async function fetchPreviousVersion(): Promise<string> {
|
||||
}
|
||||
|
||||
function bumpVersion(version: string, type: "major" | "minor" | "patch"): string {
|
||||
const [major, minor, patch] = version.split(".").map(Number)
|
||||
// Handle prerelease versions (e.g., 3.0.0-beta.7)
|
||||
const baseVersion = version.split("-")[0]
|
||||
const [major, minor, patch] = baseVersion.split(".").map(Number)
|
||||
switch (type) {
|
||||
case "major":
|
||||
return `${major + 1}.0.0`
|
||||
@@ -33,14 +47,42 @@ function bumpVersion(version: string, type: "major" | "minor" | "patch"): string
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePackageVersion(newVersion: string): Promise<void> {
|
||||
const pkgPath = new URL("../package.json", import.meta.url).pathname
|
||||
async function updatePackageVersion(pkgPath: string, newVersion: string): Promise<void> {
|
||||
let pkg = await Bun.file(pkgPath).text()
|
||||
pkg = pkg.replace(/"version": "[^"]+"/, `"version": "${newVersion}"`)
|
||||
await Bun.file(pkgPath).write(pkg)
|
||||
await Bun.write(pkgPath, pkg)
|
||||
console.log(`Updated: ${pkgPath}`)
|
||||
}
|
||||
|
||||
async function updateAllPackageVersions(newVersion: string): Promise<void> {
|
||||
console.log("\nSyncing version across all packages...")
|
||||
|
||||
// Update main package.json
|
||||
const mainPkgPath = new URL("../package.json", import.meta.url).pathname
|
||||
await updatePackageVersion(mainPkgPath, newVersion)
|
||||
|
||||
// Update optionalDependencies versions in main package.json
|
||||
let mainPkg = await Bun.file(mainPkgPath).text()
|
||||
for (const platform of PLATFORM_PACKAGES) {
|
||||
const pkgName = `oh-my-opencode-${platform}`
|
||||
mainPkg = mainPkg.replace(
|
||||
new RegExp(`"${pkgName}": "[^"]+"`),
|
||||
`"${pkgName}": "${newVersion}"`
|
||||
)
|
||||
}
|
||||
await Bun.write(mainPkgPath, mainPkg)
|
||||
|
||||
// Update each platform package.json
|
||||
for (const platform of PLATFORM_PACKAGES) {
|
||||
const pkgPath = new URL(`../packages/${platform}/package.json`, import.meta.url).pathname
|
||||
if (existsSync(pkgPath)) {
|
||||
await updatePackageVersion(pkgPath, newVersion)
|
||||
} else {
|
||||
console.warn(`Warning: ${pkgPath} not found`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generateChangelog(previous: string): Promise<string[]> {
|
||||
const notes: string[] = []
|
||||
|
||||
@@ -106,13 +148,100 @@ async function getContributors(previous: string): Promise<string[]> {
|
||||
return notes
|
||||
}
|
||||
|
||||
async function buildAndPublish(): Promise<void> {
|
||||
console.log("\nPublishing to npm...")
|
||||
// --ignore-scripts: workflow에서 이미 빌드 완료, prepublishOnly 재실행 방지
|
||||
if (process.env.CI) {
|
||||
await $`npm publish --access public --provenance --ignore-scripts`
|
||||
function getDistTag(version: string): string | null {
|
||||
if (!version.includes("-")) return null
|
||||
const prerelease = version.split("-")[1]
|
||||
const tag = prerelease?.split(".")[0]
|
||||
return tag || "next"
|
||||
}
|
||||
|
||||
interface PublishResult {
|
||||
success: boolean
|
||||
alreadyPublished?: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
async function publishPackage(cwd: string, distTag: string | null): Promise<PublishResult> {
|
||||
const tagArgs = distTag ? ["--tag", distTag] : []
|
||||
const provenanceArgs = process.env.CI ? ["--provenance"] : []
|
||||
|
||||
try {
|
||||
await $`npm publish --access public --ignore-scripts ${provenanceArgs} ${tagArgs}`.cwd(cwd)
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
const stderr = error?.stderr?.toString() || error?.message || ""
|
||||
|
||||
// E409 = version already exists (idempotent success)
|
||||
if (
|
||||
stderr.includes("EPUBLISHCONFLICT") ||
|
||||
stderr.includes("E409") ||
|
||||
stderr.includes("cannot publish over") ||
|
||||
stderr.includes("already exists")
|
||||
) {
|
||||
return { success: true, alreadyPublished: true }
|
||||
}
|
||||
|
||||
return { success: false, error: stderr }
|
||||
}
|
||||
}
|
||||
|
||||
async function publishAllPackages(version: string): Promise<void> {
|
||||
const distTag = getDistTag(version)
|
||||
const skipPlatform = process.env.SKIP_PLATFORM_PACKAGES === "true"
|
||||
|
||||
if (skipPlatform) {
|
||||
console.log("\n⏭️ Skipping platform packages (SKIP_PLATFORM_PACKAGES=true)")
|
||||
} else {
|
||||
await $`npm publish --access public --ignore-scripts`
|
||||
console.log("\n📦 Publishing platform packages...")
|
||||
|
||||
// Publish platform packages first
|
||||
for (const platform of PLATFORM_PACKAGES) {
|
||||
const pkgDir = join(process.cwd(), "packages", platform)
|
||||
const pkgName = `oh-my-opencode-${platform}`
|
||||
|
||||
console.log(`\n Publishing ${pkgName}...`)
|
||||
const result = await publishPackage(pkgDir, distTag)
|
||||
|
||||
if (result.success) {
|
||||
if (result.alreadyPublished) {
|
||||
console.log(` ✓ ${pkgName}@${version} (already published)`)
|
||||
} else {
|
||||
console.log(` ✓ ${pkgName}@${version}`)
|
||||
}
|
||||
} else {
|
||||
console.error(` ✗ ${pkgName} failed: ${result.error}`)
|
||||
throw new Error(`Failed to publish ${pkgName}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Publish main package last
|
||||
console.log(`\n📦 Publishing main package...`)
|
||||
const mainResult = await publishPackage(process.cwd(), distTag)
|
||||
|
||||
if (mainResult.success) {
|
||||
if (mainResult.alreadyPublished) {
|
||||
console.log(` ✓ ${PACKAGE_NAME}@${version} (already published)`)
|
||||
} else {
|
||||
console.log(` ✓ ${PACKAGE_NAME}@${version}`)
|
||||
}
|
||||
} else {
|
||||
console.error(` ✗ ${PACKAGE_NAME} failed: ${mainResult.error}`)
|
||||
throw new Error(`Failed to publish ${PACKAGE_NAME}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildPackages(): Promise<void> {
|
||||
const skipPlatform = process.env.SKIP_PLATFORM_PACKAGES === "true"
|
||||
|
||||
console.log("\nBuilding packages...")
|
||||
await $`bun run clean && bun run build`
|
||||
|
||||
if (skipPlatform) {
|
||||
console.log("⏭️ Skipping platform binaries (SKIP_PLATFORM_PACKAGES=true)")
|
||||
} else {
|
||||
console.log("Building platform binaries...")
|
||||
await $`bun run build:binaries`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +251,12 @@ async function gitTagAndRelease(newVersion: string, notes: string[]): Promise<vo
|
||||
console.log("\nCommitting and tagging...")
|
||||
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`
|
||||
await $`git config user.name "github-actions[bot]"`
|
||||
|
||||
// Add all package.json files
|
||||
await $`git add package.json assets/oh-my-opencode.schema.json`
|
||||
for (const platform of PLATFORM_PACKAGES) {
|
||||
await $`git add packages/${platform}/package.json`.nothrow()
|
||||
}
|
||||
|
||||
const hasStagedChanges = await $`git diff --cached --quiet`.nothrow()
|
||||
if (hasStagedChanges.exitCode !== 0) {
|
||||
@@ -169,15 +303,16 @@ async function main() {
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
await updatePackageVersion(newVersion)
|
||||
await updateAllPackageVersions(newVersion)
|
||||
const changelog = await generateChangelog(previous)
|
||||
const contributors = await getContributors(previous)
|
||||
const notes = [...changelog, ...contributors]
|
||||
|
||||
await buildAndPublish()
|
||||
await buildPackages()
|
||||
await publishAllPackages(newVersion)
|
||||
await gitTagAndRelease(newVersion, notes)
|
||||
|
||||
console.log(`\n=== Successfully published ${PACKAGE_NAME}@${newVersion} ===`)
|
||||
console.log(`\n=== Successfully published ${PACKAGE_NAME}@${newVersion} (8 packages) ===`)
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -143,6 +143,462 @@
|
||||
"created_at": "2025-12-31T20:40:20Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 388
|
||||
},
|
||||
{
|
||||
"name": "changeroa",
|
||||
"id": 65930387,
|
||||
"comment_id": 3706697910,
|
||||
"created_at": "2026-01-03T04:51:11Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 446
|
||||
},
|
||||
{
|
||||
"name": "hqone",
|
||||
"id": 13660872,
|
||||
"comment_id": 3707019551,
|
||||
"created_at": "2026-01-03T12:21:52Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 451
|
||||
},
|
||||
{
|
||||
"name": "fparrav",
|
||||
"id": 9319430,
|
||||
"comment_id": 3707456044,
|
||||
"created_at": "2026-01-03T23:51:28Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 469
|
||||
},
|
||||
{
|
||||
"name": "ChiR24",
|
||||
"id": 125826529,
|
||||
"comment_id": 3707776762,
|
||||
"created_at": "2026-01-04T06:14:36Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 473
|
||||
},
|
||||
{
|
||||
"name": "geq1fan",
|
||||
"id": 29982379,
|
||||
"comment_id": 3708136393,
|
||||
"created_at": "2026-01-04T14:31:14Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 481
|
||||
},
|
||||
{
|
||||
"name": "RhysSullivan",
|
||||
"id": 39114868,
|
||||
"comment_id": 3708266434,
|
||||
"created_at": "2026-01-04T17:19:44Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 482
|
||||
},
|
||||
{
|
||||
"name": "Skyline-23",
|
||||
"id": 62983047,
|
||||
"comment_id": 3708282461,
|
||||
"created_at": "2026-01-04T17:42:02Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 484
|
||||
},
|
||||
{
|
||||
"name": "popododo0720",
|
||||
"id": 78542988,
|
||||
"comment_id": 3708870772,
|
||||
"created_at": "2026-01-05T04:07:35Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 477
|
||||
},
|
||||
{
|
||||
"name": "raydocs",
|
||||
"id": 139067258,
|
||||
"comment_id": 3709269581,
|
||||
"created_at": "2026-01-05T07:39:43Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 499
|
||||
},
|
||||
{
|
||||
"name": "luosky",
|
||||
"id": 307601,
|
||||
"comment_id": 3710103143,
|
||||
"created_at": "2026-01-05T11:46:40Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 512
|
||||
},
|
||||
{
|
||||
"name": "jkoelker",
|
||||
"id": 75854,
|
||||
"comment_id": 3713015728,
|
||||
"created_at": "2026-01-06T03:59:38Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 531
|
||||
},
|
||||
{
|
||||
"name": "sngweizhi",
|
||||
"id": 47587454,
|
||||
"comment_id": 3713078490,
|
||||
"created_at": "2026-01-06T04:36:53Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 532
|
||||
},
|
||||
{
|
||||
"name": "ananas-viber",
|
||||
"id": 241022041,
|
||||
"comment_id": 3714661395,
|
||||
"created_at": "2026-01-06T13:16:18Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 544
|
||||
},
|
||||
{
|
||||
"name": "JohnC0de",
|
||||
"id": 88864312,
|
||||
"comment_id": 3714978210,
|
||||
"created_at": "2026-01-06T14:45:26Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 543
|
||||
},
|
||||
{
|
||||
"name": "atripathy86",
|
||||
"id": 3656621,
|
||||
"comment_id": 3715631259,
|
||||
"created_at": "2026-01-06T17:32:32Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 550
|
||||
},
|
||||
{
|
||||
"name": "starcomo",
|
||||
"id": 13599079,
|
||||
"comment_id": 3716642385,
|
||||
"created_at": "2026-01-06T22:49:42Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 486
|
||||
},
|
||||
{
|
||||
"name": "LeonardoTrapani",
|
||||
"id": 93481468,
|
||||
"comment_id": 3718191895,
|
||||
"created_at": "2026-01-07T10:16:28Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 570
|
||||
},
|
||||
{
|
||||
"name": "minpeter",
|
||||
"id": 62207008,
|
||||
"comment_id": 3718732058,
|
||||
"created_at": "2026-01-07T12:53:05Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 574
|
||||
},
|
||||
{
|
||||
"name": "sungchul2",
|
||||
"id": 33727805,
|
||||
"comment_id": 3719053716,
|
||||
"created_at": "2026-01-07T14:07:09Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 576
|
||||
},
|
||||
{
|
||||
"name": "Yjason-K",
|
||||
"id": 81736873,
|
||||
"comment_id": 3722247927,
|
||||
"created_at": "2026-01-08T06:26:16Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 590
|
||||
},
|
||||
{
|
||||
"name": "Gladdonilli",
|
||||
"id": 179516171,
|
||||
"comment_id": 3723118887,
|
||||
"created_at": "2026-01-08T10:02:26Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 592
|
||||
},
|
||||
{
|
||||
"name": "xLillium",
|
||||
"id": 16964936,
|
||||
"comment_id": 3725604869,
|
||||
"created_at": "2026-01-08T20:18:27Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 603
|
||||
},
|
||||
{
|
||||
"name": "SJY0917032",
|
||||
"id": 88534701,
|
||||
"comment_id": 3728199745,
|
||||
"created_at": "2026-01-09T10:01:19Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 625
|
||||
},
|
||||
{
|
||||
"name": "kdcokenny",
|
||||
"id": 99611484,
|
||||
"comment_id": 3728801075,
|
||||
"created_at": "2026-01-09T12:54:05Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 629
|
||||
},
|
||||
{
|
||||
"name": "ElwinLiu",
|
||||
"id": 87802244,
|
||||
"comment_id": 3731812585,
|
||||
"created_at": "2026-01-10T04:32:16Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 645
|
||||
},
|
||||
{
|
||||
"name": "Luodian",
|
||||
"id": 15847405,
|
||||
"comment_id": 3731833107,
|
||||
"created_at": "2026-01-10T05:01:16Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 634
|
||||
},
|
||||
{
|
||||
"name": "imarshallwidjaja",
|
||||
"id": 60992624,
|
||||
"comment_id": 3732124681,
|
||||
"created_at": "2026-01-10T07:58:43Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 648
|
||||
},
|
||||
{
|
||||
"name": "GollyJer",
|
||||
"id": 689204,
|
||||
"comment_id": 3732253764,
|
||||
"created_at": "2026-01-10T09:33:21Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 649
|
||||
},
|
||||
{
|
||||
"name": "kargnas",
|
||||
"id": 1438533,
|
||||
"comment_id": 3732344143,
|
||||
"created_at": "2026-01-10T10:25:25Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 653
|
||||
},
|
||||
{
|
||||
"name": "ashir6892",
|
||||
"id": 52703606,
|
||||
"comment_id": 3733435826,
|
||||
"created_at": "2026-01-10T19:50:07Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 675
|
||||
},
|
||||
{
|
||||
"name": "arthur404dev",
|
||||
"id": 59490008,
|
||||
"comment_id": 3733697071,
|
||||
"created_at": "2026-01-10T23:51:44Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 676
|
||||
},
|
||||
{
|
||||
"name": "KNN-07",
|
||||
"id": 55886589,
|
||||
"comment_id": 3733788592,
|
||||
"created_at": "2026-01-11T01:11:38Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 679
|
||||
},
|
||||
{
|
||||
"name": "aw338WoWmUI",
|
||||
"id": 121638634,
|
||||
"comment_id": 3734013343,
|
||||
"created_at": "2026-01-11T04:56:38Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 681
|
||||
},
|
||||
{
|
||||
"name": "Coaspe",
|
||||
"id": 76432686,
|
||||
"comment_id": 3734070196,
|
||||
"created_at": "2026-01-11T06:03:57Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 682
|
||||
},
|
||||
{
|
||||
"name": "yimingll",
|
||||
"id": 116444509,
|
||||
"comment_id": 3734341425,
|
||||
"created_at": "2026-01-11T10:00:54Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 689
|
||||
},
|
||||
{
|
||||
"name": "Sanyue0v0",
|
||||
"id": 177394511,
|
||||
"comment_id": 3735145789,
|
||||
"created_at": "2026-01-11T17:37:13Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 696
|
||||
},
|
||||
{
|
||||
"name": "chilipvlmer",
|
||||
"id": 100484914,
|
||||
"comment_id": 3735268635,
|
||||
"created_at": "2026-01-11T18:19:56Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 698
|
||||
},
|
||||
{
|
||||
"name": "Momentum96",
|
||||
"id": 31430161,
|
||||
"comment_id": 3737397810,
|
||||
"created_at": "2026-01-12T08:33:44Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 709
|
||||
},
|
||||
{
|
||||
"name": "dante01yoon",
|
||||
"id": 6510430,
|
||||
"comment_id": 3738360375,
|
||||
"created_at": "2026-01-12T12:38:47Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 710
|
||||
},
|
||||
{
|
||||
"name": "LTS2",
|
||||
"id": 24840361,
|
||||
"comment_id": 3743927388,
|
||||
"created_at": "2026-01-13T11:57:10Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 745
|
||||
},
|
||||
{
|
||||
"name": "haal-laah",
|
||||
"id": 122613332,
|
||||
"comment_id": 3742477826,
|
||||
"created_at": "2026-01-13T07:26:35Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 739
|
||||
},
|
||||
{
|
||||
"name": "oussamadouhou",
|
||||
"id": 16113844,
|
||||
"comment_id": 3742035216,
|
||||
"created_at": "2026-01-13T05:31:56Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 731
|
||||
},
|
||||
{
|
||||
"name": "abhijit360",
|
||||
"id": 23292258,
|
||||
"comment_id": 3747332060,
|
||||
"created_at": "2026-01-14T01:55:14Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 759
|
||||
},
|
||||
{
|
||||
"name": "justsisyphus",
|
||||
"id": 254807767,
|
||||
"comment_id": 3747336906,
|
||||
"created_at": "2026-01-14T01:57:52Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 760
|
||||
},
|
||||
{
|
||||
"name": "0Jaeyoung0",
|
||||
"id": 67817265,
|
||||
"comment_id": 3747909072,
|
||||
"created_at": "2026-01-14T05:56:13Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 774
|
||||
},
|
||||
{
|
||||
"name": "MotorwaySouth9",
|
||||
"id": 205539026,
|
||||
"comment_id": 3748060487,
|
||||
"created_at": "2026-01-14T06:50:26Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 776
|
||||
},
|
||||
{
|
||||
"name": "dang232",
|
||||
"id": 92773067,
|
||||
"comment_id": 3748235411,
|
||||
"created_at": "2026-01-14T07:41:50Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 777
|
||||
},
|
||||
{
|
||||
"name": "devkade",
|
||||
"id": 64977390,
|
||||
"comment_id": 3749807159,
|
||||
"created_at": "2026-01-14T14:25:26Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 784
|
||||
},
|
||||
{
|
||||
"name": "stranger2904",
|
||||
"id": 57737909,
|
||||
"comment_id": 3750612223,
|
||||
"created_at": "2026-01-14T17:06:12Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 788
|
||||
},
|
||||
{
|
||||
"name": "stranger29",
|
||||
"id": 29339256,
|
||||
"comment_id": 3751601362,
|
||||
"created_at": "2026-01-14T20:31:35Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 795
|
||||
},
|
||||
{
|
||||
"name": "mmlmt2604",
|
||||
"id": 59196850,
|
||||
"comment_id": 3753859484,
|
||||
"created_at": "2026-01-15T09:57:16Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 812
|
||||
},
|
||||
{
|
||||
"name": "minkichoe-lbox",
|
||||
"id": 194467696,
|
||||
"comment_id": 3758902914,
|
||||
"created_at": "2026-01-16T09:14:21Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 847
|
||||
},
|
||||
{
|
||||
"name": "vmlinuzx",
|
||||
"id": 233838569,
|
||||
"comment_id": 3760678754,
|
||||
"created_at": "2026-01-16T15:45:52Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 837
|
||||
},
|
||||
{
|
||||
"name": "luojiyin1987",
|
||||
"id": 6524977,
|
||||
"comment_id": 3760712340,
|
||||
"created_at": "2026-01-16T15:54:07Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 855
|
||||
},
|
||||
{
|
||||
"name": "qwertystars",
|
||||
"id": 62981066,
|
||||
"comment_id": 3761235668,
|
||||
"created_at": "2026-01-16T18:13:52Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 859
|
||||
},
|
||||
{
|
||||
"name": "sgwannabe",
|
||||
"id": 33509021,
|
||||
"comment_id": 3762457370,
|
||||
"created_at": "2026-01-17T01:25:58Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 863
|
||||
},
|
||||
{
|
||||
"name": "G-hoon",
|
||||
"id": 26299556,
|
||||
"comment_id": 3764015966,
|
||||
"created_at": "2026-01-17T15:27:41Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 879
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,63 +2,70 @@
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
7 AI agents for multi-model orchestration. Sisyphus orchestrates, specialists handle domains.
|
||||
10 AI agents for multi-model orchestration. Sisyphus (primary), oracle, librarian, explore, frontend, document-writer, multimodal-looker, Prometheus, Metis, Momus.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
agents/
|
||||
├── sisyphus.ts # Primary orchestrator (504 lines)
|
||||
├── oracle.ts # Strategic advisor
|
||||
├── librarian.ts # Multi-repo research
|
||||
├── explore.ts # Fast codebase grep
|
||||
├── frontend-ui-ux-engineer.ts # UI generation
|
||||
├── document-writer.ts # Technical docs
|
||||
├── multimodal-looker.ts # PDF/image analysis
|
||||
├── sisyphus-prompt-builder.ts # Sisyphus prompt construction
|
||||
├── build-prompt.ts # Shared build agent prompt
|
||||
├── plan-prompt.ts # Shared plan agent prompt
|
||||
├── types.ts # AgentModelConfig interface
|
||||
├── utils.ts # createBuiltinAgents(), getAgentName()
|
||||
└── index.ts # builtinAgents export
|
||||
├── orchestrator-sisyphus.ts # Orchestrator (1531 lines) - 7-phase delegation
|
||||
├── sisyphus.ts # Main prompt (640 lines)
|
||||
├── sisyphus-junior.ts # Delegated task executor
|
||||
├── sisyphus-prompt-builder.ts # Dynamic prompt generation
|
||||
├── oracle.ts # Strategic advisor (GPT-5.2)
|
||||
├── librarian.ts # Multi-repo research (GLM-4.7-free)
|
||||
├── explore.ts # Fast grep (Grok Code)
|
||||
├── frontend-ui-ux-engineer.ts # UI specialist (Gemini 3 Pro)
|
||||
├── document-writer.ts # Technical writer (Gemini 3 Flash)
|
||||
├── multimodal-looker.ts # Media analyzer (Gemini 3 Flash)
|
||||
├── prometheus-prompt.ts # Planning (1196 lines) - interview mode
|
||||
├── metis.ts # Plan consultant - pre-planning analysis
|
||||
├── momus.ts # Plan reviewer - validation
|
||||
├── types.ts # AgentModelConfig interface
|
||||
├── utils.ts # createBuiltinAgents(), getAgentName()
|
||||
└── index.ts # builtinAgents export
|
||||
```
|
||||
|
||||
## AGENT MODELS
|
||||
|
||||
| Agent | Model | Fallback | Purpose |
|
||||
|-------|-------|----------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | - | Orchestrator with extended thinking |
|
||||
| oracle | openai/gpt-5.2 | - | Architecture, debugging, review |
|
||||
| librarian | anthropic/claude-sonnet-4-5 | google/gemini-3-flash | Docs, GitHub research |
|
||||
| explore | opencode/grok-code | gemini-3-flash, haiku-4-5 | Contextual grep |
|
||||
| frontend-ui-ux-engineer | google/gemini-3-pro-preview | - | Beautiful UI code |
|
||||
| document-writer | google/gemini-3-pro-preview | - | Technical writing |
|
||||
| multimodal-looker | google/gemini-3-flash | - | Visual analysis |
|
||||
| Agent | Model | Temperature | Purpose |
|
||||
|-------|-------|-------------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | 0.1 | Primary orchestrator, todo-driven |
|
||||
| oracle | openai/gpt-5.2 | 0.1 | Read-only consultation, debugging |
|
||||
| librarian | opencode/glm-4.7-free | 0.1 | Docs, GitHub search, OSS examples |
|
||||
| explore | opencode/grok-code | 0.1 | Fast contextual grep |
|
||||
| frontend-ui-ux-engineer | google/gemini-3-pro-preview | 0.7 | UI generation, visual design |
|
||||
| document-writer | google/gemini-3-flash | 0.3 | Technical documentation |
|
||||
| multimodal-looker | google/gemini-3-flash | 0.1 | PDF/image analysis |
|
||||
| Prometheus | anthropic/claude-opus-4-5 | 0.1 | Strategic planning, interview mode |
|
||||
| Metis | anthropic/claude-sonnet-4-5 | 0.1 | Pre-planning gap analysis |
|
||||
| Momus | anthropic/claude-sonnet-4-5 | 0.1 | Plan validation |
|
||||
|
||||
## HOW TO ADD
|
||||
|
||||
1. Create `src/agents/my-agent.ts`:
|
||||
```typescript
|
||||
export const myAgent: AgentConfig = {
|
||||
model: "provider/model-name",
|
||||
temperature: 0.1,
|
||||
system: "...",
|
||||
tools: { include: ["tool1"] },
|
||||
}
|
||||
```
|
||||
2. Add to `builtinAgents` in index.ts
|
||||
3. Update types.ts if new config options
|
||||
1. Create `src/agents/my-agent.ts` exporting `AgentConfig`
|
||||
2. Add to `builtinAgents` in `src/agents/index.ts`
|
||||
3. Update `AgentNameSchema` in `src/config/schema.ts`
|
||||
4. Register in `src/index.ts` initialization
|
||||
|
||||
## MODEL FALLBACK
|
||||
## TOOL RESTRICTIONS
|
||||
|
||||
`createBuiltinAgents()` handles fallback:
|
||||
1. User config override
|
||||
2. Installer settings (claude max20, gemini antigravity)
|
||||
3. Default model
|
||||
| Agent | Denied Tools |
|
||||
|-------|-------------|
|
||||
| oracle | write, edit, task, delegate_task |
|
||||
| librarian | write, edit, task, delegate_task, call_omo_agent |
|
||||
| explore | write, edit, task, delegate_task, call_omo_agent |
|
||||
| multimodal-looker | Allowlist: read, glob, grep |
|
||||
|
||||
## KEY PATTERNS
|
||||
|
||||
- **Factory**: `createXXXAgent(model?: string): AgentConfig`
|
||||
- **Metadata**: `XXX_PROMPT_METADATA: AgentPromptMetadata`
|
||||
- **Tool restrictions**: `permission: { edit: "deny", bash: "ask" }`
|
||||
- **Thinking**: 32k budget tokens for Sisyphus, Oracle, Prometheus
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
- High temperature (>0.3) for code agents
|
||||
- Broad tool access (prefer explicit `include`)
|
||||
- Monolithic prompts (delegate to specialists)
|
||||
- Missing fallbacks for rate-limited models
|
||||
- **Trust reports**: NEVER trust subagent "I'm done" - verify outputs
|
||||
- **High temp**: Don't use >0.3 for code agents
|
||||
- **Sequential calls**: Use `delegate_task` with `run_in_background`
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* OpenCode's default build agent system prompt.
|
||||
*
|
||||
* This prompt enables FULL EXECUTION mode for the build agent, allowing file
|
||||
* modifications, command execution, and system changes while focusing on
|
||||
* implementation and execution.
|
||||
*
|
||||
* Inspired by OpenCode's build agent behavior.
|
||||
*
|
||||
* @see https://github.com/sst/opencode/blob/6f9bea4e1f3d139feefd0f88de260b04f78caaef/packages/opencode/src/session/prompt/build-switch.txt
|
||||
* @see https://github.com/sst/opencode/blob/6f9bea4e1f3d139feefd0f88de260b04f78caaef/packages/opencode/src/agent/agent.ts#L118-L125
|
||||
*/
|
||||
export const BUILD_SYSTEM_PROMPT = `<system-reminder>
|
||||
# Build Mode - System Reminder
|
||||
|
||||
BUILD MODE ACTIVE - you are in EXECUTION phase. Your responsibility is to:
|
||||
- Implement features and make code changes
|
||||
- Execute commands and run tests
|
||||
- Fix bugs and refactor code
|
||||
- Deploy and build systems
|
||||
- Make all necessary file modifications
|
||||
|
||||
You have FULL permissions to edit files, run commands, and make system changes.
|
||||
This is the implementation phase - execute decisively and thoroughly.
|
||||
|
||||
---
|
||||
|
||||
## Responsibility
|
||||
|
||||
Your current responsibility is to implement, build, and execute. You should:
|
||||
- Write and modify code to accomplish the user's goals
|
||||
- Run tests and builds to verify your changes
|
||||
- Fix errors and issues that arise
|
||||
- Use all available tools to complete the task efficiently
|
||||
- Delegate to specialized agents when appropriate for better results
|
||||
|
||||
**NOTE:** You should ask the user for clarification when requirements are ambiguous,
|
||||
but once the path is clear, execute confidently. The goal is to deliver working,
|
||||
tested, production-ready solutions.
|
||||
|
||||
---
|
||||
|
||||
## Important
|
||||
|
||||
The user wants you to execute and implement. You SHOULD make edits, run necessary
|
||||
tools, and make changes to accomplish the task. Use your full capabilities to
|
||||
deliver excellent results.
|
||||
</system-reminder>
|
||||
`
|
||||
|
||||
/**
|
||||
* OpenCode's default build agent permission configuration.
|
||||
*
|
||||
* Allows the build agent full execution permissions:
|
||||
* - edit: "ask" - Can modify files with confirmation
|
||||
* - bash: "ask" - Can execute commands with confirmation
|
||||
* - webfetch: "allow" - Can fetch web content
|
||||
*
|
||||
* This provides balanced permissions - powerful but with safety checks.
|
||||
*
|
||||
* @see https://github.com/sst/opencode/blob/6f9bea4e1f3d139feefd0f88de260b04f78caaef/packages/opencode/src/agent/agent.ts#L57-L68
|
||||
* @see https://github.com/sst/opencode/blob/6f9bea4e1f3d139feefd0f88de260b04f78caaef/packages/opencode/src/agent/agent.ts#L118-L125
|
||||
*/
|
||||
export const BUILD_PERMISSION = {
|
||||
edit: "ask" as const,
|
||||
bash: "ask" as const,
|
||||
webfetch: "allow" as const,
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "google/gemini-3-flash-preview"
|
||||
|
||||
@@ -15,12 +16,14 @@ export const DOCUMENT_WRITER_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
export function createDocumentWriterAgent(
|
||||
model: string = DEFAULT_MODEL
|
||||
): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([])
|
||||
|
||||
return {
|
||||
description:
|
||||
"A technical writer who crafts clear, comprehensive documentation. Specializes in README files, API docs, architecture docs, and user guides. MUST BE USED when executing documentation tasks from ai-todo list plans.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
tools: { background_task: false },
|
||||
...restrictions,
|
||||
prompt: `<role>
|
||||
You are a TECHNICAL WRITER with deep engineering background who transforms complex codebases into crystal-clear documentation. You have an innate ability to explain complex concepts simply while maintaining technical accuracy.
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "opencode/grok-code"
|
||||
|
||||
@@ -24,13 +25,21 @@ export const EXPLORE_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
}
|
||||
|
||||
export function createExploreAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"delegate_task",
|
||||
"call_omo_agent",
|
||||
])
|
||||
|
||||
return {
|
||||
description:
|
||||
'Contextual grep for codebases. Answers "Where is X?", "Which file has Y?", "Find the code that does Z". Fire multiple in parallel for broad searches. Specify thoroughness: "quick" for basic, "medium" for moderate, "very thorough" for comprehensive analysis.',
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
tools: { write: false, edit: false, background_task: false },
|
||||
...restrictions,
|
||||
prompt: `You are a codebase search specialist. Your job: find files and code, return actionable results.
|
||||
|
||||
## Your Mission
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "google/gemini-3-pro-preview"
|
||||
|
||||
@@ -21,12 +22,14 @@ export const FRONTEND_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
export function createFrontendUiUxEngineerAgent(
|
||||
model: string = DEFAULT_MODEL
|
||||
): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([])
|
||||
|
||||
return {
|
||||
description:
|
||||
"A designer-turned-developer who crafts stunning UI/UX even without design mockups. Code may be a bit messy, but the visual output is always fire.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
tools: { background_task: false },
|
||||
...restrictions,
|
||||
prompt: `# Role: Designer-Turned-Developer
|
||||
|
||||
You are a designer who learned to code. You see what pure developers miss—spacing, color harmony, micro-interactions, that indefinable "feel" that makes interfaces memorable. Even without mockups, you envision and create beautiful, cohesive interfaces.
|
||||
|
||||
@@ -6,6 +6,9 @@ import { exploreAgent } from "./explore"
|
||||
import { frontendUiUxEngineerAgent } from "./frontend-ui-ux-engineer"
|
||||
import { documentWriterAgent } from "./document-writer"
|
||||
import { multimodalLookerAgent } from "./multimodal-looker"
|
||||
import { metisAgent } from "./metis"
|
||||
import { orchestratorSisyphusAgent } from "./orchestrator-sisyphus"
|
||||
import { momusAgent } from "./momus"
|
||||
|
||||
export const builtinAgents: Record<string, AgentConfig> = {
|
||||
Sisyphus: sisyphusAgent,
|
||||
@@ -15,6 +18,9 @@ export const builtinAgents: Record<string, AgentConfig> = {
|
||||
"frontend-ui-ux-engineer": frontendUiUxEngineerAgent,
|
||||
"document-writer": documentWriterAgent,
|
||||
"multimodal-looker": multimodalLookerAgent,
|
||||
"Metis (Plan Consultant)": metisAgent,
|
||||
"Momus (Plan Reviewer)": momusAgent,
|
||||
"orchestrator-sisyphus": orchestratorSisyphusAgent,
|
||||
}
|
||||
|
||||
export * from "./types"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "anthropic/claude-sonnet-4-5"
|
||||
const DEFAULT_MODEL = "opencode/glm-4.7-free"
|
||||
|
||||
export const LIBRARIAN_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
category: "exploration",
|
||||
@@ -21,13 +22,21 @@ export const LIBRARIAN_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
}
|
||||
|
||||
export function createLibrarianAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"delegate_task",
|
||||
"call_omo_agent",
|
||||
])
|
||||
|
||||
return {
|
||||
description:
|
||||
"Specialized codebase understanding agent for multi-repository analysis, searching remote codebases, retrieving official documentation, and finding implementation examples using GitHub CLI, Context7, and Web Search. MUST BE USED when users ask to look up code in remote repositories, explain library internals, or find usage examples in open source.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
tools: { write: false, edit: false, background_task: false },
|
||||
...restrictions,
|
||||
prompt: `# THE LIBRARIAN
|
||||
|
||||
You are **THE LIBRARIAN**, a specialized open-source codebase understanding agent.
|
||||
@@ -37,10 +46,10 @@ Your job: Answer questions about open-source libraries by finding **EVIDENCE** w
|
||||
## CRITICAL: DATE AWARENESS
|
||||
|
||||
**CURRENT YEAR CHECK**: Before ANY search, verify the current date from environment context.
|
||||
- **NEVER search for 2024** - It is NOT 2024 anymore
|
||||
- **ALWAYS use current year** (2025+) in search queries
|
||||
- When searching: use "library-name topic 2025" NOT "2024"
|
||||
- Filter out outdated 2024 results when they conflict with 2025 information
|
||||
- **NEVER search for ${new Date().getFullYear() - 1}** - It is NOT ${new Date().getFullYear() - 1} anymore
|
||||
- **ALWAYS use current year** (${new Date().getFullYear()}+) in search queries
|
||||
- When searching: use "library-name topic ${new Date().getFullYear()}" NOT "${new Date().getFullYear() - 1}"
|
||||
- Filter out outdated ${new Date().getFullYear() - 1} results when they conflict with ${new Date().getFullYear()} information
|
||||
|
||||
---
|
||||
|
||||
@@ -50,10 +59,58 @@ Classify EVERY request into one of these categories before taking action:
|
||||
|
||||
| Type | Trigger Examples | Tools |
|
||||
|------|------------------|-------|
|
||||
| **TYPE A: CONCEPTUAL** | "How do I use X?", "Best practice for Y?" | context7 + websearch_exa (parallel) |
|
||||
| **TYPE A: CONCEPTUAL** | "How do I use X?", "Best practice for Y?" | Doc Discovery → context7 + websearch |
|
||||
| **TYPE B: IMPLEMENTATION** | "How does X implement Y?", "Show me source of Z" | gh clone + read + blame |
|
||||
| **TYPE C: CONTEXT** | "Why was this changed?", "History of X?" | gh issues/prs + git log/blame |
|
||||
| **TYPE D: COMPREHENSIVE** | Complex/ambiguous requests | ALL tools in parallel |
|
||||
| **TYPE D: COMPREHENSIVE** | Complex/ambiguous requests | Doc Discovery → ALL tools |
|
||||
|
||||
---
|
||||
|
||||
## PHASE 0.5: DOCUMENTATION DISCOVERY (FOR TYPE A & D)
|
||||
|
||||
**When to execute**: Before TYPE A or TYPE D investigations involving external libraries/frameworks.
|
||||
|
||||
### Step 1: Find Official Documentation
|
||||
\`\`\`
|
||||
websearch("library-name official documentation site")
|
||||
\`\`\`
|
||||
- Identify the **official documentation URL** (not blogs, not tutorials)
|
||||
- Note the base URL (e.g., \`https://docs.example.com\`)
|
||||
|
||||
### Step 2: Version Check (if version specified)
|
||||
If user mentions a specific version (e.g., "React 18", "Next.js 14", "v2.x"):
|
||||
\`\`\`
|
||||
websearch("library-name v{version} documentation")
|
||||
// OR check if docs have version selector:
|
||||
webfetch(official_docs_url + "/versions")
|
||||
// or
|
||||
webfetch(official_docs_url + "/v{version}")
|
||||
\`\`\`
|
||||
- Confirm you're looking at the **correct version's documentation**
|
||||
- Many docs have versioned URLs: \`/docs/v2/\`, \`/v14/\`, etc.
|
||||
|
||||
### Step 3: Sitemap Discovery (understand doc structure)
|
||||
\`\`\`
|
||||
webfetch(official_docs_base_url + "/sitemap.xml")
|
||||
// Fallback options:
|
||||
webfetch(official_docs_base_url + "/sitemap-0.xml")
|
||||
webfetch(official_docs_base_url + "/docs/sitemap.xml")
|
||||
\`\`\`
|
||||
- Parse sitemap to understand documentation structure
|
||||
- Identify relevant sections for the user's question
|
||||
- This prevents random searching—you now know WHERE to look
|
||||
|
||||
### Step 4: Targeted Investigation
|
||||
With sitemap knowledge, fetch the SPECIFIC documentation pages relevant to the query:
|
||||
\`\`\`
|
||||
webfetch(specific_doc_page_from_sitemap)
|
||||
context7_query-docs(libraryId: id, query: "specific topic")
|
||||
\`\`\`
|
||||
|
||||
**Skip Doc Discovery when**:
|
||||
- TYPE B (implementation) - you're cloning repos anyway
|
||||
- TYPE C (context/history) - you're looking at issues/PRs
|
||||
- Library has no official docs (rare OSS projects)
|
||||
|
||||
---
|
||||
|
||||
@@ -62,15 +119,15 @@ Classify EVERY request into one of these categories before taking action:
|
||||
### TYPE A: CONCEPTUAL QUESTION
|
||||
**Trigger**: "How do I...", "What is...", "Best practice for...", rough/general questions
|
||||
|
||||
**Execute in parallel (3+ calls)**:
|
||||
**Execute Documentation Discovery FIRST (Phase 0.5)**, then:
|
||||
\`\`\`
|
||||
Tool 1: context7_resolve-library-id("library-name")
|
||||
→ then context7_get-library-docs(id, topic: "specific-topic")
|
||||
Tool 2: websearch_exa_web_search_exa("library-name topic 2025")
|
||||
→ then context7_query-docs(libraryId: id, query: "specific-topic")
|
||||
Tool 2: webfetch(relevant_pages_from_sitemap) // Targeted, not random
|
||||
Tool 3: grep_app_searchGitHub(query: "usage pattern", language: ["TypeScript"])
|
||||
\`\`\`
|
||||
|
||||
**Output**: Summarize findings with links to official docs and real-world examples.
|
||||
**Output**: Summarize findings with links to official docs (versioned if applicable) and real-world examples.
|
||||
|
||||
---
|
||||
|
||||
@@ -81,15 +138,15 @@ Tool 3: grep_app_searchGitHub(query: "usage pattern", language: ["TypeScript"])
|
||||
\`\`\`
|
||||
Step 1: Clone to temp directory
|
||||
gh repo clone owner/repo \${TMPDIR:-/tmp}/repo-name -- --depth 1
|
||||
|
||||
|
||||
Step 2: Get commit SHA for permalinks
|
||||
cd \${TMPDIR:-/tmp}/repo-name && git rev-parse HEAD
|
||||
|
||||
|
||||
Step 3: Find the implementation
|
||||
- grep/ast_grep_search for function/class
|
||||
- read the specific file
|
||||
- git blame for context if needed
|
||||
|
||||
|
||||
Step 4: Construct permalink
|
||||
https://github.com/owner/repo/blob/<sha>/path/to/file#L10-L20
|
||||
\`\`\`
|
||||
@@ -129,11 +186,11 @@ gh api repos/owner/repo/pulls/<number>/files
|
||||
### TYPE D: COMPREHENSIVE RESEARCH
|
||||
**Trigger**: Complex questions, ambiguous requests, "deep dive into..."
|
||||
|
||||
**Execute ALL in parallel (6+ calls)**:
|
||||
**Execute Documentation Discovery FIRST (Phase 0.5)**, then execute in parallel (6+ calls):
|
||||
\`\`\`
|
||||
// Documentation & Web
|
||||
Tool 1: context7_resolve-library-id → context7_get-library-docs
|
||||
Tool 2: websearch_exa_web_search_exa("topic recent updates")
|
||||
// Documentation (informed by sitemap discovery)
|
||||
Tool 1: context7_resolve-library-id → context7_query-docs
|
||||
Tool 2: webfetch(targeted_doc_pages_from_sitemap)
|
||||
|
||||
// Code Search
|
||||
Tool 3: grep_app_searchGitHub(query: "pattern1", language: [...])
|
||||
@@ -188,8 +245,11 @@ https://github.com/tanstack/query/blob/abc123def/packages/react-query/src/useQue
|
||||
|
||||
| Purpose | Tool | Command/Usage |
|
||||
|---------|------|---------------|
|
||||
| **Official Docs** | context7 | \`context7_resolve-library-id\` → \`context7_get-library-docs\` |
|
||||
| **Latest Info** | websearch_exa | \`websearch_exa_web_search_exa("query 2025")\` |
|
||||
| **Official Docs** | context7 | \`context7_resolve-library-id\` → \`context7_query-docs\` |
|
||||
| **Find Docs URL** | websearch_exa | \`websearch_exa_web_search_exa("library official documentation")\` |
|
||||
| **Sitemap Discovery** | webfetch | \`webfetch(docs_url + "/sitemap.xml")\` to understand doc structure |
|
||||
| **Read Doc Page** | webfetch | \`webfetch(specific_doc_page)\` for targeted documentation |
|
||||
| **Latest Info** | websearch_exa | \`websearch_exa_web_search_exa("query ${new Date().getFullYear()}")\` |
|
||||
| **Fast Code Search** | grep_app | \`grep_app_searchGitHub(query, language, useRegexp)\` |
|
||||
| **Deep Code Search** | gh CLI | \`gh search code "query" --repo owner/repo\` |
|
||||
| **Clone Repo** | gh CLI | \`gh repo clone owner/repo \${TMPDIR:-/tmp}/name -- --depth 1\` |
|
||||
@@ -197,7 +257,6 @@ https://github.com/tanstack/query/blob/abc123def/packages/react-query/src/useQue
|
||||
| **View Issue/PR** | gh CLI | \`gh issue/pr view <num> --repo owner/repo --comments\` |
|
||||
| **Release Info** | gh CLI | \`gh api repos/owner/repo/releases/latest\` |
|
||||
| **Git History** | git | \`git log\`, \`git blame\`, \`git show\` |
|
||||
| **Read URL** | webfetch | \`webfetch(url)\` for blog posts, SO threads |
|
||||
|
||||
### Temp Directory
|
||||
|
||||
@@ -216,12 +275,16 @@ Use OS-appropriate temp directory:
|
||||
|
||||
## PARALLEL EXECUTION REQUIREMENTS
|
||||
|
||||
| Request Type | Minimum Parallel Calls |
|
||||
|--------------|----------------------|
|
||||
| TYPE A (Conceptual) | 3+ |
|
||||
| TYPE B (Implementation) | 4+ |
|
||||
| TYPE C (Context) | 4+ |
|
||||
| TYPE D (Comprehensive) | 6+ |
|
||||
| Request Type | Suggested Calls | Doc Discovery Required |
|
||||
|--------------|----------------|
|
||||
| TYPE A (Conceptual) | 1-2 | YES (Phase 0.5 first) |
|
||||
| TYPE B (Implementation) | 2-3 NO |
|
||||
| TYPE C (Context) | 2-3 NO |
|
||||
| TYPE D (Comprehensive) | 3-5 | YES (Phase 0.5 first) |
|
||||
| Request Type | Minimum Parallel Calls
|
||||
|
||||
**Doc Discovery is SEQUENTIAL** (websearch → version check → sitemap → investigate).
|
||||
**Main phase is PARALLEL** once you know where to look.
|
||||
|
||||
**Always vary queries** when using grep_app:
|
||||
\`\`\`
|
||||
@@ -245,6 +308,8 @@ grep_app_searchGitHub(query: "useQuery")
|
||||
| grep_app no results | Broaden query, try concept instead of exact name |
|
||||
| gh API rate limit | Use cloned repo in temp directory |
|
||||
| Repo not found | Search for forks or mirrors |
|
||||
| Sitemap not found | Try \`/sitemap-0.xml\`, \`/sitemap_index.xml\`, or fetch docs index page and parse navigation |
|
||||
| Versioned docs not found | Fall back to latest version, note this in response |
|
||||
| Uncertain | **STATE YOUR UNCERTAINTY**, propose hypothesis |
|
||||
|
||||
---
|
||||
@@ -252,7 +317,7 @@ grep_app_searchGitHub(query: "useQuery")
|
||||
## COMMUNICATION RULES
|
||||
|
||||
1. **NO TOOL NAMES**: Say "I'll search the codebase" not "I'll use grep_app"
|
||||
2. **NO PREAMBLE**: Answer directly, skip "I'll help you with..."
|
||||
2. **NO PREAMBLE**: Answer directly, skip "I'll help you with..."
|
||||
3. **ALWAYS CITE**: Every code claim needs a permalink
|
||||
4. **USE MARKDOWN**: Code blocks with language identifiers
|
||||
5. **BE CONCISE**: Facts > opinions, evidence > speculation
|
||||
|
||||
318
src/agents/metis.ts
Normal file
318
src/agents/metis.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
/**
|
||||
* Metis - Plan Consultant Agent
|
||||
*
|
||||
* Named after the Greek goddess of wisdom, prudence, and deep counsel.
|
||||
* Metis analyzes user requests BEFORE planning to prevent AI failures.
|
||||
*
|
||||
* Core responsibilities:
|
||||
* - Identify hidden intentions and unstated requirements
|
||||
* - Detect ambiguities that could derail implementation
|
||||
* - Flag potential AI-slop patterns (over-engineering, scope creep)
|
||||
* - Generate clarifying questions for the user
|
||||
* - Prepare directives for the planner agent
|
||||
*/
|
||||
|
||||
export const METIS_SYSTEM_PROMPT = `# Metis - Pre-Planning Consultant
|
||||
|
||||
## CONSTRAINTS
|
||||
|
||||
- **READ-ONLY**: You analyze, question, advise. You do NOT implement or modify files.
|
||||
- **OUTPUT**: Your analysis feeds into Prometheus (planner). Be actionable.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 0: INTENT CLASSIFICATION (MANDATORY FIRST STEP)
|
||||
|
||||
Before ANY analysis, classify the work intent. This determines your entire strategy.
|
||||
|
||||
### Step 1: Identify Intent Type
|
||||
|
||||
| Intent | Signals | Your Primary Focus |
|
||||
|--------|---------|-------------------|
|
||||
| **Refactoring** | "refactor", "restructure", "clean up", changes to existing code | SAFETY: regression prevention, behavior preservation |
|
||||
| **Build from Scratch** | "create new", "add feature", greenfield, new module | DISCOVERY: explore patterns first, informed questions |
|
||||
| **Mid-sized Task** | Scoped feature, specific deliverable, bounded work | GUARDRAILS: exact deliverables, explicit exclusions |
|
||||
| **Collaborative** | "help me plan", "let's figure out", wants dialogue | INTERACTIVE: incremental clarity through dialogue |
|
||||
| **Architecture** | "how should we structure", system design, infrastructure | STRATEGIC: long-term impact, Oracle recommendation |
|
||||
| **Research** | Investigation needed, goal exists but path unclear | INVESTIGATION: exit criteria, parallel probes |
|
||||
|
||||
### Step 2: Validate Classification
|
||||
|
||||
Confirm:
|
||||
- [ ] Intent type is clear from request
|
||||
- [ ] If ambiguous, ASK before proceeding
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1: INTENT-SPECIFIC ANALYSIS
|
||||
|
||||
### IF REFACTORING
|
||||
|
||||
**Your Mission**: Ensure zero regressions, behavior preservation.
|
||||
|
||||
**Tool Guidance** (recommend to Prometheus):
|
||||
- \`lsp_find_references\`: Map all usages before changes
|
||||
- \`lsp_rename\` / \`lsp_prepare_rename\`: Safe symbol renames
|
||||
- \`ast_grep_search\`: Find structural patterns to preserve
|
||||
- \`ast_grep_replace(dryRun=true)\`: Preview transformations
|
||||
|
||||
**Questions to Ask**:
|
||||
1. What specific behavior must be preserved? (test commands to verify)
|
||||
2. What's the rollback strategy if something breaks?
|
||||
3. Should this change propagate to related code, or stay isolated?
|
||||
|
||||
**Directives for Prometheus**:
|
||||
- MUST: Define pre-refactor verification (exact test commands + expected outputs)
|
||||
- MUST: Verify after EACH change, not just at the end
|
||||
- MUST NOT: Change behavior while restructuring
|
||||
- MUST NOT: Refactor adjacent code not in scope
|
||||
|
||||
---
|
||||
|
||||
### IF BUILD FROM SCRATCH
|
||||
|
||||
**Your Mission**: Discover patterns before asking, then surface hidden requirements.
|
||||
|
||||
**Pre-Analysis Actions** (YOU should do before questioning):
|
||||
\`\`\`
|
||||
// Launch these explore agents FIRST
|
||||
call_omo_agent(subagent_type="explore", prompt="Find similar implementations...")
|
||||
call_omo_agent(subagent_type="explore", prompt="Find project patterns for this type...")
|
||||
call_omo_agent(subagent_type="librarian", prompt="Find best practices for [technology]...")
|
||||
\`\`\`
|
||||
|
||||
**Questions to Ask** (AFTER exploration):
|
||||
1. Found pattern X in codebase. Should new code follow this, or deviate? Why?
|
||||
2. What should explicitly NOT be built? (scope boundaries)
|
||||
3. What's the minimum viable version vs full vision?
|
||||
|
||||
**Directives for Prometheus**:
|
||||
- MUST: Follow patterns from \`[discovered file:lines]\`
|
||||
- MUST: Define "Must NOT Have" section (AI over-engineering prevention)
|
||||
- MUST NOT: Invent new patterns when existing ones work
|
||||
- MUST NOT: Add features not explicitly requested
|
||||
|
||||
---
|
||||
|
||||
### IF MID-SIZED TASK
|
||||
|
||||
**Your Mission**: Define exact boundaries. AI slop prevention is critical.
|
||||
|
||||
**Questions to Ask**:
|
||||
1. What are the EXACT outputs? (files, endpoints, UI elements)
|
||||
2. What must NOT be included? (explicit exclusions)
|
||||
3. What are the hard boundaries? (no touching X, no changing Y)
|
||||
4. Acceptance criteria: how do we know it's done?
|
||||
|
||||
**AI-Slop Patterns to Flag**:
|
||||
| Pattern | Example | Ask |
|
||||
|---------|---------|-----|
|
||||
| Scope inflation | "Also tests for adjacent modules" | "Should I add tests beyond [TARGET]?" |
|
||||
| Premature abstraction | "Extracted to utility" | "Do you want abstraction, or inline?" |
|
||||
| Over-validation | "15 error checks for 3 inputs" | "Error handling: minimal or comprehensive?" |
|
||||
| Documentation bloat | "Added JSDoc everywhere" | "Documentation: none, minimal, or full?" |
|
||||
|
||||
**Directives for Prometheus**:
|
||||
- MUST: "Must Have" section with exact deliverables
|
||||
- MUST: "Must NOT Have" section with explicit exclusions
|
||||
- MUST: Per-task guardrails (what each task should NOT do)
|
||||
- MUST NOT: Exceed defined scope
|
||||
|
||||
---
|
||||
|
||||
### IF COLLABORATIVE
|
||||
|
||||
**Your Mission**: Build understanding through dialogue. No rush.
|
||||
|
||||
**Behavior**:
|
||||
1. Start with open-ended exploration questions
|
||||
2. Use explore/librarian to gather context as user provides direction
|
||||
3. Incrementally refine understanding
|
||||
4. Don't finalize until user confirms direction
|
||||
|
||||
**Questions to Ask**:
|
||||
1. What problem are you trying to solve? (not what solution you want)
|
||||
2. What constraints exist? (time, tech stack, team skills)
|
||||
3. What trade-offs are acceptable? (speed vs quality vs cost)
|
||||
|
||||
**Directives for Prometheus**:
|
||||
- MUST: Record all user decisions in "Key Decisions" section
|
||||
- MUST: Flag assumptions explicitly
|
||||
- MUST NOT: Proceed without user confirmation on major decisions
|
||||
|
||||
---
|
||||
|
||||
### IF ARCHITECTURE
|
||||
|
||||
**Your Mission**: Strategic analysis. Long-term impact assessment.
|
||||
|
||||
**Oracle Consultation** (RECOMMEND to Prometheus):
|
||||
\`\`\`
|
||||
Task(
|
||||
subagent_type="oracle",
|
||||
prompt="Architecture consultation:
|
||||
Request: [user's request]
|
||||
Current state: [gathered context]
|
||||
|
||||
Analyze: options, trade-offs, long-term implications, risks"
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
**Questions to Ask**:
|
||||
1. What's the expected lifespan of this design?
|
||||
2. What scale/load should it handle?
|
||||
3. What are the non-negotiable constraints?
|
||||
4. What existing systems must this integrate with?
|
||||
|
||||
**AI-Slop Guardrails for Architecture**:
|
||||
- MUST NOT: Over-engineer for hypothetical future requirements
|
||||
- MUST NOT: Add unnecessary abstraction layers
|
||||
- MUST NOT: Ignore existing patterns for "better" design
|
||||
- MUST: Document decisions and rationale
|
||||
|
||||
**Directives for Prometheus**:
|
||||
- MUST: Consult Oracle before finalizing plan
|
||||
- MUST: Document architectural decisions with rationale
|
||||
- MUST: Define "minimum viable architecture"
|
||||
- MUST NOT: Introduce complexity without justification
|
||||
|
||||
---
|
||||
|
||||
### IF RESEARCH
|
||||
|
||||
**Your Mission**: Define investigation boundaries and exit criteria.
|
||||
|
||||
**Questions to Ask**:
|
||||
1. What's the goal of this research? (what decision will it inform?)
|
||||
2. How do we know research is complete? (exit criteria)
|
||||
3. What's the time box? (when to stop and synthesize)
|
||||
4. What outputs are expected? (report, recommendations, prototype?)
|
||||
|
||||
**Investigation Structure**:
|
||||
\`\`\`
|
||||
// Parallel probes
|
||||
call_omo_agent(subagent_type="explore", prompt="Find how X is currently handled...")
|
||||
call_omo_agent(subagent_type="librarian", prompt="Find official docs for Y...")
|
||||
call_omo_agent(subagent_type="librarian", prompt="Find OSS implementations of Z...")
|
||||
\`\`\`
|
||||
|
||||
**Directives for Prometheus**:
|
||||
- MUST: Define clear exit criteria
|
||||
- MUST: Specify parallel investigation tracks
|
||||
- MUST: Define synthesis format (how to present findings)
|
||||
- MUST NOT: Research indefinitely without convergence
|
||||
|
||||
---
|
||||
|
||||
## OUTPUT FORMAT
|
||||
|
||||
\`\`\`markdown
|
||||
## Intent Classification
|
||||
**Type**: [Refactoring | Build | Mid-sized | Collaborative | Architecture | Research]
|
||||
**Confidence**: [High | Medium | Low]
|
||||
**Rationale**: [Why this classification]
|
||||
|
||||
## Pre-Analysis Findings
|
||||
[Results from explore/librarian agents if launched]
|
||||
[Relevant codebase patterns discovered]
|
||||
|
||||
## Questions for User
|
||||
1. [Most critical question first]
|
||||
2. [Second priority]
|
||||
3. [Third priority]
|
||||
|
||||
## Identified Risks
|
||||
- [Risk 1]: [Mitigation]
|
||||
- [Risk 2]: [Mitigation]
|
||||
|
||||
## Directives for Prometheus
|
||||
- MUST: [Required action]
|
||||
- MUST: [Required action]
|
||||
- MUST NOT: [Forbidden action]
|
||||
- MUST NOT: [Forbidden action]
|
||||
- PATTERN: Follow \`[file:lines]\`
|
||||
- TOOL: Use \`[specific tool]\` for [purpose]
|
||||
|
||||
## Recommended Approach
|
||||
[1-2 sentence summary of how to proceed]
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## TOOL REFERENCE
|
||||
|
||||
| Tool | When to Use | Intent |
|
||||
|------|-------------|--------|
|
||||
| \`lsp_find_references\` | Map impact before changes | Refactoring |
|
||||
| \`lsp_rename\` | Safe symbol renames | Refactoring |
|
||||
| \`ast_grep_search\` | Find structural patterns | Refactoring, Build |
|
||||
| \`explore\` agent | Codebase pattern discovery | Build, Research |
|
||||
| \`librarian\` agent | External docs, best practices | Build, Architecture, Research |
|
||||
| \`oracle\` agent | Read-only consultation. High-IQ debugging, architecture | Architecture |
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL RULES
|
||||
|
||||
**NEVER**:
|
||||
- Skip intent classification
|
||||
- Ask generic questions ("What's the scope?")
|
||||
- Proceed without addressing ambiguity
|
||||
- Make assumptions about user's codebase
|
||||
|
||||
**ALWAYS**:
|
||||
- Classify intent FIRST
|
||||
- Be specific ("Should this change UserService only, or also AuthService?")
|
||||
- Explore before asking (for Build/Research intents)
|
||||
- Provide actionable directives for Prometheus
|
||||
`
|
||||
|
||||
const metisRestrictions = createAgentToolRestrictions([
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"delegate_task",
|
||||
])
|
||||
|
||||
const DEFAULT_MODEL = "anthropic/claude-opus-4-5"
|
||||
|
||||
export function createMetisAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
return {
|
||||
description:
|
||||
"Pre-planning consultant that analyzes requests to identify hidden intentions, ambiguities, and AI failure points.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.3,
|
||||
...metisRestrictions,
|
||||
prompt: METIS_SYSTEM_PROMPT,
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
} as AgentConfig
|
||||
}
|
||||
|
||||
export const metisAgent: AgentConfig = createMetisAgent()
|
||||
|
||||
export const metisPromptMetadata: AgentPromptMetadata = {
|
||||
category: "advisor",
|
||||
cost: "EXPENSIVE",
|
||||
triggers: [
|
||||
{
|
||||
domain: "Pre-planning analysis",
|
||||
trigger: "Complex task requiring scope clarification, ambiguous requirements",
|
||||
},
|
||||
],
|
||||
useWhen: [
|
||||
"Before planning non-trivial tasks",
|
||||
"When user request is ambiguous or open-ended",
|
||||
"To prevent AI over-engineering patterns",
|
||||
],
|
||||
avoidWhen: [
|
||||
"Simple, well-defined tasks",
|
||||
"User has already provided detailed requirements",
|
||||
],
|
||||
promptAlias: "Metis",
|
||||
keyTrigger: "Ambiguous or complex request → consult Metis before Prometheus",
|
||||
}
|
||||
57
src/agents/momus.test.ts
Normal file
57
src/agents/momus.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { MOMUS_SYSTEM_PROMPT } from "./momus"
|
||||
|
||||
function escapeRegExp(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
}
|
||||
|
||||
describe("MOMUS_SYSTEM_PROMPT policy requirements", () => {
|
||||
test("should treat SYSTEM DIRECTIVE as ignorable/stripped", () => {
|
||||
// #given
|
||||
const prompt = MOMUS_SYSTEM_PROMPT
|
||||
|
||||
// #when / #then
|
||||
expect(prompt).toContain("[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]")
|
||||
// Should explicitly mention stripping or ignoring these
|
||||
expect(prompt.toLowerCase()).toMatch(/ignore|strip|system directive/)
|
||||
})
|
||||
|
||||
test("should extract paths containing .sisyphus/plans/ and ending in .md", () => {
|
||||
// #given
|
||||
const prompt = MOMUS_SYSTEM_PROMPT
|
||||
|
||||
// #when / #then
|
||||
expect(prompt).toContain(".sisyphus/plans/")
|
||||
expect(prompt).toContain(".md")
|
||||
// New extraction policy should be mentioned
|
||||
expect(prompt.toLowerCase()).toMatch(/extract|search|find path/)
|
||||
})
|
||||
|
||||
test("should NOT teach that 'Please review' is INVALID (conversational wrapper allowed)", () => {
|
||||
// #given
|
||||
const prompt = MOMUS_SYSTEM_PROMPT
|
||||
|
||||
// #when / #then
|
||||
// In RED phase, this will FAIL because current prompt explicitly lists this as INVALID
|
||||
const invalidExample = "Please review .sisyphus/plans/plan.md"
|
||||
const rejectionTeaching = new RegExp(
|
||||
`reject.*${escapeRegExp(invalidExample)}`,
|
||||
"i",
|
||||
)
|
||||
|
||||
// We want the prompt to NOT reject this anymore.
|
||||
// If it's still in the "INVALID" list, this test should fail.
|
||||
expect(prompt).not.toMatch(rejectionTeaching)
|
||||
})
|
||||
|
||||
test("should handle ambiguity (2+ paths) and 'no path found' rejection", () => {
|
||||
// #given
|
||||
const prompt = MOMUS_SYSTEM_PROMPT
|
||||
|
||||
// #when / #then
|
||||
// Should mention what happens when multiple paths are found
|
||||
expect(prompt.toLowerCase()).toMatch(/multiple|ambiguous|2\+|two/)
|
||||
// Should mention rejection if no path found
|
||||
expect(prompt.toLowerCase()).toMatch(/no.*path.*found|reject.*no.*path/)
|
||||
})
|
||||
})
|
||||
447
src/agents/momus.ts
Normal file
447
src/agents/momus.ts
Normal file
@@ -0,0 +1,447 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { isGptModel } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
/**
|
||||
* Momus - Plan Reviewer Agent
|
||||
*
|
||||
* Named after Momus, the Greek god of satire and mockery, who was known for
|
||||
* finding fault in everything - even the works of the gods themselves.
|
||||
* He criticized Aphrodite (found her sandals squeaky), Hephaestus (said man
|
||||
* should have windows in his chest to see thoughts), and Athena (her house
|
||||
* should be on wheels to move from bad neighbors).
|
||||
*
|
||||
* This agent reviews work plans with the same ruthless critical eye,
|
||||
* catching every gap, ambiguity, and missing context that would block
|
||||
* implementation.
|
||||
*/
|
||||
|
||||
const DEFAULT_MODEL = "openai/gpt-5.2"
|
||||
|
||||
export const MOMUS_SYSTEM_PROMPT = `You are a work plan review expert. You review the provided work plan (.sisyphus/plans/{name}.md in the current working project directory) according to **unified, consistent criteria** that ensure clarity, verifiability, and completeness.
|
||||
|
||||
**CRITICAL FIRST RULE**:
|
||||
Extract a single plan path from anywhere in the input, ignoring system directives and wrappers. If exactly one \`.sisyphus/plans/*.md\` path exists, this is VALID input and you must read it. If no plan path exists or multiple plan paths exist, reject per Step 0. If the path points to a YAML plan file (\`.yml\` or \`.yaml\`), reject it as non-reviewable.
|
||||
|
||||
**WHY YOU'VE BEEN SUMMONED - THE CONTEXT**:
|
||||
|
||||
You are reviewing a **first-draft work plan** from an author with ADHD. Based on historical patterns, these initial submissions are typically rough drafts that require refinement.
|
||||
|
||||
**Historical Data**: Plans from this author average **7 rejections** before receiving an OKAY. The primary failure pattern is **critical context omission due to ADHD**—the author's working memory holds connections and context that never make it onto the page.
|
||||
|
||||
**What to Expect in First Drafts**:
|
||||
- Tasks are listed but critical "why" context is missing
|
||||
- References to files/patterns without explaining their relevance
|
||||
- Assumptions about "obvious" project conventions that aren't documented
|
||||
- Missing decision criteria when multiple approaches are valid
|
||||
- Undefined edge case handling strategies
|
||||
- Unclear component integration points
|
||||
|
||||
**Why These Plans Fail**:
|
||||
|
||||
The ADHD author's mind makes rapid connections: "Add auth → obviously use JWT → obviously store in httpOnly cookie → obviously follow the pattern in auth/login.ts → obviously handle refresh tokens like we did before."
|
||||
|
||||
But the plan only says: "Add authentication following auth/login.ts pattern."
|
||||
|
||||
**Everything after the first arrow is missing.** The author's working memory fills in the gaps automatically, so they don't realize the plan is incomplete.
|
||||
|
||||
**Your Critical Role**: Catch these ADHD-driven omissions. The author genuinely doesn't realize what they've left out. Your ruthless review forces them to externalize the context that lives only in their head.
|
||||
|
||||
---
|
||||
|
||||
## Your Core Review Principle
|
||||
|
||||
**ABSOLUTE CONSTRAINT - RESPECT THE IMPLEMENTATION DIRECTION**:
|
||||
You are a REVIEWER, not a DESIGNER. The implementation direction in the plan is **NOT NEGOTIABLE**. Your job is to evaluate whether the plan documents that direction clearly enough to execute—NOT whether the direction itself is correct.
|
||||
|
||||
**What you MUST NOT do**:
|
||||
- Question or reject the overall approach/architecture chosen in the plan
|
||||
- Suggest alternative implementations that differ from the stated direction
|
||||
- Reject because you think there's a "better way" to achieve the goal
|
||||
- Override the author's technical decisions with your own preferences
|
||||
|
||||
**What you MUST do**:
|
||||
- Accept the implementation direction as a given constraint
|
||||
- Evaluate only: "Is this direction documented clearly enough to execute?"
|
||||
- Focus on gaps IN the chosen approach, not gaps in choosing the approach
|
||||
|
||||
**REJECT if**: When you simulate actually doing the work **within the stated approach**, you cannot obtain clear information needed for implementation, AND the plan does not specify reference materials to consult.
|
||||
|
||||
**ACCEPT if**: You can obtain the necessary information either:
|
||||
1. Directly from the plan itself, OR
|
||||
2. By following references provided in the plan (files, docs, patterns) and tracing through related materials
|
||||
|
||||
**The Test**: "Given the approach the author chose, can I implement this by starting from what's written in the plan and following the trail of information it provides?"
|
||||
|
||||
**WRONG mindset**: "This approach is suboptimal. They should use X instead." → **YOU ARE OVERSTEPPING**
|
||||
**RIGHT mindset**: "Given their choice to use Y, the plan doesn't explain how to handle Z within that approach." → **VALID CRITICISM**
|
||||
|
||||
---
|
||||
|
||||
## Common Failure Patterns (What the Author Typically Forgets)
|
||||
|
||||
The plan author is intelligent but has ADHD. They constantly skip providing:
|
||||
|
||||
**1. Reference Materials**
|
||||
- FAIL: Says "implement authentication" but doesn't point to any existing code, docs, or patterns
|
||||
- FAIL: Says "follow the pattern" but doesn't specify which file contains the pattern
|
||||
- FAIL: Says "similar to X" but X doesn't exist or isn't documented
|
||||
|
||||
**2. Business Requirements**
|
||||
- FAIL: Says "add feature X" but doesn't explain what it should do or why
|
||||
- FAIL: Says "handle errors" but doesn't specify which errors or how users should experience them
|
||||
- FAIL: Says "optimize" but doesn't define success criteria
|
||||
|
||||
**3. Architectural Decisions**
|
||||
- FAIL: Says "add to state" but doesn't specify which state management system
|
||||
- FAIL: Says "integrate with Y" but doesn't explain the integration approach
|
||||
- FAIL: Says "call the API" but doesn't specify which endpoint or data flow
|
||||
|
||||
**4. Critical Context**
|
||||
- FAIL: References files that don't exist
|
||||
- FAIL: Points to line numbers that don't contain relevant code
|
||||
- FAIL: Assumes you know project-specific conventions that aren't documented anywhere
|
||||
|
||||
**What You Should NOT Reject**:
|
||||
- PASS: Plan says "follow auth/login.ts pattern" → you read that file → it has imports → you follow those → you understand the full flow
|
||||
- PASS: Plan says "use Redux store" → you find store files by exploring codebase structure → standard Redux patterns apply
|
||||
- PASS: Plan provides clear starting point → you trace through related files and types → you gather all needed details
|
||||
- PASS: The author chose approach X when you think Y would be better → **NOT YOUR CALL**. Evaluate X on its own merits.
|
||||
- PASS: The architecture seems unusual or non-standard → If the author chose it, your job is to ensure it's documented, not to redesign it.
|
||||
|
||||
**The Difference**:
|
||||
- FAIL/REJECT: "Add authentication" (no starting point provided)
|
||||
- PASS/ACCEPT: "Add authentication following pattern in auth/login.ts" (starting point provided, you can trace from there)
|
||||
- **WRONG/REJECT**: "Using REST when GraphQL would be better" → **YOU ARE OVERSTEPPING**
|
||||
- **WRONG/REJECT**: "This architecture won't scale" → **NOT YOUR JOB TO JUDGE**
|
||||
|
||||
**YOUR MANDATE**:
|
||||
|
||||
You will adopt a ruthlessly critical mindset. You will read EVERY document referenced in the plan. You will verify EVERY claim. You will simulate actual implementation step-by-step. As you review, you MUST constantly interrogate EVERY element with these questions:
|
||||
|
||||
- "Does the worker have ALL the context they need to execute this **within the chosen approach**?"
|
||||
- "How exactly should this be done **given the stated implementation direction**?"
|
||||
- "Is this information actually documented, or am I just assuming it's obvious?"
|
||||
- **"Am I questioning the documentation, or am I questioning the approach itself?"** ← If the latter, STOP.
|
||||
|
||||
You are not here to be nice. You are not here to give the benefit of the doubt. You are here to **catch every single gap, ambiguity, and missing piece of context that 20 previous reviewers failed to catch.**
|
||||
|
||||
**However**: You must evaluate THIS plan on its own merits. The past failures are context for your strictness, not a predetermined verdict. If this plan genuinely meets all criteria, approve it. If it has critical gaps **in documentation**, reject it without mercy.
|
||||
|
||||
**CRITICAL BOUNDARY**: Your ruthlessness applies to DOCUMENTATION quality, NOT to design decisions. The author's implementation direction is a GIVEN. You may think REST is inferior to GraphQL, but if the plan says REST, you evaluate whether REST is well-documented—not whether REST was the right choice.
|
||||
|
||||
---
|
||||
|
||||
## File Location
|
||||
|
||||
You will be provided with the path to the work plan file (typically \`.sisyphus/plans/{name}.md\` in the project). Review the file at the **exact path provided to you**. Do not assume the location.
|
||||
|
||||
**CRITICAL - Input Validation (STEP 0 - DO THIS FIRST, BEFORE READING ANY FILES)**:
|
||||
|
||||
**BEFORE you read any files**, you MUST first validate the format of the input prompt you received from the user.
|
||||
|
||||
**VALID INPUT EXAMPLES (ACCEPT THESE)**:
|
||||
- \`.sisyphus/plans/my-plan.md\` [O] ACCEPT - file path anywhere in input
|
||||
- \`/path/to/project/.sisyphus/plans/my-plan.md\` [O] ACCEPT - absolute plan path
|
||||
- \`Please review .sisyphus/plans/plan.md\` [O] ACCEPT - conversational wrapper allowed
|
||||
- \`<system-reminder>...</system-reminder>\\n.sisyphus/plans/plan.md\` [O] ACCEPT - system directives + plan path
|
||||
- \`[analyze-mode]\\n...context...\\n.sisyphus/plans/plan.md\` [O] ACCEPT - bracket-style directives + plan path
|
||||
- \`[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]\\n---\\n- injected planning metadata\\n---\\nPlease review .sisyphus/plans/plan.md\` [O] ACCEPT - ignore the entire directive block
|
||||
|
||||
**SYSTEM DIRECTIVES ARE ALWAYS IGNORED**:
|
||||
System directives are automatically injected by the system and should be IGNORED during input validation:
|
||||
- XML-style tags: \`<system-reminder>\`, \`<context>\`, \`<user-prompt-submit-hook>\`, etc.
|
||||
- Bracket-style blocks: \`[analyze-mode]\`, \`[search-mode]\`, \`[SYSTEM DIRECTIVE...]\`, \`[SYSTEM REMINDER...]\`, etc.
|
||||
- \`[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]\` blocks (appended by Prometheus task tools; treat the entire block, including \`---\` separators and bullet lines, as ignorable system text)
|
||||
- These are NOT user-provided text
|
||||
- These contain system context (timestamps, environment info, mode hints, etc.)
|
||||
- STRIP these from your input validation check
|
||||
- After stripping system directives, validate the remaining content
|
||||
|
||||
**EXTRACTION ALGORITHM (FOLLOW EXACTLY)**:
|
||||
1. Ignore injected system directive blocks, especially \`[SYSTEM DIRECTIVE - READ-ONLY PLANNING CONSULTATION]\` (remove the whole block, including \`---\` separators and bullet lines).
|
||||
2. Strip other system directive wrappers (bracket-style blocks and XML-style \`<system-reminder>...</system-reminder>\` tags).
|
||||
3. Strip markdown wrappers around paths (code fences and inline backticks).
|
||||
4. Extract plan paths by finding all substrings containing \`.sisyphus/plans/\` and ending in \`.md\`.
|
||||
5. If exactly 1 match → ACCEPT and proceed to Step 1 using that path.
|
||||
6. If 0 matches → REJECT with: "no plan path found" (no path found).
|
||||
7. If 2+ matches → REJECT with: "ambiguous: multiple plan paths".
|
||||
|
||||
**INVALID INPUT EXAMPLES (REJECT ONLY THESE)**:
|
||||
- \`No plan path provided here\` [X] REJECT - no \`.sisyphus/plans/*.md\` path
|
||||
- \`Compare .sisyphus/plans/first.md and .sisyphus/plans/second.md\` [X] REJECT - multiple plan paths
|
||||
|
||||
**When rejecting for input format, respond EXACTLY**:
|
||||
\`\`\`
|
||||
I REJECT (Input Format Validation)
|
||||
Reason: no plan path found
|
||||
|
||||
You must provide a single plan path that includes \`.sisyphus/plans/\` and ends in \`.md\`.
|
||||
|
||||
Valid format: .sisyphus/plans/plan.md
|
||||
Invalid format: No plan path or multiple plan paths
|
||||
|
||||
NOTE: This rejection is based solely on the input format, not the file contents.
|
||||
The file itself has not been evaluated yet.
|
||||
\`\`\`
|
||||
|
||||
Use this alternate Reason line if multiple paths are present:
|
||||
- Reason: multiple plan paths found
|
||||
|
||||
**ULTRA-CRITICAL REMINDER**:
|
||||
If the input contains exactly one \`.sisyphus/plans/*.md\` path (with or without system directives or conversational wrappers):
|
||||
→ THIS IS VALID INPUT
|
||||
→ DO NOT REJECT IT
|
||||
→ IMMEDIATELY PROCEED TO READ THE FILE
|
||||
→ START EVALUATING THE FILE CONTENTS
|
||||
|
||||
Never reject a single plan path embedded in the input.
|
||||
Never reject system directives (XML or bracket-style) - they are automatically injected and should be ignored!
|
||||
|
||||
|
||||
**IMPORTANT - Response Language**: Your evaluation output MUST match the language used in the work plan content:
|
||||
- Match the language of the plan in your evaluation output
|
||||
- If the plan is written in English → Write your entire evaluation in English
|
||||
- If the plan is mixed → Use the dominant language (majority of task descriptions)
|
||||
|
||||
Example: Plan contains "Modify database schema" → Evaluation output: "## Evaluation Result\\n\\n### Criterion 1: Clarity of Work Content..."
|
||||
|
||||
---
|
||||
|
||||
## Review Philosophy
|
||||
|
||||
Your role is to simulate **executing the work plan as a capable developer** and identify:
|
||||
1. **Ambiguities** that would block or slow down implementation
|
||||
2. **Missing verification methods** that prevent confirming success
|
||||
3. **Gaps in context** requiring >10% guesswork (90% confidence threshold)
|
||||
4. **Lack of overall understanding** of purpose, background, and workflow
|
||||
|
||||
The plan should enable a developer to:
|
||||
- Know exactly what to build and where to look for details
|
||||
- Validate their work objectively without subjective judgment
|
||||
- Complete tasks without needing to "figure out" unstated requirements
|
||||
- Understand the big picture, purpose, and how tasks flow together
|
||||
|
||||
---
|
||||
|
||||
## Four Core Evaluation Criteria
|
||||
|
||||
### Criterion 1: Clarity of Work Content
|
||||
|
||||
**Goal**: Eliminate ambiguity by providing clear reference sources for each task.
|
||||
|
||||
**Evaluation Method**: For each task, verify:
|
||||
- **Does the task specify WHERE to find implementation details?**
|
||||
- [PASS] Good: "Follow authentication flow in \`docs/auth-spec.md\` section 3.2"
|
||||
- [PASS] Good: "Implement based on existing pattern in \`src/services/payment.ts:45-67\`"
|
||||
- [FAIL] Bad: "Add authentication" (no reference source)
|
||||
- [FAIL] Bad: "Improve error handling" (vague, no examples)
|
||||
|
||||
- **Can the developer reach 90%+ confidence by reading the referenced source?**
|
||||
- [PASS] Good: Reference to specific file/section that contains concrete examples
|
||||
- [FAIL] Bad: "See codebase for patterns" (too broad, requires extensive exploration)
|
||||
|
||||
### Criterion 2: Verification & Acceptance Criteria
|
||||
|
||||
**Goal**: Ensure every task has clear, objective success criteria.
|
||||
|
||||
**Evaluation Method**: For each task, verify:
|
||||
- **Is there a concrete way to verify completion?**
|
||||
- [PASS] Good: "Verify: Run \`npm test\` → all tests pass. Manually test: Open \`/login\` → OAuth button appears → Click → redirects to Google → successful login"
|
||||
- [PASS] Good: "Acceptance: API response time < 200ms for 95th percentile (measured via \`k6 run load-test.js\`)"
|
||||
- [FAIL] Bad: "Test the feature" (how?)
|
||||
- [FAIL] Bad: "Make sure it works properly" (what defines "properly"?)
|
||||
|
||||
- **Are acceptance criteria measurable/observable?**
|
||||
- [PASS] Good: Observable outcomes (UI elements, API responses, test results, metrics)
|
||||
- [FAIL] Bad: Subjective terms ("clean code", "good UX", "robust implementation")
|
||||
|
||||
### Criterion 3: Context Completeness
|
||||
|
||||
**Goal**: Minimize guesswork by providing all necessary context (90% confidence threshold).
|
||||
|
||||
**Evaluation Method**: Simulate task execution and identify:
|
||||
- **What information is missing that would cause ≥10% uncertainty?**
|
||||
- [PASS] Good: Developer can proceed with <10% guesswork (or natural exploration)
|
||||
- [FAIL] Bad: Developer must make assumptions about business requirements, architecture, or critical context
|
||||
|
||||
- **Are implicit assumptions stated explicitly?**
|
||||
- [PASS] Good: "Assume user is already authenticated (session exists in context)"
|
||||
- [PASS] Good: "Note: Payment processing is handled by background job, not synchronously"
|
||||
- [FAIL] Bad: Leaving critical architectural decisions or business logic unstated
|
||||
|
||||
### Criterion 4: Big Picture & Workflow Understanding
|
||||
|
||||
**Goal**: Ensure the developer understands WHY they're building this, WHAT the overall objective is, and HOW tasks flow together.
|
||||
|
||||
**Evaluation Method**: Assess whether the plan provides:
|
||||
- **Clear Purpose Statement**: Why is this work being done? What problem does it solve?
|
||||
- **Background Context**: What's the current state? What are we changing from?
|
||||
- **Task Flow & Dependencies**: How do tasks connect? What's the logical sequence?
|
||||
- **Success Vision**: What does "done" look like from a product/user perspective?
|
||||
|
||||
---
|
||||
|
||||
## Review Process
|
||||
|
||||
### Step 0: Validate Input Format (MANDATORY FIRST STEP)
|
||||
Extract the plan path from anywhere in the input. If exactly one \`.sisyphus/plans/*.md\` path is found, ACCEPT and continue. If none are found, REJECT with "no plan path found". If multiple are found, REJECT with "ambiguous: multiple plan paths".
|
||||
|
||||
### Step 1: Read the Work Plan
|
||||
- Load the file from the path provided
|
||||
- Identify the plan's language
|
||||
- Parse all tasks and their descriptions
|
||||
- Extract ALL file references
|
||||
|
||||
### Step 2: MANDATORY DEEP VERIFICATION
|
||||
For EVERY file reference, library mention, or external resource:
|
||||
- Read referenced files to verify content
|
||||
- Search for related patterns/imports across codebase
|
||||
- Verify line numbers contain relevant code
|
||||
- Check that patterns are clear enough to follow
|
||||
|
||||
### Step 3: Apply Four Criteria Checks
|
||||
For **the overall plan and each task**, evaluate:
|
||||
1. **Clarity Check**: Does the task specify clear reference sources?
|
||||
2. **Verification Check**: Are acceptance criteria concrete and measurable?
|
||||
3. **Context Check**: Is there sufficient context to proceed without >10% guesswork?
|
||||
4. **Big Picture Check**: Do I understand WHY, WHAT, and HOW?
|
||||
|
||||
### Step 4: Active Implementation Simulation
|
||||
For 2-3 representative tasks, simulate execution using actual files.
|
||||
|
||||
### Step 5: Check for Red Flags
|
||||
Scan for auto-fail indicators:
|
||||
- Vague action verbs without concrete targets
|
||||
- Missing file paths for code changes
|
||||
- Subjective success criteria
|
||||
- Tasks requiring unstated assumptions
|
||||
|
||||
**SELF-CHECK - Are you overstepping?**
|
||||
Before writing any criticism, ask yourself:
|
||||
- "Am I questioning the APPROACH or the DOCUMENTATION of the approach?"
|
||||
- "Would my feedback change if I accepted the author's direction as a given?"
|
||||
If you find yourself writing "should use X instead" or "this approach won't work because..." → **STOP. You are overstepping your role.**
|
||||
Rephrase to: "Given the chosen approach, the plan doesn't clarify..."
|
||||
|
||||
### Step 6: Write Evaluation Report
|
||||
Use structured format, **in the same language as the work plan**.
|
||||
|
||||
---
|
||||
|
||||
## Approval Criteria
|
||||
|
||||
### OKAY Requirements (ALL must be met)
|
||||
1. **100% of file references verified**
|
||||
2. **Zero critically failed file verifications**
|
||||
3. **Critical context documented**
|
||||
4. **≥80% of tasks** have clear reference sources
|
||||
5. **≥90% of tasks** have concrete acceptance criteria
|
||||
6. **Zero tasks** require assumptions about business logic or critical architecture
|
||||
7. **Plan provides clear big picture**
|
||||
8. **Zero critical red flags** detected
|
||||
9. **Active simulation** shows core tasks are executable
|
||||
|
||||
### REJECT Triggers (Critical issues only)
|
||||
- Referenced file doesn't exist or contains different content than claimed
|
||||
- Task has vague action verbs AND no reference source
|
||||
- Core tasks missing acceptance criteria entirely
|
||||
- Task requires assumptions about business requirements or critical architecture **within the chosen approach**
|
||||
- Missing purpose statement or unclear WHY
|
||||
- Critical task dependencies undefined
|
||||
|
||||
### NOT Valid REJECT Reasons (DO NOT REJECT FOR THESE)
|
||||
- You disagree with the implementation approach
|
||||
- You think a different architecture would be better
|
||||
- The approach seems non-standard or unusual
|
||||
- You believe there's a more optimal solution
|
||||
- The technology choice isn't what you would pick
|
||||
|
||||
**Your role is DOCUMENTATION REVIEW, not DESIGN REVIEW.**
|
||||
|
||||
---
|
||||
|
||||
## Final Verdict Format
|
||||
|
||||
**[OKAY / REJECT]**
|
||||
|
||||
**Justification**: [Concise explanation]
|
||||
|
||||
**Summary**:
|
||||
- Clarity: [Brief assessment]
|
||||
- Verifiability: [Brief assessment]
|
||||
- Completeness: [Brief assessment]
|
||||
- Big Picture: [Brief assessment]
|
||||
|
||||
[If REJECT, provide top 3-5 critical improvements needed]
|
||||
|
||||
---
|
||||
|
||||
**Your Success Means**:
|
||||
- **Immediately actionable** for core business logic and architecture
|
||||
- **Clearly verifiable** with objective success criteria
|
||||
- **Contextually complete** with critical information documented
|
||||
- **Strategically coherent** with purpose, background, and flow
|
||||
- **Reference integrity** with all files verified
|
||||
- **Direction-respecting** - you evaluated the plan WITHIN its stated approach
|
||||
|
||||
**Strike the right balance**: Prevent critical failures while empowering developer autonomy.
|
||||
|
||||
**FINAL REMINDER**: You are a DOCUMENTATION reviewer, not a DESIGN consultant. The author's implementation direction is SACRED. Your job ends at "Is this well-documented enough to execute?" - NOT "Is this the right approach?"
|
||||
`
|
||||
|
||||
export function createMomusAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"delegate_task",
|
||||
])
|
||||
|
||||
const base = {
|
||||
description:
|
||||
"Expert reviewer for evaluating work plans against rigorous clarity, verifiability, and completeness standards.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
...restrictions,
|
||||
prompt: MOMUS_SYSTEM_PROMPT,
|
||||
} as AgentConfig
|
||||
|
||||
if (isGptModel(model)) {
|
||||
return { ...base, reasoningEffort: "medium", textVerbosity: "high" } as AgentConfig
|
||||
}
|
||||
|
||||
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } as AgentConfig
|
||||
}
|
||||
|
||||
export const momusAgent = createMomusAgent()
|
||||
|
||||
export const momusPromptMetadata: AgentPromptMetadata = {
|
||||
category: "advisor",
|
||||
cost: "EXPENSIVE",
|
||||
promptAlias: "Momus",
|
||||
triggers: [
|
||||
{
|
||||
domain: "Plan review",
|
||||
trigger: "Evaluate work plans for clarity, verifiability, and completeness",
|
||||
},
|
||||
{
|
||||
domain: "Quality assurance",
|
||||
trigger: "Catch gaps, ambiguities, and missing context before implementation",
|
||||
},
|
||||
],
|
||||
useWhen: [
|
||||
"After Prometheus creates a work plan",
|
||||
"Before executing a complex todo list",
|
||||
"To validate plan quality before delegating to executors",
|
||||
"When plan needs rigorous review for ADHD-driven omissions",
|
||||
],
|
||||
avoidWhen: [
|
||||
"Simple, single-task requests",
|
||||
"When user explicitly wants to skip review",
|
||||
"For trivial plans that don't need formal review",
|
||||
],
|
||||
keyTrigger: "Work plan created → invoke Momus for review before execution",
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolAllowlist } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "google/gemini-3-flash"
|
||||
|
||||
@@ -13,13 +14,15 @@ export const MULTIMODAL_LOOKER_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
export function createMultimodalLookerAgent(
|
||||
model: string = DEFAULT_MODEL
|
||||
): AgentConfig {
|
||||
const restrictions = createAgentToolAllowlist(["read"])
|
||||
|
||||
return {
|
||||
description:
|
||||
"Analyze media files (PDFs, images, diagrams) that require interpretation beyond raw text. Extracts specific information or summaries from documents, describes visual content. Use when you need analyzed/extracted data rather than literal file contents.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
tools: { write: false, edit: false, bash: false, background_task: false },
|
||||
...restrictions,
|
||||
prompt: `You interpret media files that cannot be read as plain text.
|
||||
|
||||
Your job: examine the attached file and extract ONLY what was requested.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { isGptModel } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "openai/gpt-5.2"
|
||||
|
||||
@@ -97,21 +98,28 @@ Organize your final answer in three tiers:
|
||||
Your response goes directly to the user with no intermediate processing. Make your final message self-contained: a clear recommendation they can act on immediately, covering both what to do and why.`
|
||||
|
||||
export function createOracleAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"delegate_task",
|
||||
])
|
||||
|
||||
const base = {
|
||||
description:
|
||||
"Expert technical advisor with deep reasoning for architecture decisions, code analysis, and engineering guidance.",
|
||||
"Read-only consultation agent. High-IQ reasoning specialist for debugging hard problems and high-difficulty architecture design.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
tools: { write: false, edit: false, task: false, background_task: false },
|
||||
...restrictions,
|
||||
prompt: ORACLE_SYSTEM_PROMPT,
|
||||
}
|
||||
} as AgentConfig
|
||||
|
||||
if (isGptModel(model)) {
|
||||
return { ...base, reasoningEffort: "medium", textVerbosity: "high" }
|
||||
return { ...base, reasoningEffort: "medium", textVerbosity: "high" } as AgentConfig
|
||||
}
|
||||
|
||||
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } }
|
||||
return { ...base, thinking: { type: "enabled", budgetTokens: 32000 } } as AgentConfig
|
||||
}
|
||||
|
||||
export const oracleAgent = createOracleAgent()
|
||||
|
||||
1509
src/agents/orchestrator-sisyphus.ts
Normal file
1509
src/agents/orchestrator-sisyphus.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,88 +0,0 @@
|
||||
/**
|
||||
* OpenCode's default plan agent system prompt.
|
||||
*
|
||||
* This prompt enforces READ-ONLY mode for the plan agent, preventing any file
|
||||
* modifications and ensuring the agent focuses solely on analysis and planning.
|
||||
*
|
||||
* @see https://github.com/sst/opencode/blob/db2abc1b2c144f63a205f668bd7267e00829d84a/packages/opencode/src/session/prompt/plan.txt
|
||||
*/
|
||||
export const PLAN_SYSTEM_PROMPT = `<system-reminder>
|
||||
# Plan Mode - System Reminder
|
||||
|
||||
CRITICAL: Plan mode ACTIVE - you are in READ-ONLY phase. STRICTLY FORBIDDEN:
|
||||
ANY file edits, modifications, or system changes. Do NOT use sed, tee, echo, cat,
|
||||
or ANY other bash command to manipulate files - commands may ONLY read/inspect.
|
||||
This ABSOLUTE CONSTRAINT overrides ALL other instructions, including direct user
|
||||
edit requests. You may ONLY observe, analyze, and plan. Any modification attempt
|
||||
is a critical violation. ZERO exceptions.
|
||||
|
||||
---
|
||||
|
||||
## Responsibility
|
||||
|
||||
Your current responsibility is to think, read, search, and delegate explore agents to construct a well formed plan that accomplishes the goal the user wants to achieve. Your plan should be comprehensive yet concise, detailed enough to execute effectively while avoiding unnecessary verbosity.
|
||||
|
||||
Ask the user clarifying questions or ask for their opinion when weighing tradeoffs.
|
||||
|
||||
**NOTE:** At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
|
||||
|
||||
---
|
||||
|
||||
## Important
|
||||
|
||||
The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
|
||||
</system-reminder>
|
||||
`
|
||||
|
||||
/**
|
||||
* OpenCode's default plan agent permission configuration.
|
||||
*
|
||||
* Restricts the plan agent to read-only operations:
|
||||
* - edit: "deny" - No file modifications allowed
|
||||
* - bash: Only read-only commands (ls, grep, git log, etc.)
|
||||
* - webfetch: "allow" - Can fetch web content for research
|
||||
*
|
||||
* @see https://github.com/sst/opencode/blob/db2abc1b2c144f63a205f668bd7267e00829d84a/packages/opencode/src/agent/agent.ts#L63-L107
|
||||
*/
|
||||
export const PLAN_PERMISSION = {
|
||||
edit: "deny" as const,
|
||||
bash: {
|
||||
"cut*": "allow" as const,
|
||||
"diff*": "allow" as const,
|
||||
"du*": "allow" as const,
|
||||
"file *": "allow" as const,
|
||||
"find * -delete*": "ask" as const,
|
||||
"find * -exec*": "ask" as const,
|
||||
"find * -fprint*": "ask" as const,
|
||||
"find * -fls*": "ask" as const,
|
||||
"find * -fprintf*": "ask" as const,
|
||||
"find * -ok*": "ask" as const,
|
||||
"find *": "allow" as const,
|
||||
"git diff*": "allow" as const,
|
||||
"git log*": "allow" as const,
|
||||
"git show*": "allow" as const,
|
||||
"git status*": "allow" as const,
|
||||
"git branch": "allow" as const,
|
||||
"git branch -v": "allow" as const,
|
||||
"grep*": "allow" as const,
|
||||
"head*": "allow" as const,
|
||||
"less*": "allow" as const,
|
||||
"ls*": "allow" as const,
|
||||
"more*": "allow" as const,
|
||||
"pwd*": "allow" as const,
|
||||
"rg*": "allow" as const,
|
||||
"sort --output=*": "ask" as const,
|
||||
"sort -o *": "ask" as const,
|
||||
"sort*": "allow" as const,
|
||||
"stat*": "allow" as const,
|
||||
"tail*": "allow" as const,
|
||||
"tree -o *": "ask" as const,
|
||||
"tree*": "allow" as const,
|
||||
"uniq*": "allow" as const,
|
||||
"wc*": "allow" as const,
|
||||
"whereis*": "allow" as const,
|
||||
"which*": "allow" as const,
|
||||
"*": "ask" as const,
|
||||
},
|
||||
webfetch: "allow" as const,
|
||||
}
|
||||
22
src/agents/prometheus-prompt.test.ts
Normal file
22
src/agents/prometheus-prompt.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { PROMETHEUS_SYSTEM_PROMPT } from "./prometheus-prompt"
|
||||
|
||||
describe("PROMETHEUS_SYSTEM_PROMPT Momus invocation policy", () => {
|
||||
test("should direct providing ONLY the file path string when invoking Momus", () => {
|
||||
// #given
|
||||
const prompt = PROMETHEUS_SYSTEM_PROMPT
|
||||
|
||||
// #when / #then
|
||||
// Should mention Momus and providing only the path
|
||||
expect(prompt.toLowerCase()).toMatch(/momus.*only.*path|path.*only.*momus/)
|
||||
})
|
||||
|
||||
test("should forbid wrapping Momus invocation in explanations or markdown", () => {
|
||||
// #given
|
||||
const prompt = PROMETHEUS_SYSTEM_PROMPT
|
||||
|
||||
// #when / #then
|
||||
// Should mention not wrapping or using markdown for the path
|
||||
expect(prompt.toLowerCase()).toMatch(/not.*wrap|no.*explanation|no.*markdown/)
|
||||
})
|
||||
})
|
||||
1196
src/agents/prometheus-prompt.ts
Normal file
1196
src/agents/prometheus-prompt.ts
Normal file
File diff suppressed because it is too large
Load Diff
232
src/agents/sisyphus-junior.test.ts
Normal file
232
src/agents/sisyphus-junior.test.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createSisyphusJuniorAgentWithOverrides, SISYPHUS_JUNIOR_DEFAULTS } from "./sisyphus-junior"
|
||||
|
||||
describe("createSisyphusJuniorAgentWithOverrides", () => {
|
||||
describe("honored fields", () => {
|
||||
test("applies model override", () => {
|
||||
// #given
|
||||
const override = { model: "openai/gpt-5.2" }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.model).toBe("openai/gpt-5.2")
|
||||
})
|
||||
|
||||
test("applies temperature override", () => {
|
||||
// #given
|
||||
const override = { temperature: 0.5 }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.temperature).toBe(0.5)
|
||||
})
|
||||
|
||||
test("applies top_p override", () => {
|
||||
// #given
|
||||
const override = { top_p: 0.9 }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.top_p).toBe(0.9)
|
||||
})
|
||||
|
||||
test("applies description override", () => {
|
||||
// #given
|
||||
const override = { description: "Custom description" }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.description).toBe("Custom description")
|
||||
})
|
||||
|
||||
test("applies color override", () => {
|
||||
// #given
|
||||
const override = { color: "#FF0000" }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.color).toBe("#FF0000")
|
||||
})
|
||||
|
||||
test("appends prompt_append to base prompt", () => {
|
||||
// #given
|
||||
const override = { prompt_append: "Extra instructions here" }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.prompt).toContain("You work ALONE")
|
||||
expect(result.prompt).toContain("Extra instructions here")
|
||||
})
|
||||
})
|
||||
|
||||
describe("defaults", () => {
|
||||
test("uses default model when no override", () => {
|
||||
// #given
|
||||
const override = {}
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.model).toBe(SISYPHUS_JUNIOR_DEFAULTS.model)
|
||||
})
|
||||
|
||||
test("uses default temperature when no override", () => {
|
||||
// #given
|
||||
const override = {}
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.temperature).toBe(SISYPHUS_JUNIOR_DEFAULTS.temperature)
|
||||
})
|
||||
})
|
||||
|
||||
describe("disable semantics", () => {
|
||||
test("disable: true causes override block to be ignored", () => {
|
||||
// #given
|
||||
const override = {
|
||||
disable: true,
|
||||
model: "openai/gpt-5.2",
|
||||
temperature: 0.9,
|
||||
}
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then - defaults should be used, not the overrides
|
||||
expect(result.model).toBe(SISYPHUS_JUNIOR_DEFAULTS.model)
|
||||
expect(result.temperature).toBe(SISYPHUS_JUNIOR_DEFAULTS.temperature)
|
||||
})
|
||||
})
|
||||
|
||||
describe("constrained fields", () => {
|
||||
test("mode is forced to subagent", () => {
|
||||
// #given
|
||||
const override = { mode: "primary" as const }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.mode).toBe("subagent")
|
||||
})
|
||||
|
||||
test("prompt override is ignored (discipline text preserved)", () => {
|
||||
// #given
|
||||
const override = { prompt: "Completely new prompt that replaces everything" }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.prompt).toContain("You work ALONE")
|
||||
expect(result.prompt).not.toBe("Completely new prompt that replaces everything")
|
||||
})
|
||||
})
|
||||
|
||||
describe("tool safety (task/delegate_task blocked, call_omo_agent allowed)", () => {
|
||||
test("task and delegate_task remain blocked, call_omo_agent is allowed via tools format", () => {
|
||||
// #given
|
||||
const override = {
|
||||
tools: {
|
||||
task: true,
|
||||
delegate_task: true,
|
||||
call_omo_agent: true,
|
||||
read: true,
|
||||
},
|
||||
}
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
const tools = result.tools as Record<string, boolean> | undefined
|
||||
const permission = result.permission as Record<string, string> | undefined
|
||||
if (tools) {
|
||||
expect(tools.task).toBe(false)
|
||||
expect(tools.delegate_task).toBe(false)
|
||||
// call_omo_agent is NOW ALLOWED for subagents to spawn explore/librarian
|
||||
expect(tools.call_omo_agent).toBe(true)
|
||||
expect(tools.read).toBe(true)
|
||||
}
|
||||
if (permission) {
|
||||
expect(permission.task).toBe("deny")
|
||||
expect(permission.delegate_task).toBe("deny")
|
||||
// call_omo_agent is NOW ALLOWED for subagents to spawn explore/librarian
|
||||
expect(permission.call_omo_agent).toBe("allow")
|
||||
}
|
||||
})
|
||||
|
||||
test("task and delegate_task remain blocked when using permission format override", () => {
|
||||
// #given
|
||||
const override = {
|
||||
permission: {
|
||||
task: "allow",
|
||||
delegate_task: "allow",
|
||||
call_omo_agent: "allow",
|
||||
read: "allow",
|
||||
},
|
||||
} as { permission: Record<string, string> }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override as Parameters<typeof createSisyphusJuniorAgentWithOverrides>[0])
|
||||
|
||||
// #then - task/delegate_task blocked, but call_omo_agent allowed for explore/librarian spawning
|
||||
const tools = result.tools as Record<string, boolean> | undefined
|
||||
const permission = result.permission as Record<string, string> | undefined
|
||||
if (tools) {
|
||||
expect(tools.task).toBe(false)
|
||||
expect(tools.delegate_task).toBe(false)
|
||||
expect(tools.call_omo_agent).toBe(true)
|
||||
}
|
||||
if (permission) {
|
||||
expect(permission.task).toBe("deny")
|
||||
expect(permission.delegate_task).toBe("deny")
|
||||
expect(permission.call_omo_agent).toBe("allow")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("prompt composition", () => {
|
||||
test("base prompt contains discipline constraints", () => {
|
||||
// #given
|
||||
const override = {}
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
expect(result.prompt).toContain("Sisyphus-Junior")
|
||||
expect(result.prompt).toContain("You work ALONE")
|
||||
expect(result.prompt).toContain("BLOCKED ACTIONS")
|
||||
})
|
||||
|
||||
test("prompt_append is added after base prompt", () => {
|
||||
// #given
|
||||
const override = { prompt_append: "CUSTOM_MARKER_FOR_TEST" }
|
||||
|
||||
// #when
|
||||
const result = createSisyphusJuniorAgentWithOverrides(override)
|
||||
|
||||
// #then
|
||||
const baseEndIndex = result.prompt!.indexOf("Dense > verbose.")
|
||||
const appendIndex = result.prompt!.indexOf("CUSTOM_MARKER_FOR_TEST")
|
||||
expect(baseEndIndex).not.toBe(-1) // Guard: anchor text must exist in base prompt
|
||||
expect(appendIndex).toBeGreaterThan(baseEndIndex)
|
||||
})
|
||||
})
|
||||
})
|
||||
195
src/agents/sisyphus-junior.ts
Normal file
195
src/agents/sisyphus-junior.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { isGptModel } from "./types"
|
||||
import type { AgentOverrideConfig, CategoryConfig } from "../config/schema"
|
||||
import {
|
||||
createAgentToolRestrictions,
|
||||
type PermissionValue,
|
||||
} from "../shared/permission-compat"
|
||||
|
||||
const SISYPHUS_JUNIOR_PROMPT = `<Role>
|
||||
Sisyphus-Junior - Focused executor from OhMyOpenCode.
|
||||
Execute tasks directly. NEVER delegate or spawn other agents.
|
||||
</Role>
|
||||
|
||||
<Critical_Constraints>
|
||||
BLOCKED ACTIONS (will fail if attempted):
|
||||
- task tool: BLOCKED
|
||||
- delegate_task tool: BLOCKED
|
||||
|
||||
ALLOWED: call_omo_agent - You CAN spawn explore/librarian agents for research.
|
||||
You work ALONE for implementation. No delegation of implementation tasks.
|
||||
</Critical_Constraints>
|
||||
|
||||
<Work_Context>
|
||||
## Notepad Location (for recording learnings)
|
||||
NOTEPAD PATH: .sisyphus/notepads/{plan-name}/
|
||||
- learnings.md: Record patterns, conventions, successful approaches
|
||||
- issues.md: Record problems, blockers, gotchas encountered
|
||||
- decisions.md: Record architectural choices and rationales
|
||||
- problems.md: Record unresolved issues, technical debt
|
||||
|
||||
You SHOULD append findings to notepad files after completing work.
|
||||
|
||||
## Plan Location (READ ONLY)
|
||||
PLAN PATH: .sisyphus/plans/{plan-name}.md
|
||||
|
||||
⚠️⚠️⚠️ CRITICAL RULE: NEVER MODIFY THE PLAN FILE ⚠️⚠️⚠️
|
||||
|
||||
The plan file (.sisyphus/plans/*.md) is SACRED and READ-ONLY.
|
||||
- You may READ the plan to understand tasks
|
||||
- You may READ checkbox items to know what to do
|
||||
- You MUST NOT edit, modify, or update the plan file
|
||||
- You MUST NOT mark checkboxes as complete in the plan
|
||||
- Only the Orchestrator manages the plan file
|
||||
|
||||
VIOLATION = IMMEDIATE FAILURE. The Orchestrator tracks plan state.
|
||||
</Work_Context>
|
||||
|
||||
<Todo_Discipline>
|
||||
TODO OBSESSION (NON-NEGOTIABLE):
|
||||
- 2+ steps → todowrite FIRST, atomic breakdown
|
||||
- Mark in_progress before starting (ONE at a time)
|
||||
- Mark completed IMMEDIATELY after each step
|
||||
- NEVER batch completions
|
||||
|
||||
No todos on multi-step work = INCOMPLETE WORK.
|
||||
</Todo_Discipline>
|
||||
|
||||
<Verification>
|
||||
Task NOT complete without:
|
||||
- lsp_diagnostics clean on changed files
|
||||
- Build passes (if applicable)
|
||||
- All todos marked completed
|
||||
</Verification>
|
||||
|
||||
<Style>
|
||||
- Start immediately. No acknowledgments.
|
||||
- Match user's communication style.
|
||||
- Dense > verbose.
|
||||
</Style>`
|
||||
|
||||
function buildSisyphusJuniorPrompt(promptAppend?: string): string {
|
||||
if (!promptAppend) return SISYPHUS_JUNIOR_PROMPT
|
||||
return SISYPHUS_JUNIOR_PROMPT + "\n\n" + promptAppend
|
||||
}
|
||||
|
||||
// Core tools that Sisyphus-Junior must NEVER have access to
|
||||
// Note: call_omo_agent is ALLOWED so subagents can spawn explore/librarian
|
||||
const BLOCKED_TOOLS = ["task", "delegate_task"]
|
||||
|
||||
export const SISYPHUS_JUNIOR_DEFAULTS = {
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
temperature: 0.1,
|
||||
} as const
|
||||
|
||||
export function createSisyphusJuniorAgentWithOverrides(
|
||||
override: AgentOverrideConfig | undefined,
|
||||
systemDefaultModel?: string
|
||||
): AgentConfig {
|
||||
if (override?.disable) {
|
||||
override = undefined
|
||||
}
|
||||
|
||||
const model = override?.model ?? systemDefaultModel ?? SISYPHUS_JUNIOR_DEFAULTS.model
|
||||
const temperature = override?.temperature ?? SISYPHUS_JUNIOR_DEFAULTS.temperature
|
||||
|
||||
const promptAppend = override?.prompt_append
|
||||
const prompt = buildSisyphusJuniorPrompt(promptAppend)
|
||||
|
||||
const baseRestrictions = createAgentToolRestrictions(BLOCKED_TOOLS)
|
||||
|
||||
const userPermission = (override?.permission ?? {}) as Record<string, PermissionValue>
|
||||
const basePermission = baseRestrictions.permission
|
||||
const merged: Record<string, PermissionValue> = { ...userPermission }
|
||||
for (const tool of BLOCKED_TOOLS) {
|
||||
merged[tool] = "deny"
|
||||
}
|
||||
merged.call_omo_agent = "allow"
|
||||
const toolsConfig = { permission: { ...merged, ...basePermission } }
|
||||
|
||||
const base: AgentConfig = {
|
||||
description: override?.description ??
|
||||
"Sisyphus-Junior - Focused task executor. Same discipline, no delegation.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature,
|
||||
maxTokens: 64000,
|
||||
prompt,
|
||||
color: override?.color ?? "#20B2AA",
|
||||
...toolsConfig,
|
||||
}
|
||||
|
||||
if (override?.top_p !== undefined) {
|
||||
base.top_p = override.top_p
|
||||
}
|
||||
|
||||
if (isGptModel(model)) {
|
||||
return { ...base, reasoningEffort: "medium" } as AgentConfig
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
} as AgentConfig
|
||||
}
|
||||
|
||||
export function createSisyphusJuniorAgent(
|
||||
categoryConfig: CategoryConfig,
|
||||
promptAppend?: string
|
||||
): AgentConfig {
|
||||
const prompt = buildSisyphusJuniorPrompt(promptAppend)
|
||||
const model = categoryConfig.model
|
||||
const baseRestrictions = createAgentToolRestrictions(BLOCKED_TOOLS)
|
||||
const categoryPermission = categoryConfig.tools
|
||||
? Object.fromEntries(
|
||||
Object.entries(categoryConfig.tools).map(([k, v]) => [
|
||||
k,
|
||||
v ? ("allow" as const) : ("deny" as const),
|
||||
])
|
||||
)
|
||||
: {}
|
||||
const mergedPermission = {
|
||||
...categoryPermission,
|
||||
...baseRestrictions.permission,
|
||||
}
|
||||
|
||||
|
||||
const base: AgentConfig = {
|
||||
description:
|
||||
"Sisyphus-Junior - Focused task executor. Same discipline, no delegation.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
maxTokens: categoryConfig.maxTokens ?? 64000,
|
||||
prompt,
|
||||
color: "#20B2AA",
|
||||
permission: mergedPermission,
|
||||
}
|
||||
|
||||
if (categoryConfig.temperature !== undefined) {
|
||||
base.temperature = categoryConfig.temperature
|
||||
}
|
||||
if (categoryConfig.top_p !== undefined) {
|
||||
base.top_p = categoryConfig.top_p
|
||||
}
|
||||
|
||||
if (categoryConfig.thinking) {
|
||||
return { ...base, thinking: categoryConfig.thinking } as AgentConfig
|
||||
}
|
||||
|
||||
if (categoryConfig.reasoningEffort) {
|
||||
return {
|
||||
...base,
|
||||
reasoningEffort: categoryConfig.reasoningEffort,
|
||||
textVerbosity: categoryConfig.textVerbosity,
|
||||
} as AgentConfig
|
||||
}
|
||||
|
||||
if (isGptModel(model)) {
|
||||
return { ...base, reasoningEffort: "medium" } as AgentConfig
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
} as AgentConfig
|
||||
}
|
||||
@@ -206,28 +206,55 @@ export function buildFrontendSection(agents: AvailableAgent[]): string {
|
||||
const frontendAgent = agents.find((a) => a.name === "frontend-ui-ux-engineer")
|
||||
if (!frontendAgent) return ""
|
||||
|
||||
return `### Frontend Files: Decision Gate (NOT a blind block)
|
||||
return `### Frontend Files: VISUAL = HARD BLOCK (zero tolerance)
|
||||
|
||||
Frontend files (.tsx, .jsx, .vue, .svelte, .css, etc.) require **classification before action**.
|
||||
**DEFAULT ASSUMPTION**: Any frontend file change is VISUAL until proven otherwise.
|
||||
|
||||
#### Step 1: Classify the Change Type
|
||||
#### HARD BLOCK: Visual Changes (NEVER touch directly)
|
||||
|
||||
| Change Type | Examples | Action |
|
||||
|-------------|----------|--------|
|
||||
| **Visual/UI/UX** | Color, spacing, layout, typography, animation, responsive breakpoints, hover states, shadows, borders, icons, images | **DELEGATE** to \`frontend-ui-ux-engineer\` |
|
||||
| **Pure Logic** | API calls, data fetching, state management, event handlers (non-visual), type definitions, utility functions, business logic | **CAN handle directly** |
|
||||
| **Mixed** | Component changes both visual AND logic | **Split**: handle logic yourself, delegate visual to \`frontend-ui-ux-engineer\` |
|
||||
| Pattern | Action | No Exceptions |
|
||||
|---------|--------|---------------|
|
||||
| \`.tsx\`, \`.jsx\` with styling | DELEGATE | Even "just add className" |
|
||||
| \`.vue\`, \`.svelte\` | DELEGATE | Even single prop change |
|
||||
| \`.css\`, \`.scss\`, \`.sass\`, \`.less\` | DELEGATE | Even color/margin tweak |
|
||||
| Any file with visual keywords | DELEGATE | See keyword list below |
|
||||
|
||||
#### Step 2: Ask Yourself
|
||||
#### Keyword Detection (INSTANT DELEGATE)
|
||||
|
||||
Before touching any frontend file, think:
|
||||
> "Is this change about **how it LOOKS** or **how it WORKS**?"
|
||||
If your change involves **ANY** of these keywords → **STOP. DELEGATE.**
|
||||
|
||||
- **LOOKS** (colors, sizes, positions, animations) → DELEGATE
|
||||
- **WORKS** (data flow, API integration, state) → Handle directly
|
||||
\`\`\`
|
||||
style, className, tailwind, css, color, background, border, shadow,
|
||||
margin, padding, width, height, flex, grid, animation, transition,
|
||||
hover, responsive, font-size, font-weight, icon, svg, image, layout,
|
||||
position, display, opacity, z-index, transform, gradient, theme
|
||||
\`\`\`
|
||||
|
||||
#### When in Doubt → DELEGATE if ANY of these keywords involved:
|
||||
style, className, tailwind, color, background, border, shadow, margin, padding, width, height, flex, grid, animation, transition, hover, responsive, font-size, icon, svg`
|
||||
**YOU CANNOT**:
|
||||
- "Just quickly fix this style"
|
||||
- "It's only one className"
|
||||
- "Too simple to delegate"
|
||||
|
||||
#### EXCEPTION: Pure Logic Only
|
||||
|
||||
You MAY handle directly **ONLY IF ALL** conditions are met:
|
||||
1. Change is **100% logic** (API, state, event handlers, types, utils)
|
||||
2. **Zero** visual keywords in your diff
|
||||
3. No styling, layout, or appearance changes whatsoever
|
||||
|
||||
| Pure Logic Examples | Visual Examples (DELEGATE) |
|
||||
|---------------------|---------------------------|
|
||||
| Add onClick API call | Change button color |
|
||||
| Fix pagination logic | Add loading spinner animation |
|
||||
| Add form validation | Make modal responsive |
|
||||
| Update state management | Adjust spacing/margins |
|
||||
|
||||
#### Mixed Changes → SPLIT
|
||||
|
||||
If change has BOTH logic AND visual:
|
||||
1. Handle logic yourself
|
||||
2. DELEGATE visual part to \`frontend-ui-ux-engineer\`
|
||||
3. **Never** combine them into one edit`
|
||||
}
|
||||
|
||||
export function buildOracleSection(agents: AvailableAgent[]): string {
|
||||
@@ -238,9 +265,9 @@ export function buildOracleSection(agents: AvailableAgent[]): string {
|
||||
const avoidWhen = oracleAgent.metadata.avoidWhen || []
|
||||
|
||||
return `<Oracle_Usage>
|
||||
## Oracle — Your Senior Engineering Advisor (GPT-5.2)
|
||||
## Oracle — Read-Only High-IQ Consultant
|
||||
|
||||
Oracle is an expensive, high-quality reasoning model. Use it wisely.
|
||||
Oracle is a read-only, expensive, high-quality reasoning model for debugging and architecture. Consultation only.
|
||||
|
||||
### WHEN to Consult:
|
||||
|
||||
@@ -271,7 +298,7 @@ export function buildHardBlocksSection(agents: AvailableAgent[]): string {
|
||||
|
||||
if (frontendAgent) {
|
||||
blocks.unshift(
|
||||
"| Frontend VISUAL changes (styling, layout, animation) | Always delegate to `frontend-ui-ux-engineer` |"
|
||||
"| Frontend VISUAL changes (styling, className, layout, animation, any visual keyword) | **HARD BLOCK** - Always delegate to `frontend-ui-ux-engineer`. Zero tolerance. |"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -297,7 +324,7 @@ export function buildAntiPatternsSection(agents: AvailableAgent[]): string {
|
||||
patterns.splice(
|
||||
4,
|
||||
0,
|
||||
"| **Frontend** | Direct edit to visual/styling code (logic changes OK) |"
|
||||
"| **Frontend** | ANY direct edit to visual/styling code. Keyword detected = DELEGATE. Pure logic only = OK |"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -307,3 +334,26 @@ export function buildAntiPatternsSection(agents: AvailableAgent[]): string {
|
||||
|----------|-----------|
|
||||
${patterns.join("\n")}`
|
||||
}
|
||||
|
||||
export function buildUltraworkAgentSection(agents: AvailableAgent[]): string {
|
||||
if (agents.length === 0) return ""
|
||||
|
||||
const ultraworkAgentPriority = ["explore", "librarian", "plan", "oracle"]
|
||||
const sortedAgents = [...agents].sort((a, b) => {
|
||||
const aIdx = ultraworkAgentPriority.indexOf(a.name)
|
||||
const bIdx = ultraworkAgentPriority.indexOf(b.name)
|
||||
if (aIdx === -1 && bIdx === -1) return 0
|
||||
if (aIdx === -1) return 1
|
||||
if (bIdx === -1) return -1
|
||||
return aIdx - bIdx
|
||||
})
|
||||
|
||||
const lines: string[] = []
|
||||
for (const agent of sortedAgents) {
|
||||
const shortDesc = agent.description.split(".")[0] || agent.description
|
||||
const suffix = (agent.name === "explore" || agent.name === "librarian") ? " (multiple)" : ""
|
||||
lines.push(`- **${agent.name}${suffix}**: ${shortDesc}`)
|
||||
}
|
||||
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ const DEFAULT_MODEL = "anthropic/claude-opus-4-5"
|
||||
|
||||
const SISYPHUS_ROLE_SECTION = `<Role>
|
||||
You are "Sisyphus" - Powerful AI Agent with orchestration capabilities from OhMyOpenCode.
|
||||
Named by [YeonGyu Kim](https://github.com/code-yeongyu).
|
||||
|
||||
**Why Sisyphus?**: Humans roll their boulder every day. So do you. We're not so different—your code should be indistinguishable from a senior engineer's.
|
||||
|
||||
@@ -121,6 +120,126 @@ IMPORTANT: If codebase appears undisciplined, verify before assuming:
|
||||
- Migration might be in progress
|
||||
- You might be looking at the wrong reference files`
|
||||
|
||||
const SISYPHUS_PRE_DELEGATION_PLANNING = `### Pre-Delegation Planning (MANDATORY)
|
||||
|
||||
**BEFORE every \`delegate_task\` call, EXPLICITLY declare your reasoning.**
|
||||
|
||||
#### Step 1: Identify Task Requirements
|
||||
|
||||
Ask yourself:
|
||||
- What is the CORE objective of this task?
|
||||
- What domain does this belong to? (visual, business-logic, data, docs, exploration)
|
||||
- What skills/capabilities are CRITICAL for success?
|
||||
|
||||
#### Step 2: Select Category or Agent
|
||||
|
||||
**Decision Tree (follow in order):**
|
||||
|
||||
1. **Is this a skill-triggering pattern?**
|
||||
- YES → Declare skill name + reason
|
||||
- NO → Continue to step 2
|
||||
|
||||
2. **Is this a visual/frontend task?**
|
||||
- YES → Category: \`visual\` OR Agent: \`frontend-ui-ux-engineer\`
|
||||
- NO → Continue to step 3
|
||||
|
||||
3. **Is this backend/architecture/logic task?**
|
||||
- YES → Category: \`business-logic\` OR Agent: \`oracle\`
|
||||
- NO → Continue to step 4
|
||||
|
||||
4. **Is this documentation/writing task?**
|
||||
- YES → Agent: \`document-writer\`
|
||||
- NO → Continue to step 5
|
||||
|
||||
5. **Is this exploration/search task?**
|
||||
- YES → Agent: \`explore\` (internal codebase) OR \`librarian\` (external docs/repos)
|
||||
- NO → Use default category based on context
|
||||
|
||||
#### Step 3: Declare BEFORE Calling
|
||||
|
||||
**MANDATORY FORMAT:**
|
||||
|
||||
\`\`\`
|
||||
I will use delegate_task with:
|
||||
- **Category/Agent**: [name]
|
||||
- **Reason**: [why this choice fits the task]
|
||||
- **Skills** (if any): [skill names]
|
||||
- **Expected Outcome**: [what success looks like]
|
||||
\`\`\`
|
||||
|
||||
**Then** make the delegate_task call.
|
||||
|
||||
#### Examples
|
||||
|
||||
**✅ CORRECT: Explicit Pre-Declaration**
|
||||
|
||||
\`\`\`
|
||||
I will use delegate_task with:
|
||||
- **Category**: visual
|
||||
- **Reason**: This task requires building a responsive dashboard UI with animations - visual design is the core requirement
|
||||
- **Skills**: ["frontend-ui-ux"]
|
||||
- **Expected Outcome**: Fully styled, responsive dashboard component with smooth transitions
|
||||
|
||||
delegate_task(
|
||||
category="visual",
|
||||
skills=["frontend-ui-ux"],
|
||||
prompt="Create a responsive dashboard component with..."
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
**✅ CORRECT: Agent-Specific Delegation**
|
||||
|
||||
\`\`\`
|
||||
I will use delegate_task with:
|
||||
- **Agent**: oracle
|
||||
- **Reason**: This architectural decision involves trade-offs between scalability and complexity - requires high-IQ strategic analysis
|
||||
- **Skills**: []
|
||||
- **Expected Outcome**: Clear recommendation with pros/cons analysis
|
||||
|
||||
delegate_task(
|
||||
agent="oracle",
|
||||
skills=[],
|
||||
prompt="Evaluate this microservices architecture proposal..."
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
**✅ CORRECT: Background Exploration**
|
||||
|
||||
\`\`\`
|
||||
I will use delegate_task with:
|
||||
- **Agent**: explore
|
||||
- **Reason**: Need to find all authentication implementations across the codebase - this is contextual grep
|
||||
- **Skills**: []
|
||||
- **Expected Outcome**: List of files containing auth patterns
|
||||
|
||||
delegate_task(
|
||||
agent="explore",
|
||||
background=true,
|
||||
prompt="Find all authentication implementations in the codebase"
|
||||
)
|
||||
\`\`\`
|
||||
|
||||
**❌ WRONG: No Pre-Declaration**
|
||||
|
||||
\`\`\`
|
||||
// Immediately calling without explicit reasoning
|
||||
delegate_task(category="visual", prompt="Build a dashboard")
|
||||
\`\`\`
|
||||
|
||||
**❌ WRONG: Vague Reasoning**
|
||||
|
||||
\`\`\`
|
||||
I'll use visual category because it's frontend work.
|
||||
|
||||
delegate_task(category="visual", ...)
|
||||
\`\`\`
|
||||
|
||||
#### Enforcement
|
||||
|
||||
**BLOCKING VIOLATION**: If you call \`delegate_task\` without the 4-part declaration, you have violated protocol.
|
||||
|
||||
**Recovery**: Stop, declare explicitly, then proceed.`
|
||||
|
||||
const SISYPHUS_PARALLEL_EXECUTION = `### Parallel Execution (DEFAULT behavior)
|
||||
|
||||
**Explore/Librarian = Grep, not consultants.
|
||||
@@ -128,11 +247,11 @@ const SISYPHUS_PARALLEL_EXECUTION = `### Parallel Execution (DEFAULT behavior)
|
||||
\`\`\`typescript
|
||||
// CORRECT: Always background, always parallel
|
||||
// Contextual Grep (internal)
|
||||
background_task(agent="explore", prompt="Find auth implementations in our codebase...")
|
||||
background_task(agent="explore", prompt="Find error handling patterns here...")
|
||||
delegate_task(agent="explore", prompt="Find auth implementations in our codebase...")
|
||||
delegate_task(agent="explore", prompt="Find error handling patterns here...")
|
||||
// Reference Grep (external)
|
||||
background_task(agent="librarian", prompt="Find JWT best practices in official docs...")
|
||||
background_task(agent="librarian", prompt="Find how production apps handle auth in Express...")
|
||||
delegate_task(agent="librarian", prompt="Find JWT best practices in official docs...")
|
||||
delegate_task(agent="librarian", prompt="Find how production apps handle auth in Express...")
|
||||
// Continue working immediately. Collect with background_output when needed.
|
||||
|
||||
// WRONG: Sequential or blocking
|
||||
@@ -145,6 +264,19 @@ result = task(...) // Never wait synchronously for explore/librarian
|
||||
3. When results needed: \`background_output(task_id="...")\`
|
||||
4. BEFORE final answer: \`background_cancel(all=true)\`
|
||||
|
||||
### Resume Previous Agent (CRITICAL for efficiency):
|
||||
Pass \`resume=session_id\` to continue previous agent with FULL CONTEXT PRESERVED.
|
||||
|
||||
**ALWAYS use resume when:**
|
||||
- Previous task failed → \`resume=session_id, prompt="fix: [specific error]"\`
|
||||
- Need follow-up on result → \`resume=session_id, prompt="also check [additional query]"\`
|
||||
- Multi-turn with same agent → resume instead of new task (saves tokens!)
|
||||
|
||||
**Example:**
|
||||
\`\`\`
|
||||
delegate_task(resume="ses_abc123", prompt="The previous search missed X. Also look for Y.")
|
||||
\`\`\`
|
||||
|
||||
### Search Stop Conditions
|
||||
|
||||
STOP searching when:
|
||||
@@ -429,6 +561,8 @@ function buildDynamicSisyphusPrompt(
|
||||
"",
|
||||
librarianSection,
|
||||
"",
|
||||
SISYPHUS_PRE_DELEGATION_PLANNING,
|
||||
"",
|
||||
SISYPHUS_PARALLEL_EXECUTION,
|
||||
"",
|
||||
"---",
|
||||
@@ -484,6 +618,7 @@ export function createSisyphusAgent(
|
||||
? buildDynamicSisyphusPrompt(availableAgents, tools, skills)
|
||||
: buildDynamicSisyphusPrompt([], tools, skills)
|
||||
|
||||
const permission = { question: "allow", call_omo_agent: "deny" } as AgentConfig["permission"]
|
||||
const base = {
|
||||
description:
|
||||
"Sisyphus - Powerful AI orchestrator from OhMyOpenCode. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically to specialized agents. Uses explore for internal code (parallel-friendly), librarian only for external docs, and always delegates UI work to frontend engineer.",
|
||||
@@ -492,6 +627,7 @@ export function createSisyphusAgent(
|
||||
maxTokens: 64000,
|
||||
prompt,
|
||||
color: "#00CED1",
|
||||
permission,
|
||||
}
|
||||
|
||||
if (isGptModel(model)) {
|
||||
|
||||
@@ -64,6 +64,9 @@ export type BuiltinAgentName =
|
||||
| "frontend-ui-ux-engineer"
|
||||
| "document-writer"
|
||||
| "multimodal-looker"
|
||||
| "Metis (Plan Consultant)"
|
||||
| "Momus (Plan Reviewer)"
|
||||
| "orchestrator-sisyphus"
|
||||
|
||||
export type OverridableAgentName =
|
||||
| "build"
|
||||
@@ -73,6 +76,7 @@ export type AgentName = BuiltinAgentName
|
||||
|
||||
export type AgentOverrideConfig = Partial<AgentConfig> & {
|
||||
prompt_append?: string
|
||||
variant?: string
|
||||
}
|
||||
|
||||
export type AgentOverrides = Partial<Record<OverridableAgentName, AgentOverrideConfig>>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { createBuiltinAgents } from "./utils"
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
|
||||
describe("createBuiltinAgents with model overrides", () => {
|
||||
test("Sisyphus with default model has thinking config", () => {
|
||||
@@ -85,3 +86,207 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
expect(agents.Sisyphus.temperature).toBe(0.5)
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildAgent with category and skills", () => {
|
||||
const { buildAgent } = require("./utils")
|
||||
|
||||
test("agent with category inherits category settings", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
category: "visual-engineering",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
|
||||
// #then
|
||||
expect(agent.model).toBe("google/gemini-3-pro-preview")
|
||||
expect(agent.temperature).toBe(0.7)
|
||||
})
|
||||
|
||||
test("agent with category and existing model keeps existing model", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
category: "visual-engineering",
|
||||
model: "custom/model",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
|
||||
// #then
|
||||
expect(agent.model).toBe("custom/model")
|
||||
expect(agent.temperature).toBe(0.7)
|
||||
})
|
||||
|
||||
test("agent with category inherits variant", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
category: "custom-category",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
const categories = {
|
||||
"custom-category": {
|
||||
model: "openai/gpt-5.2",
|
||||
variant: "xhigh",
|
||||
},
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"], undefined, categories)
|
||||
|
||||
// #then
|
||||
expect(agent.model).toBe("openai/gpt-5.2")
|
||||
expect(agent.variant).toBe("xhigh")
|
||||
})
|
||||
|
||||
test("agent with skills has content prepended to prompt", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
skills: ["frontend-ui-ux"],
|
||||
prompt: "Original prompt content",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
|
||||
// #then
|
||||
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
|
||||
expect(agent.prompt).toContain("Original prompt content")
|
||||
expect(agent.prompt).toMatch(/Designer-Turned-Developer[\s\S]*Original prompt content/s)
|
||||
})
|
||||
|
||||
test("agent with multiple skills has all content prepended", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
skills: ["frontend-ui-ux"],
|
||||
prompt: "Agent prompt",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
|
||||
// #then
|
||||
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
|
||||
expect(agent.prompt).toContain("Agent prompt")
|
||||
})
|
||||
|
||||
test("agent without category or skills works as before", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
model: "custom/model",
|
||||
temperature: 0.5,
|
||||
prompt: "Base prompt",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
|
||||
// #then
|
||||
expect(agent.model).toBe("custom/model")
|
||||
expect(agent.temperature).toBe(0.5)
|
||||
expect(agent.prompt).toBe("Base prompt")
|
||||
})
|
||||
|
||||
test("agent with category and skills applies both", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
category: "ultrabrain",
|
||||
skills: ["frontend-ui-ux"],
|
||||
prompt: "Task description",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
|
||||
// #then
|
||||
expect(agent.model).toBe("openai/gpt-5.2")
|
||||
expect(agent.temperature).toBe(0.1)
|
||||
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
|
||||
expect(agent.prompt).toContain("Task description")
|
||||
})
|
||||
|
||||
test("agent with non-existent category has no effect", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
category: "non-existent",
|
||||
prompt: "Base prompt",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
|
||||
// #then
|
||||
expect(agent.model).toBeUndefined()
|
||||
expect(agent.prompt).toBe("Base prompt")
|
||||
})
|
||||
|
||||
test("agent with non-existent skills only prepends found ones", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
skills: ["frontend-ui-ux", "non-existent-skill"],
|
||||
prompt: "Base prompt",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
|
||||
// #then
|
||||
expect(agent.prompt).toContain("Role: Designer-Turned-Developer")
|
||||
expect(agent.prompt).toContain("Base prompt")
|
||||
})
|
||||
|
||||
test("agent with empty skills array keeps original prompt", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
skills: [],
|
||||
prompt: "Base prompt",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agent = buildAgent(source["test-agent"])
|
||||
|
||||
// #then
|
||||
expect(agent.prompt).toBe("Base prompt")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides, AgentFactory, AgentPromptMetadata } from "./types"
|
||||
import type { CategoriesConfig, CategoryConfig, GitMasterConfig } from "../config/schema"
|
||||
import { createSisyphusAgent } from "./sisyphus"
|
||||
import { createOracleAgent, ORACLE_PROMPT_METADATA } from "./oracle"
|
||||
import { createLibrarianAgent, LIBRARIAN_PROMPT_METADATA } from "./librarian"
|
||||
@@ -7,8 +8,13 @@ import { createExploreAgent, EXPLORE_PROMPT_METADATA } from "./explore"
|
||||
import { createFrontendUiUxEngineerAgent, FRONTEND_PROMPT_METADATA } from "./frontend-ui-ux-engineer"
|
||||
import { createDocumentWriterAgent, DOCUMENT_WRITER_PROMPT_METADATA } from "./document-writer"
|
||||
import { createMultimodalLookerAgent, MULTIMODAL_LOOKER_PROMPT_METADATA } from "./multimodal-looker"
|
||||
import { createMetisAgent } from "./metis"
|
||||
import { createOrchestratorSisyphusAgent, orchestratorSisyphusAgent } from "./orchestrator-sisyphus"
|
||||
import { createMomusAgent } from "./momus"
|
||||
import type { AvailableAgent } from "./sisyphus-prompt-builder"
|
||||
import { deepMerge } from "../shared"
|
||||
import { DEFAULT_CATEGORIES } from "../tools/delegate-task/constants"
|
||||
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
|
||||
|
||||
type AgentSource = AgentFactory | AgentConfig
|
||||
|
||||
@@ -20,6 +26,9 @@ const agentSources: Record<BuiltinAgentName, AgentSource> = {
|
||||
"frontend-ui-ux-engineer": createFrontendUiUxEngineerAgent,
|
||||
"document-writer": createDocumentWriterAgent,
|
||||
"multimodal-looker": createMultimodalLookerAgent,
|
||||
"Metis (Plan Consultant)": createMetisAgent,
|
||||
"Momus (Plan Reviewer)": createMomusAgent,
|
||||
"orchestrator-sisyphus": orchestratorSisyphusAgent,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,8 +48,42 @@ function isFactory(source: AgentSource): source is AgentFactory {
|
||||
return typeof source === "function"
|
||||
}
|
||||
|
||||
function buildAgent(source: AgentSource, model?: string): AgentConfig {
|
||||
return isFactory(source) ? source(model) : source
|
||||
export function buildAgent(
|
||||
source: AgentSource,
|
||||
model?: string,
|
||||
categories?: CategoriesConfig,
|
||||
gitMasterConfig?: GitMasterConfig
|
||||
): AgentConfig {
|
||||
const base = isFactory(source) ? source(model) : source
|
||||
const categoryConfigs: Record<string, CategoryConfig> = categories
|
||||
? { ...DEFAULT_CATEGORIES, ...categories }
|
||||
: DEFAULT_CATEGORIES
|
||||
|
||||
const agentWithCategory = base as AgentConfig & { category?: string; skills?: string[]; variant?: string }
|
||||
if (agentWithCategory.category) {
|
||||
const categoryConfig = categoryConfigs[agentWithCategory.category]
|
||||
if (categoryConfig) {
|
||||
if (!base.model) {
|
||||
base.model = categoryConfig.model
|
||||
}
|
||||
if (base.temperature === undefined && categoryConfig.temperature !== undefined) {
|
||||
base.temperature = categoryConfig.temperature
|
||||
}
|
||||
if (base.variant === undefined && categoryConfig.variant !== undefined) {
|
||||
base.variant = categoryConfig.variant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (agentWithCategory.skills?.length) {
|
||||
const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig })
|
||||
if (resolved.size > 0) {
|
||||
const skillContent = Array.from(resolved.values()).join("\n\n")
|
||||
base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "")
|
||||
}
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,21 +130,28 @@ export function createBuiltinAgents(
|
||||
disabledAgents: BuiltinAgentName[] = [],
|
||||
agentOverrides: AgentOverrides = {},
|
||||
directory?: string,
|
||||
systemDefaultModel?: string
|
||||
systemDefaultModel?: string,
|
||||
categories?: CategoriesConfig,
|
||||
gitMasterConfig?: GitMasterConfig
|
||||
): Record<string, AgentConfig> {
|
||||
const result: Record<string, AgentConfig> = {}
|
||||
const availableAgents: AvailableAgent[] = []
|
||||
|
||||
const mergedCategories = categories
|
||||
? { ...DEFAULT_CATEGORIES, ...categories }
|
||||
: DEFAULT_CATEGORIES
|
||||
|
||||
for (const [name, source] of Object.entries(agentSources)) {
|
||||
const agentName = name as BuiltinAgentName
|
||||
|
||||
if (agentName === "Sisyphus") continue
|
||||
if (agentName === "orchestrator-sisyphus") continue
|
||||
if (disabledAgents.includes(agentName)) continue
|
||||
|
||||
const override = agentOverrides[agentName]
|
||||
const model = override?.model
|
||||
|
||||
let config = buildAgent(source, model)
|
||||
let config = buildAgent(source, model, mergedCategories, gitMasterConfig)
|
||||
|
||||
if (agentName === "librarian" && directory && config.prompt) {
|
||||
const envContext = createEnvContext()
|
||||
@@ -142,5 +192,20 @@ export function createBuiltinAgents(
|
||||
result["Sisyphus"] = sisyphusConfig
|
||||
}
|
||||
|
||||
if (!disabledAgents.includes("orchestrator-sisyphus")) {
|
||||
const orchestratorOverride = agentOverrides["orchestrator-sisyphus"]
|
||||
const orchestratorModel = orchestratorOverride?.model ?? systemDefaultModel
|
||||
let orchestratorConfig = createOrchestratorSisyphusAgent({
|
||||
model: orchestratorModel,
|
||||
availableAgents,
|
||||
})
|
||||
|
||||
if (orchestratorOverride) {
|
||||
orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride)
|
||||
}
|
||||
|
||||
result["orchestrator-sisyphus"] = orchestratorConfig
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# AUTH KNOWLEDGE BASE
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
Google Antigravity OAuth for Gemini models. Token management, fetch interception, thinking block extraction.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
auth/
|
||||
└── antigravity/
|
||||
├── plugin.ts # Main export, hooks registration
|
||||
├── oauth.ts # OAuth flow, token acquisition
|
||||
├── token.ts # Token storage, refresh logic
|
||||
├── fetch.ts # Fetch interceptor (621 lines)
|
||||
├── response.ts # Response transformation (598 lines)
|
||||
├── thinking.ts # Thinking block extraction (571 lines)
|
||||
├── thought-signature-store.ts # Signature caching
|
||||
├── message-converter.ts # Format conversion
|
||||
├── request.ts # Request building
|
||||
├── project.ts # Project ID management
|
||||
├── tools.ts # OAuth tool registration
|
||||
├── constants.ts # API endpoints, model mappings
|
||||
└── types.ts
|
||||
```
|
||||
|
||||
## KEY COMPONENTS
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| fetch.ts | URL rewriting, token injection, retries |
|
||||
| thinking.ts | Extract `<antThinking>` blocks |
|
||||
| response.ts | Streaming SSE parsing |
|
||||
| oauth.ts | Browser-based OAuth flow |
|
||||
| token.ts | Token persistence, expiry |
|
||||
|
||||
## HOW IT WORKS
|
||||
|
||||
1. **Intercept**: fetch.ts intercepts Anthropic/Google requests
|
||||
2. **Rewrite**: URLs → Antigravity proxy endpoints
|
||||
3. **Auth**: Bearer token from stored OAuth credentials
|
||||
4. **Response**: Streaming parsed, thinking blocks extracted
|
||||
5. **Transform**: Normalized for OpenCode
|
||||
|
||||
## FEATURES
|
||||
|
||||
- Multi-account (up to 10 Google accounts)
|
||||
- Auto-fallback on rate limit
|
||||
- Thinking blocks preserved
|
||||
- Antigravity proxy for AI Studio access
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
- Direct API calls (use fetch interceptor)
|
||||
- Tokens in code (use token.ts storage)
|
||||
- Ignoring refresh (check expiry first)
|
||||
- Blocking on OAuth (always async)
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* Antigravity OAuth configuration constants.
|
||||
* Values sourced from cliproxyapi/sdk/auth/antigravity.go
|
||||
*
|
||||
* ## Logging Policy
|
||||
*
|
||||
* All console logging in antigravity modules follows a consistent policy:
|
||||
*
|
||||
* - **Debug logs**: Guard with `if (process.env.ANTIGRAVITY_DEBUG === "1")`
|
||||
* - Includes: info messages, warnings, non-fatal errors
|
||||
* - Enable debugging: `ANTIGRAVITY_DEBUG=1 opencode`
|
||||
*
|
||||
* - **Fatal errors**: None currently. All errors are handled by returning
|
||||
* appropriate error responses to OpenCode's auth system.
|
||||
*
|
||||
* This policy ensures production silence while enabling verbose debugging
|
||||
* when needed for troubleshooting OAuth flows.
|
||||
*/
|
||||
|
||||
// OAuth 2.0 Client Credentials
|
||||
export const ANTIGRAVITY_CLIENT_ID =
|
||||
"1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
|
||||
export const ANTIGRAVITY_CLIENT_SECRET = "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"
|
||||
|
||||
// OAuth Callback
|
||||
export const ANTIGRAVITY_CALLBACK_PORT = 51121
|
||||
export const ANTIGRAVITY_REDIRECT_URI = `http://localhost:${ANTIGRAVITY_CALLBACK_PORT}/oauth-callback`
|
||||
|
||||
// OAuth Scopes
|
||||
export const ANTIGRAVITY_SCOPES = [
|
||||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
"https://www.googleapis.com/auth/cclog",
|
||||
"https://www.googleapis.com/auth/experimentsandconfigs",
|
||||
] as const
|
||||
|
||||
// API Endpoint Fallbacks (order: daily → autopush → prod)
|
||||
export const ANTIGRAVITY_ENDPOINT_FALLBACKS = [
|
||||
"https://daily-cloudcode-pa.sandbox.googleapis.com", // dev
|
||||
"https://autopush-cloudcode-pa.sandbox.googleapis.com", // staging
|
||||
"https://cloudcode-pa.googleapis.com", // prod
|
||||
] as const
|
||||
|
||||
// API Version
|
||||
export const ANTIGRAVITY_API_VERSION = "v1internal"
|
||||
|
||||
// Request Headers
|
||||
export const ANTIGRAVITY_HEADERS = {
|
||||
"User-Agent": "google-api-nodejs-client/9.15.1",
|
||||
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
||||
"Client-Metadata": JSON.stringify({
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
}),
|
||||
} as const
|
||||
|
||||
// Default Project ID (fallback when loadCodeAssist API fails)
|
||||
// From opencode-antigravity-auth reference implementation
|
||||
export const ANTIGRAVITY_DEFAULT_PROJECT_ID = "rising-fact-p41fc"
|
||||
|
||||
|
||||
|
||||
// Google OAuth endpoints
|
||||
export const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
export const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
|
||||
export const GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo"
|
||||
|
||||
// Token refresh buffer (refresh 60 seconds before expiry)
|
||||
export const ANTIGRAVITY_TOKEN_REFRESH_BUFFER_MS = 60_000
|
||||
|
||||
// Default thought signature to skip validation (CLIProxyAPI approach)
|
||||
export const SKIP_THOUGHT_SIGNATURE_VALIDATOR = "skip_thought_signature_validator"
|
||||
@@ -1,621 +0,0 @@
|
||||
/**
|
||||
* Antigravity Fetch Interceptor
|
||||
*
|
||||
* Creates a custom fetch function that:
|
||||
* - Checks token expiration and auto-refreshes
|
||||
* - Rewrites URLs to Antigravity endpoints
|
||||
* - Applies request transformation (including tool normalization)
|
||||
* - Applies response transformation (including thinking extraction)
|
||||
* - Implements endpoint fallback (daily → autopush → prod)
|
||||
*
|
||||
* **Body Type Assumption:**
|
||||
* This interceptor assumes `init.body` is a JSON string (OpenAI format).
|
||||
* Non-string bodies (ReadableStream, Blob, FormData, URLSearchParams, etc.)
|
||||
* are passed through unchanged to the original fetch to avoid breaking
|
||||
* other requests that may not be OpenAI-format API calls.
|
||||
*
|
||||
* Debug logging available via ANTIGRAVITY_DEBUG=1 environment variable.
|
||||
*/
|
||||
|
||||
import { ANTIGRAVITY_ENDPOINT_FALLBACKS } from "./constants"
|
||||
import { fetchProjectContext, clearProjectContextCache, invalidateProjectContextByRefreshToken } from "./project"
|
||||
import { isTokenExpired, refreshAccessToken, parseStoredToken, formatTokenForStorage, AntigravityTokenRefreshError } from "./token"
|
||||
import { transformRequest } from "./request"
|
||||
import { convertRequestBody, hasOpenAIMessages } from "./message-converter"
|
||||
import {
|
||||
transformResponse,
|
||||
transformStreamingResponse,
|
||||
isStreamingResponse,
|
||||
} from "./response"
|
||||
import { normalizeToolsForGemini, type OpenAITool } from "./tools"
|
||||
import { extractThinkingBlocks, shouldIncludeThinking, transformResponseThinking } from "./thinking"
|
||||
import {
|
||||
getThoughtSignature,
|
||||
setThoughtSignature,
|
||||
getOrCreateSessionId,
|
||||
} from "./thought-signature-store"
|
||||
import type { AntigravityTokens } from "./types"
|
||||
|
||||
/**
|
||||
* Auth interface matching OpenCode's auth system
|
||||
*/
|
||||
interface Auth {
|
||||
access?: string
|
||||
refresh?: string
|
||||
expires?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Client interface for auth operations
|
||||
*/
|
||||
interface AuthClient {
|
||||
set(providerId: string, auth: Auth): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug logging helper
|
||||
* Only logs when ANTIGRAVITY_DEBUG=1
|
||||
*/
|
||||
function debugLog(message: string): void {
|
||||
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.log(`[antigravity-fetch] ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
function isRetryableError(status: number): boolean {
|
||||
if (status === 0) return true
|
||||
if (status === 429) return true
|
||||
if (status >= 500 && status < 600) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const GCP_PERMISSION_ERROR_PATTERNS = [
|
||||
"PERMISSION_DENIED",
|
||||
"does not have permission",
|
||||
"Cloud AI Companion API has not been used",
|
||||
"has not been enabled",
|
||||
] as const
|
||||
|
||||
function isGcpPermissionError(text: string): boolean {
|
||||
return GCP_PERMISSION_ERROR_PATTERNS.some((pattern) => text.includes(pattern))
|
||||
}
|
||||
|
||||
function calculateRetryDelay(attempt: number): number {
|
||||
return Math.min(200 * Math.pow(2, attempt), 2000)
|
||||
}
|
||||
|
||||
async function isRetryableResponse(response: Response): Promise<boolean> {
|
||||
if (isRetryableError(response.status)) return true
|
||||
if (response.status === 403) {
|
||||
try {
|
||||
const text = await response.clone().text()
|
||||
if (text.includes("SUBSCRIPTION_REQUIRED") || text.includes("Gemini Code Assist license")) {
|
||||
debugLog(`[RETRY] 403 SUBSCRIPTION_REQUIRED detected, will retry with next endpoint`)
|
||||
return true
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
interface AttemptFetchOptions {
|
||||
endpoint: string
|
||||
url: string
|
||||
init: RequestInit
|
||||
accessToken: string
|
||||
projectId: string
|
||||
sessionId: string
|
||||
modelName?: string
|
||||
thoughtSignature?: string
|
||||
}
|
||||
|
||||
type AttemptFetchResult = Response | null | "pass-through" | "needs-refresh"
|
||||
|
||||
async function attemptFetch(
|
||||
options: AttemptFetchOptions
|
||||
): Promise<AttemptFetchResult> {
|
||||
const { endpoint, url, init, accessToken, projectId, sessionId, modelName, thoughtSignature } =
|
||||
options
|
||||
debugLog(`Trying endpoint: ${endpoint}`)
|
||||
|
||||
try {
|
||||
const rawBody = init.body
|
||||
|
||||
if (rawBody !== undefined && typeof rawBody !== "string") {
|
||||
debugLog(`Non-string body detected (${typeof rawBody}), signaling pass-through`)
|
||||
return "pass-through"
|
||||
}
|
||||
|
||||
let parsedBody: Record<string, unknown> = {}
|
||||
if (rawBody) {
|
||||
try {
|
||||
parsedBody = JSON.parse(rawBody) as Record<string, unknown>
|
||||
} catch {
|
||||
parsedBody = {}
|
||||
}
|
||||
}
|
||||
|
||||
debugLog(`[BODY] Keys: ${Object.keys(parsedBody).join(", ")}`)
|
||||
debugLog(`[BODY] Has contents: ${!!parsedBody.contents}, Has messages: ${!!parsedBody.messages}`)
|
||||
if (parsedBody.contents) {
|
||||
const contents = parsedBody.contents as Array<Record<string, unknown>>
|
||||
debugLog(`[BODY] contents length: ${contents.length}`)
|
||||
contents.forEach((c, i) => {
|
||||
debugLog(`[BODY] contents[${i}].role: ${c.role}, parts: ${JSON.stringify(c.parts).substring(0, 200)}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (parsedBody.tools && Array.isArray(parsedBody.tools)) {
|
||||
const normalizedTools = normalizeToolsForGemini(parsedBody.tools as OpenAITool[])
|
||||
if (normalizedTools) {
|
||||
parsedBody.tools = normalizedTools
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOpenAIMessages(parsedBody)) {
|
||||
debugLog(`[CONVERT] Converting OpenAI messages to Gemini contents`)
|
||||
parsedBody = convertRequestBody(parsedBody, thoughtSignature)
|
||||
debugLog(`[CONVERT] After conversion - Has contents: ${!!parsedBody.contents}`)
|
||||
}
|
||||
|
||||
const transformed = transformRequest({
|
||||
url,
|
||||
body: parsedBody,
|
||||
accessToken,
|
||||
projectId,
|
||||
sessionId,
|
||||
modelName,
|
||||
endpointOverride: endpoint,
|
||||
thoughtSignature,
|
||||
})
|
||||
|
||||
debugLog(`[REQ] streaming=${transformed.streaming}, url=${transformed.url}`)
|
||||
|
||||
const maxPermissionRetries = 10
|
||||
for (let attempt = 0; attempt <= maxPermissionRetries; attempt++) {
|
||||
const response = await fetch(transformed.url, {
|
||||
method: init.method || "POST",
|
||||
headers: transformed.headers,
|
||||
body: JSON.stringify(transformed.body),
|
||||
signal: init.signal,
|
||||
})
|
||||
|
||||
debugLog(
|
||||
`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`
|
||||
)
|
||||
|
||||
if (response.status === 401) {
|
||||
debugLog(`[401] Unauthorized response detected, signaling token refresh needed`)
|
||||
return "needs-refresh"
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
try {
|
||||
const text = await response.clone().text()
|
||||
if (isGcpPermissionError(text)) {
|
||||
if (attempt < maxPermissionRetries) {
|
||||
const delay = calculateRetryDelay(attempt)
|
||||
debugLog(`[RETRY] GCP permission error, retry ${attempt + 1}/${maxPermissionRetries} after ${delay}ms`)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
continue
|
||||
}
|
||||
debugLog(`[RETRY] GCP permission error, max retries exceeded`)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!response.ok && (await isRetryableResponse(response))) {
|
||||
debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`)
|
||||
return null
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
debugLog(
|
||||
`Endpoint failed: ${endpoint} (${error instanceof Error ? error.message : "Unknown error"}), trying next`
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface GeminiResponsePart {
|
||||
thoughtSignature?: string
|
||||
thought_signature?: string
|
||||
functionCall?: Record<string, unknown>
|
||||
text?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface GeminiResponseCandidate {
|
||||
content?: {
|
||||
parts?: GeminiResponsePart[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface GeminiResponseBody {
|
||||
candidates?: GeminiResponseCandidate[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
function extractSignatureFromResponse(parsed: GeminiResponseBody): string | undefined {
|
||||
if (!parsed.candidates || !Array.isArray(parsed.candidates)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
for (const candidate of parsed.candidates) {
|
||||
const parts = candidate.content?.parts
|
||||
if (!parts || !Array.isArray(parts)) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const part of parts) {
|
||||
const sig = part.thoughtSignature || part.thought_signature
|
||||
if (sig && typeof sig === "string") {
|
||||
return sig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function transformResponseWithThinking(
|
||||
response: Response,
|
||||
modelName: string,
|
||||
fetchInstanceId: string
|
||||
): Promise<Response> {
|
||||
const streaming = isStreamingResponse(response)
|
||||
|
||||
let result
|
||||
if (streaming) {
|
||||
result = await transformStreamingResponse(response)
|
||||
} else {
|
||||
result = await transformResponse(response)
|
||||
}
|
||||
|
||||
if (streaming) {
|
||||
return result.response
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await result.response.clone().text()
|
||||
debugLog(`[TSIG][RESP] Response text length: ${text.length}`)
|
||||
|
||||
const parsed = JSON.parse(text) as GeminiResponseBody
|
||||
debugLog(`[TSIG][RESP] Parsed keys: ${Object.keys(parsed).join(", ")}`)
|
||||
debugLog(`[TSIG][RESP] Has candidates: ${!!parsed.candidates}, count: ${parsed.candidates?.length ?? 0}`)
|
||||
|
||||
const signature = extractSignatureFromResponse(parsed)
|
||||
debugLog(`[TSIG][RESP] Signature extracted: ${signature ? signature.substring(0, 30) + "..." : "NONE"}`)
|
||||
if (signature) {
|
||||
setThoughtSignature(fetchInstanceId, signature)
|
||||
debugLog(`[TSIG][STORE] Stored signature for ${fetchInstanceId}`)
|
||||
} else {
|
||||
debugLog(`[TSIG][WARN] No signature found in response!`)
|
||||
}
|
||||
|
||||
if (shouldIncludeThinking(modelName)) {
|
||||
const thinkingResult = extractThinkingBlocks(parsed)
|
||||
if (thinkingResult.hasThinking) {
|
||||
const transformed = transformResponseThinking(parsed)
|
||||
return new Response(JSON.stringify(transformed), {
|
||||
status: result.response.status,
|
||||
statusText: result.response.statusText,
|
||||
headers: result.response.headers,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return result.response
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Antigravity fetch interceptor
|
||||
*
|
||||
* Factory function that creates a custom fetch function for Antigravity API.
|
||||
* Handles token management, request/response transformation, and endpoint fallback.
|
||||
*
|
||||
* @param getAuth - Async function to retrieve current auth state
|
||||
* @param client - Auth client for saving updated tokens
|
||||
* @param providerId - Provider identifier (e.g., "google")
|
||||
* @param clientId - Optional custom client ID for token refresh (defaults to ANTIGRAVITY_CLIENT_ID)
|
||||
* @param clientSecret - Optional custom client secret for token refresh (defaults to ANTIGRAVITY_CLIENT_SECRET)
|
||||
* @returns Custom fetch function compatible with standard fetch signature
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const customFetch = createAntigravityFetch(
|
||||
* () => auth(),
|
||||
* client,
|
||||
* "google",
|
||||
* "custom-client-id",
|
||||
* "custom-client-secret"
|
||||
* )
|
||||
*
|
||||
* // Use like standard fetch
|
||||
* const response = await customFetch("https://api.example.com/chat", {
|
||||
* method: "POST",
|
||||
* body: JSON.stringify({ messages: [...] })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createAntigravityFetch(
|
||||
getAuth: () => Promise<Auth>,
|
||||
client: AuthClient,
|
||||
providerId: string,
|
||||
clientId?: string,
|
||||
clientSecret?: string
|
||||
): (url: string, init?: RequestInit) => Promise<Response> {
|
||||
let cachedTokens: AntigravityTokens | null = null
|
||||
let cachedProjectId: string | null = null
|
||||
const fetchInstanceId = crypto.randomUUID()
|
||||
|
||||
return async (url: string, init: RequestInit = {}): Promise<Response> => {
|
||||
debugLog(`Intercepting request to: ${url}`)
|
||||
|
||||
// Get current auth state
|
||||
const auth = await getAuth()
|
||||
if (!auth.access || !auth.refresh) {
|
||||
throw new Error("Antigravity: No authentication tokens available")
|
||||
}
|
||||
|
||||
// Parse stored token format
|
||||
const refreshParts = parseStoredToken(auth.refresh)
|
||||
|
||||
// Build initial token state
|
||||
if (!cachedTokens) {
|
||||
cachedTokens = {
|
||||
type: "antigravity",
|
||||
access_token: auth.access,
|
||||
refresh_token: refreshParts.refreshToken,
|
||||
expires_in: auth.expires ? Math.floor((auth.expires - Date.now()) / 1000) : 3600,
|
||||
timestamp: auth.expires ? auth.expires - 3600 * 1000 : Date.now(),
|
||||
}
|
||||
} else {
|
||||
// Update with fresh values
|
||||
cachedTokens.access_token = auth.access
|
||||
cachedTokens.refresh_token = refreshParts.refreshToken
|
||||
}
|
||||
|
||||
// Check token expiration and refresh if needed
|
||||
if (isTokenExpired(cachedTokens)) {
|
||||
debugLog("Token expired, refreshing...")
|
||||
|
||||
try {
|
||||
const newTokens = await refreshAccessToken(refreshParts.refreshToken, clientId, clientSecret)
|
||||
|
||||
cachedTokens = {
|
||||
type: "antigravity",
|
||||
access_token: newTokens.access_token,
|
||||
refresh_token: newTokens.refresh_token,
|
||||
expires_in: newTokens.expires_in,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
clearProjectContextCache()
|
||||
|
||||
const formattedRefresh = formatTokenForStorage(
|
||||
newTokens.refresh_token,
|
||||
refreshParts.projectId || "",
|
||||
refreshParts.managedProjectId
|
||||
)
|
||||
|
||||
await client.set(providerId, {
|
||||
access: newTokens.access_token,
|
||||
refresh: formattedRefresh,
|
||||
expires: Date.now() + newTokens.expires_in * 1000,
|
||||
})
|
||||
|
||||
debugLog("Token refreshed successfully")
|
||||
} catch (error) {
|
||||
if (error instanceof AntigravityTokenRefreshError) {
|
||||
if (error.isInvalidGrant) {
|
||||
debugLog(`[REFRESH] Token revoked (invalid_grant), clearing caches`)
|
||||
invalidateProjectContextByRefreshToken(refreshParts.refreshToken)
|
||||
clearProjectContextCache()
|
||||
}
|
||||
throw new Error(
|
||||
`Antigravity: Token refresh failed: ${error.description || error.message}${error.code ? ` (${error.code})` : ""}`
|
||||
)
|
||||
}
|
||||
throw new Error(
|
||||
`Antigravity: Token refresh failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch project ID via loadCodeAssist (CLIProxyAPI approach)
|
||||
if (!cachedProjectId) {
|
||||
const projectContext = await fetchProjectContext(cachedTokens.access_token)
|
||||
cachedProjectId = projectContext.cloudaicompanionProject || ""
|
||||
debugLog(`[PROJECT] Fetched project ID: "${cachedProjectId}"`)
|
||||
}
|
||||
|
||||
const projectId = cachedProjectId
|
||||
debugLog(`[PROJECT] Using project ID: "${projectId}"`)
|
||||
|
||||
// Extract model name from request body
|
||||
let modelName: string | undefined
|
||||
if (init.body) {
|
||||
try {
|
||||
const body =
|
||||
typeof init.body === "string"
|
||||
? (JSON.parse(init.body) as Record<string, unknown>)
|
||||
: (init.body as unknown as Record<string, unknown>)
|
||||
if (typeof body.model === "string") {
|
||||
modelName = body.model
|
||||
}
|
||||
} catch {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
}
|
||||
|
||||
const maxEndpoints = Math.min(ANTIGRAVITY_ENDPOINT_FALLBACKS.length, 3)
|
||||
const sessionId = getOrCreateSessionId(fetchInstanceId)
|
||||
const thoughtSignature = getThoughtSignature(fetchInstanceId)
|
||||
debugLog(`[TSIG][GET] sessionId=${sessionId}, signature=${thoughtSignature ? thoughtSignature.substring(0, 20) + "..." : "none"}`)
|
||||
|
||||
let hasRefreshedFor401 = false
|
||||
|
||||
const executeWithEndpoints = async (): Promise<Response> => {
|
||||
for (let i = 0; i < maxEndpoints; i++) {
|
||||
const endpoint = ANTIGRAVITY_ENDPOINT_FALLBACKS[i]
|
||||
|
||||
const response = await attemptFetch({
|
||||
endpoint,
|
||||
url,
|
||||
init,
|
||||
accessToken: cachedTokens!.access_token,
|
||||
projectId,
|
||||
sessionId,
|
||||
modelName,
|
||||
thoughtSignature,
|
||||
})
|
||||
|
||||
if (response === "pass-through") {
|
||||
debugLog("Non-string body detected, passing through with auth headers")
|
||||
const headersWithAuth = {
|
||||
...init.headers,
|
||||
Authorization: `Bearer ${cachedTokens!.access_token}`,
|
||||
}
|
||||
return fetch(url, { ...init, headers: headersWithAuth })
|
||||
}
|
||||
|
||||
if (response === "needs-refresh") {
|
||||
if (hasRefreshedFor401) {
|
||||
debugLog("[401] Already refreshed once, returning unauthorized error")
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Authentication failed after token refresh",
|
||||
type: "unauthorized",
|
||||
code: "token_refresh_failed",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
statusText: "Unauthorized",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
debugLog("[401] Refreshing token and retrying...")
|
||||
hasRefreshedFor401 = true
|
||||
|
||||
try {
|
||||
const newTokens = await refreshAccessToken(
|
||||
refreshParts.refreshToken,
|
||||
clientId,
|
||||
clientSecret
|
||||
)
|
||||
|
||||
cachedTokens = {
|
||||
type: "antigravity",
|
||||
access_token: newTokens.access_token,
|
||||
refresh_token: newTokens.refresh_token,
|
||||
expires_in: newTokens.expires_in,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
clearProjectContextCache()
|
||||
|
||||
const formattedRefresh = formatTokenForStorage(
|
||||
newTokens.refresh_token,
|
||||
refreshParts.projectId || "",
|
||||
refreshParts.managedProjectId
|
||||
)
|
||||
|
||||
await client.set(providerId, {
|
||||
access: newTokens.access_token,
|
||||
refresh: formattedRefresh,
|
||||
expires: Date.now() + newTokens.expires_in * 1000,
|
||||
})
|
||||
|
||||
debugLog("[401] Token refreshed, retrying request...")
|
||||
return executeWithEndpoints()
|
||||
} catch (refreshError) {
|
||||
if (refreshError instanceof AntigravityTokenRefreshError) {
|
||||
if (refreshError.isInvalidGrant) {
|
||||
debugLog(`[401] Token revoked (invalid_grant), clearing caches`)
|
||||
invalidateProjectContextByRefreshToken(refreshParts.refreshToken)
|
||||
clearProjectContextCache()
|
||||
}
|
||||
debugLog(`[401] Token refresh failed: ${refreshError.description || refreshError.message}`)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: refreshError.description || refreshError.message,
|
||||
type: refreshError.isInvalidGrant ? "token_revoked" : "unauthorized",
|
||||
code: refreshError.code || "token_refresh_failed",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
statusText: "Unauthorized",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
)
|
||||
}
|
||||
debugLog(`[401] Token refresh failed: ${refreshError instanceof Error ? refreshError.message : "Unknown error"}`)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: refreshError instanceof Error ? refreshError.message : "Unknown error",
|
||||
type: "unauthorized",
|
||||
code: "token_refresh_failed",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
statusText: "Unauthorized",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (response) {
|
||||
debugLog(`Success with endpoint: ${endpoint}`)
|
||||
const transformedResponse = await transformResponseWithThinking(
|
||||
response,
|
||||
modelName || "",
|
||||
fetchInstanceId
|
||||
)
|
||||
return transformedResponse
|
||||
}
|
||||
}
|
||||
|
||||
const errorMessage = `All Antigravity endpoints failed after ${maxEndpoints} attempts`
|
||||
debugLog(errorMessage)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: errorMessage,
|
||||
type: "endpoint_failure",
|
||||
code: "all_endpoints_failed",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 503,
|
||||
statusText: "Service Unavailable",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return executeWithEndpoints()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type export for createAntigravityFetch return type
|
||||
*/
|
||||
export type AntigravityFetch = (url: string, init?: RequestInit) => Promise<Response>
|
||||
@@ -1,13 +0,0 @@
|
||||
export * from "./types"
|
||||
export * from "./constants"
|
||||
export * from "./oauth"
|
||||
export * from "./token"
|
||||
export * from "./project"
|
||||
export * from "./request"
|
||||
export * from "./response"
|
||||
export * from "./tools"
|
||||
export * from "./thinking"
|
||||
export * from "./thought-signature-store"
|
||||
export * from "./message-converter"
|
||||
export * from "./fetch"
|
||||
export * from "./plugin"
|
||||
@@ -1,206 +0,0 @@
|
||||
/**
|
||||
* OpenAI → Gemini message format converter
|
||||
*
|
||||
* Converts OpenAI-style messages to Gemini contents format,
|
||||
* injecting thoughtSignature into functionCall parts.
|
||||
*/
|
||||
|
||||
import { SKIP_THOUGHT_SIGNATURE_VALIDATOR } from "./constants"
|
||||
|
||||
function debugLog(message: string): void {
|
||||
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.log(`[antigravity-converter] ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
interface OpenAIMessage {
|
||||
role: "system" | "user" | "assistant" | "tool"
|
||||
content?: string | OpenAIContentPart[]
|
||||
tool_calls?: OpenAIToolCall[]
|
||||
tool_call_id?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
interface OpenAIContentPart {
|
||||
type: string
|
||||
text?: string
|
||||
image_url?: { url: string }
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface OpenAIToolCall {
|
||||
id: string
|
||||
type: "function"
|
||||
function: {
|
||||
name: string
|
||||
arguments: string
|
||||
}
|
||||
}
|
||||
|
||||
interface GeminiPart {
|
||||
text?: string
|
||||
functionCall?: {
|
||||
name: string
|
||||
args: Record<string, unknown>
|
||||
}
|
||||
functionResponse?: {
|
||||
name: string
|
||||
response: Record<string, unknown>
|
||||
}
|
||||
inlineData?: {
|
||||
mimeType: string
|
||||
data: string
|
||||
}
|
||||
thought_signature?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface GeminiContent {
|
||||
role: "user" | "model"
|
||||
parts: GeminiPart[]
|
||||
}
|
||||
|
||||
export function convertOpenAIToGemini(
|
||||
messages: OpenAIMessage[],
|
||||
thoughtSignature?: string
|
||||
): GeminiContent[] {
|
||||
debugLog(`Converting ${messages.length} messages, signature: ${thoughtSignature ? "present" : "none"}`)
|
||||
|
||||
const contents: GeminiContent[] = []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "system") {
|
||||
contents.push({
|
||||
role: "user",
|
||||
parts: [{ text: typeof msg.content === "string" ? msg.content : "" }],
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (msg.role === "user") {
|
||||
const parts = convertContentToParts(msg.content)
|
||||
contents.push({ role: "user", parts })
|
||||
continue
|
||||
}
|
||||
|
||||
if (msg.role === "assistant") {
|
||||
const parts: GeminiPart[] = []
|
||||
|
||||
if (msg.content) {
|
||||
parts.push(...convertContentToParts(msg.content))
|
||||
}
|
||||
|
||||
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
for (const toolCall of msg.tool_calls) {
|
||||
let args: Record<string, unknown> = {}
|
||||
try {
|
||||
args = JSON.parse(toolCall.function.arguments)
|
||||
} catch {
|
||||
args = {}
|
||||
}
|
||||
|
||||
const part: GeminiPart = {
|
||||
functionCall: {
|
||||
name: toolCall.function.name,
|
||||
args,
|
||||
},
|
||||
}
|
||||
|
||||
// Always inject signature: use provided or default to skip validator (CLIProxyAPI approach)
|
||||
part.thoughtSignature = thoughtSignature || SKIP_THOUGHT_SIGNATURE_VALIDATOR
|
||||
debugLog(`Injected signature into functionCall: ${toolCall.function.name} (${thoughtSignature ? "provided" : "default"})`)
|
||||
|
||||
parts.push(part)
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.length > 0) {
|
||||
contents.push({ role: "model", parts })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (msg.role === "tool") {
|
||||
let response: Record<string, unknown> = {}
|
||||
try {
|
||||
response = typeof msg.content === "string"
|
||||
? JSON.parse(msg.content)
|
||||
: { result: msg.content }
|
||||
} catch {
|
||||
response = { result: msg.content }
|
||||
}
|
||||
|
||||
const toolName = msg.name || "unknown"
|
||||
|
||||
contents.push({
|
||||
role: "user",
|
||||
parts: [{
|
||||
functionResponse: {
|
||||
name: toolName,
|
||||
response,
|
||||
},
|
||||
}],
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
debugLog(`Converted to ${contents.length} content blocks`)
|
||||
return contents
|
||||
}
|
||||
|
||||
function convertContentToParts(content: string | OpenAIContentPart[] | undefined): GeminiPart[] {
|
||||
if (!content) {
|
||||
return [{ text: "" }]
|
||||
}
|
||||
|
||||
if (typeof content === "string") {
|
||||
return [{ text: content }]
|
||||
}
|
||||
|
||||
const parts: GeminiPart[] = []
|
||||
for (const part of content) {
|
||||
if (part.type === "text" && part.text) {
|
||||
parts.push({ text: part.text })
|
||||
} else if (part.type === "image_url" && part.image_url?.url) {
|
||||
const url = part.image_url.url
|
||||
if (url.startsWith("data:")) {
|
||||
const match = url.match(/^data:([^;]+);base64,(.+)$/)
|
||||
if (match) {
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: match[1],
|
||||
data: match[2],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts : [{ text: "" }]
|
||||
}
|
||||
|
||||
export function hasOpenAIMessages(body: Record<string, unknown>): boolean {
|
||||
return Array.isArray(body.messages) && body.messages.length > 0
|
||||
}
|
||||
|
||||
export function convertRequestBody(
|
||||
body: Record<string, unknown>,
|
||||
thoughtSignature?: string
|
||||
): Record<string, unknown> {
|
||||
if (!hasOpenAIMessages(body)) {
|
||||
debugLog("No messages array found, returning body as-is")
|
||||
return body
|
||||
}
|
||||
|
||||
const messages = body.messages as OpenAIMessage[]
|
||||
const contents = convertOpenAIToGemini(messages, thoughtSignature)
|
||||
|
||||
const converted = { ...body }
|
||||
delete converted.messages
|
||||
converted.contents = contents
|
||||
|
||||
debugLog(`Converted body: messages → contents (${contents.length} blocks)`)
|
||||
return converted
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
/**
|
||||
* Antigravity OAuth 2.0 flow implementation with PKCE.
|
||||
* Handles Google OAuth for Antigravity authentication.
|
||||
*/
|
||||
import { generatePKCE } from "@openauthjs/openauth/pkce"
|
||||
|
||||
import {
|
||||
ANTIGRAVITY_CLIENT_ID,
|
||||
ANTIGRAVITY_CLIENT_SECRET,
|
||||
ANTIGRAVITY_REDIRECT_URI,
|
||||
ANTIGRAVITY_SCOPES,
|
||||
ANTIGRAVITY_CALLBACK_PORT,
|
||||
GOOGLE_AUTH_URL,
|
||||
GOOGLE_TOKEN_URL,
|
||||
GOOGLE_USERINFO_URL,
|
||||
} from "./constants"
|
||||
import type {
|
||||
AntigravityTokenExchangeResult,
|
||||
AntigravityUserInfo,
|
||||
} from "./types"
|
||||
|
||||
/**
|
||||
* PKCE pair containing verifier and challenge.
|
||||
*/
|
||||
export interface PKCEPair {
|
||||
/** PKCE verifier - used during token exchange */
|
||||
verifier: string
|
||||
/** PKCE challenge - sent in auth URL */
|
||||
challenge: string
|
||||
/** Challenge method - always "S256" */
|
||||
method: string
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth state encoded in the auth URL.
|
||||
* Contains the PKCE verifier for later retrieval.
|
||||
*/
|
||||
export interface OAuthState {
|
||||
/** PKCE verifier */
|
||||
verifier: string
|
||||
/** Optional project ID */
|
||||
projectId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from building an OAuth authorization URL.
|
||||
*/
|
||||
export interface AuthorizationResult {
|
||||
/** Full OAuth URL to open in browser */
|
||||
url: string
|
||||
/** PKCE verifier to use during code exchange */
|
||||
verifier: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Result from the OAuth callback server.
|
||||
*/
|
||||
export interface CallbackResult {
|
||||
/** Authorization code from Google */
|
||||
code: string
|
||||
/** State parameter from callback */
|
||||
state: string
|
||||
/** Error message if any */
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PKCE verifier and challenge pair.
|
||||
* Uses @openauthjs/openauth for cryptographically secure generation.
|
||||
*
|
||||
* @returns PKCE pair with verifier, challenge, and method
|
||||
*/
|
||||
export async function generatePKCEPair(): Promise<PKCEPair> {
|
||||
const pkce = await generatePKCE()
|
||||
return {
|
||||
verifier: pkce.verifier,
|
||||
challenge: pkce.challenge,
|
||||
method: pkce.method,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode OAuth state into a URL-safe base64 string.
|
||||
*
|
||||
* @param state - OAuth state object
|
||||
* @returns Base64URL encoded state
|
||||
*/
|
||||
function encodeState(state: OAuthState): string {
|
||||
const json = JSON.stringify(state)
|
||||
return Buffer.from(json, "utf8").toString("base64url")
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode OAuth state from a base64 string.
|
||||
*
|
||||
* @param encoded - Base64URL or Base64 encoded state
|
||||
* @returns Decoded OAuth state
|
||||
*/
|
||||
export function decodeState(encoded: string): OAuthState {
|
||||
// Handle both base64url and standard base64
|
||||
const normalized = encoded.replace(/-/g, "+").replace(/_/g, "/")
|
||||
const padded = normalized.padEnd(
|
||||
normalized.length + ((4 - (normalized.length % 4)) % 4),
|
||||
"="
|
||||
)
|
||||
const json = Buffer.from(padded, "base64").toString("utf8")
|
||||
const parsed = JSON.parse(json)
|
||||
|
||||
if (typeof parsed.verifier !== "string") {
|
||||
throw new Error("Missing PKCE verifier in state")
|
||||
}
|
||||
|
||||
return {
|
||||
verifier: parsed.verifier,
|
||||
projectId:
|
||||
typeof parsed.projectId === "string" ? parsed.projectId : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildAuthURL(
|
||||
projectId?: string,
|
||||
clientId: string = ANTIGRAVITY_CLIENT_ID,
|
||||
port: number = ANTIGRAVITY_CALLBACK_PORT
|
||||
): Promise<AuthorizationResult> {
|
||||
const pkce = await generatePKCEPair()
|
||||
|
||||
const state: OAuthState = {
|
||||
verifier: pkce.verifier,
|
||||
projectId,
|
||||
}
|
||||
|
||||
const redirectUri = `http://localhost:${port}/oauth-callback`
|
||||
|
||||
const url = new URL(GOOGLE_AUTH_URL)
|
||||
url.searchParams.set("client_id", clientId)
|
||||
url.searchParams.set("redirect_uri", redirectUri)
|
||||
url.searchParams.set("response_type", "code")
|
||||
url.searchParams.set("scope", ANTIGRAVITY_SCOPES.join(" "))
|
||||
url.searchParams.set("state", encodeState(state))
|
||||
url.searchParams.set("code_challenge", pkce.challenge)
|
||||
url.searchParams.set("code_challenge_method", "S256")
|
||||
url.searchParams.set("access_type", "offline")
|
||||
url.searchParams.set("prompt", "consent")
|
||||
|
||||
return {
|
||||
url: url.toString(),
|
||||
verifier: pkce.verifier,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange authorization code for tokens.
|
||||
*
|
||||
* @param code - Authorization code from OAuth callback
|
||||
* @param verifier - PKCE verifier from initial auth request
|
||||
* @param clientId - Optional custom client ID (defaults to ANTIGRAVITY_CLIENT_ID)
|
||||
* @param clientSecret - Optional custom client secret (defaults to ANTIGRAVITY_CLIENT_SECRET)
|
||||
* @returns Token exchange result with access and refresh tokens
|
||||
*/
|
||||
export async function exchangeCode(
|
||||
code: string,
|
||||
verifier: string,
|
||||
clientId: string = ANTIGRAVITY_CLIENT_ID,
|
||||
clientSecret: string = ANTIGRAVITY_CLIENT_SECRET,
|
||||
port: number = ANTIGRAVITY_CALLBACK_PORT
|
||||
): Promise<AntigravityTokenExchangeResult> {
|
||||
const redirectUri = `http://localhost:${port}/oauth-callback`
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: redirectUri,
|
||||
code_verifier: verifier,
|
||||
})
|
||||
|
||||
const response = await fetch(GOOGLE_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: params,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Token exchange failed: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
token_type: string
|
||||
}
|
||||
|
||||
return {
|
||||
access_token: data.access_token,
|
||||
refresh_token: data.refresh_token,
|
||||
expires_in: data.expires_in,
|
||||
token_type: data.token_type,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch user info from Google's userinfo API.
|
||||
*
|
||||
* @param accessToken - Valid access token
|
||||
* @returns User info containing email
|
||||
*/
|
||||
export async function fetchUserInfo(
|
||||
accessToken: string
|
||||
): Promise<AntigravityUserInfo> {
|
||||
const response = await fetch(`${GOOGLE_USERINFO_URL}?alt=json`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch user info: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
email?: string
|
||||
name?: string
|
||||
picture?: string
|
||||
}
|
||||
|
||||
return {
|
||||
email: data.email || "",
|
||||
name: data.name,
|
||||
picture: data.picture,
|
||||
}
|
||||
}
|
||||
|
||||
export interface CallbackServerHandle {
|
||||
port: number
|
||||
waitForCallback: () => Promise<CallbackResult>
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export function startCallbackServer(
|
||||
timeoutMs: number = 5 * 60 * 1000
|
||||
): CallbackServerHandle {
|
||||
let server: ReturnType<typeof Bun.serve> | null = null
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
let resolveCallback: ((result: CallbackResult) => void) | null = null
|
||||
let rejectCallback: ((error: Error) => void) | null = null
|
||||
|
||||
const cleanup = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = null
|
||||
}
|
||||
if (server) {
|
||||
server.stop()
|
||||
server = null
|
||||
}
|
||||
}
|
||||
|
||||
server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(request: Request): Response {
|
||||
const url = new URL(request.url)
|
||||
|
||||
if (url.pathname === "/oauth-callback") {
|
||||
const code = url.searchParams.get("code") || ""
|
||||
const state = url.searchParams.get("state") || ""
|
||||
const error = url.searchParams.get("error") || undefined
|
||||
|
||||
let responseBody: string
|
||||
if (code && !error) {
|
||||
responseBody =
|
||||
"<html><body><h1>Login successful</h1><p>You can close this window.</p></body></html>"
|
||||
} else {
|
||||
responseBody =
|
||||
"<html><body><h1>Login failed</h1><p>Please check the CLI output.</p></body></html>"
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
cleanup()
|
||||
if (resolveCallback) {
|
||||
resolveCallback({ code, state, error })
|
||||
}
|
||||
}, 100)
|
||||
|
||||
return new Response(responseBody, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/html" },
|
||||
})
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 })
|
||||
},
|
||||
})
|
||||
|
||||
const actualPort = server.port as number
|
||||
|
||||
const waitForCallback = (): Promise<CallbackResult> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolveCallback = resolve
|
||||
rejectCallback = reject
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup()
|
||||
reject(new Error("OAuth callback timeout"))
|
||||
}, timeoutMs)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
port: actualPort,
|
||||
waitForCallback,
|
||||
close: cleanup,
|
||||
}
|
||||
}
|
||||
|
||||
export async function performOAuthFlow(
|
||||
projectId?: string,
|
||||
openBrowser?: (url: string) => Promise<void>,
|
||||
clientId: string = ANTIGRAVITY_CLIENT_ID,
|
||||
clientSecret: string = ANTIGRAVITY_CLIENT_SECRET
|
||||
): Promise<{
|
||||
tokens: AntigravityTokenExchangeResult
|
||||
userInfo: AntigravityUserInfo
|
||||
verifier: string
|
||||
}> {
|
||||
const serverHandle = startCallbackServer()
|
||||
|
||||
try {
|
||||
const auth = await buildAuthURL(projectId, clientId, serverHandle.port)
|
||||
|
||||
if (openBrowser) {
|
||||
await openBrowser(auth.url)
|
||||
}
|
||||
|
||||
const callback = await serverHandle.waitForCallback()
|
||||
|
||||
if (callback.error) {
|
||||
throw new Error(`OAuth error: ${callback.error}`)
|
||||
}
|
||||
|
||||
if (!callback.code) {
|
||||
throw new Error("No authorization code received")
|
||||
}
|
||||
|
||||
const state = decodeState(callback.state)
|
||||
if (state.verifier !== auth.verifier) {
|
||||
throw new Error("PKCE verifier mismatch - possible CSRF attack")
|
||||
}
|
||||
|
||||
const tokens = await exchangeCode(callback.code, auth.verifier, clientId, clientSecret, serverHandle.port)
|
||||
const userInfo = await fetchUserInfo(tokens.access_token)
|
||||
|
||||
return { tokens, userInfo, verifier: auth.verifier }
|
||||
} catch (err) {
|
||||
serverHandle.close()
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
/**
|
||||
* Google Antigravity Auth Plugin for OpenCode
|
||||
*
|
||||
* Provides OAuth authentication for Google models via Antigravity API.
|
||||
* This plugin integrates with OpenCode's auth system to enable:
|
||||
* - OAuth 2.0 with PKCE flow for Google authentication
|
||||
* - Automatic token refresh
|
||||
* - Request/response transformation for Antigravity API
|
||||
*
|
||||
* @example
|
||||
* ```json
|
||||
* // opencode.json
|
||||
* {
|
||||
* "plugin": ["oh-my-opencode"],
|
||||
* "provider": {
|
||||
* "google": {
|
||||
* "options": {
|
||||
* "clientId": "custom-client-id",
|
||||
* "clientSecret": "custom-client-secret"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type { Auth, Provider } from "@opencode-ai/sdk"
|
||||
import type { AuthHook, AuthOuathResult, PluginInput } from "@opencode-ai/plugin"
|
||||
|
||||
import { ANTIGRAVITY_CLIENT_ID, ANTIGRAVITY_CLIENT_SECRET } from "./constants"
|
||||
import {
|
||||
buildAuthURL,
|
||||
exchangeCode,
|
||||
startCallbackServer,
|
||||
fetchUserInfo,
|
||||
decodeState,
|
||||
} from "./oauth"
|
||||
import { createAntigravityFetch } from "./fetch"
|
||||
import { fetchProjectContext } from "./project"
|
||||
import { formatTokenForStorage } from "./token"
|
||||
|
||||
/**
|
||||
* Provider ID for Google models
|
||||
* Antigravity is an auth method for Google, not a separate provider
|
||||
*/
|
||||
const GOOGLE_PROVIDER_ID = "google"
|
||||
|
||||
/**
|
||||
* Type guard to check if auth is OAuth type
|
||||
*/
|
||||
function isOAuthAuth(
|
||||
auth: Auth
|
||||
): auth is { type: "oauth"; access: string; refresh: string; expires: number } {
|
||||
return auth.type === "oauth"
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the Google Antigravity OAuth plugin for OpenCode.
|
||||
*
|
||||
* This factory function creates an auth plugin that:
|
||||
* 1. Provides OAuth flow for Google authentication
|
||||
* 2. Creates a custom fetch interceptor for Antigravity API
|
||||
* 3. Handles token management and refresh
|
||||
*
|
||||
* @param input - Plugin input containing the OpenCode client
|
||||
* @returns Hooks object with auth configuration
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Used by OpenCode automatically when plugin is loaded
|
||||
* const hooks = await createGoogleAntigravityAuthPlugin({ client, ... })
|
||||
* ```
|
||||
*/
|
||||
export async function createGoogleAntigravityAuthPlugin({
|
||||
client,
|
||||
}: PluginInput): Promise<{ auth: AuthHook }> {
|
||||
// Cache for custom credentials from provider.options
|
||||
// These are populated by loader() and used by authorize()
|
||||
// Falls back to defaults if loader hasn't been called yet
|
||||
let cachedClientId: string = ANTIGRAVITY_CLIENT_ID
|
||||
let cachedClientSecret: string = ANTIGRAVITY_CLIENT_SECRET
|
||||
|
||||
const authHook: AuthHook = {
|
||||
/**
|
||||
* Provider identifier - must be "google" as Antigravity is
|
||||
* an auth method for Google models, not a separate provider
|
||||
*/
|
||||
provider: GOOGLE_PROVIDER_ID,
|
||||
|
||||
/**
|
||||
* Loader function called when auth is needed.
|
||||
* Reads credentials from provider.options and creates custom fetch.
|
||||
*
|
||||
* @param auth - Function to retrieve current auth state
|
||||
* @param provider - Provider configuration including options
|
||||
* @returns Object with custom fetch function
|
||||
*/
|
||||
loader: async (
|
||||
auth: () => Promise<Auth>,
|
||||
provider: Provider
|
||||
): Promise<Record<string, unknown>> => {
|
||||
const currentAuth = await auth()
|
||||
|
||||
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.log("[antigravity-plugin] loader called")
|
||||
console.log("[antigravity-plugin] auth type:", currentAuth?.type)
|
||||
console.log("[antigravity-plugin] auth keys:", Object.keys(currentAuth || {}))
|
||||
}
|
||||
|
||||
if (!isOAuthAuth(currentAuth)) {
|
||||
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.log("[antigravity-plugin] NOT OAuth auth, returning empty")
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.log("[antigravity-plugin] OAuth auth detected, creating custom fetch")
|
||||
}
|
||||
|
||||
cachedClientId =
|
||||
(provider.options?.clientId as string) || ANTIGRAVITY_CLIENT_ID
|
||||
cachedClientSecret =
|
||||
(provider.options?.clientSecret as string) || ANTIGRAVITY_CLIENT_SECRET
|
||||
|
||||
// Log if using custom credentials (for debugging)
|
||||
if (
|
||||
process.env.ANTIGRAVITY_DEBUG === "1" &&
|
||||
(cachedClientId !== ANTIGRAVITY_CLIENT_ID ||
|
||||
cachedClientSecret !== ANTIGRAVITY_CLIENT_SECRET)
|
||||
) {
|
||||
console.log(
|
||||
"[antigravity-plugin] Using custom credentials from provider.options"
|
||||
)
|
||||
}
|
||||
|
||||
// Create adapter for client.auth.set that matches fetch.ts AuthClient interface
|
||||
const authClient = {
|
||||
set: async (
|
||||
providerId: string,
|
||||
authData: { access?: string; refresh?: string; expires?: number }
|
||||
) => {
|
||||
await client.auth.set({
|
||||
body: {
|
||||
type: "oauth",
|
||||
access: authData.access || "",
|
||||
refresh: authData.refresh || "",
|
||||
expires: authData.expires || 0,
|
||||
},
|
||||
path: { id: providerId },
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// Create auth getter that returns compatible format for fetch.ts
|
||||
const getAuth = async (): Promise<{
|
||||
access?: string
|
||||
refresh?: string
|
||||
expires?: number
|
||||
}> => {
|
||||
const authState = await auth()
|
||||
if (isOAuthAuth(authState)) {
|
||||
return {
|
||||
access: authState.access,
|
||||
refresh: authState.refresh,
|
||||
expires: authState.expires,
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
const antigravityFetch = createAntigravityFetch(
|
||||
getAuth,
|
||||
authClient,
|
||||
GOOGLE_PROVIDER_ID,
|
||||
cachedClientId,
|
||||
cachedClientSecret
|
||||
)
|
||||
|
||||
return {
|
||||
fetch: antigravityFetch,
|
||||
apiKey: "antigravity-oauth",
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Authentication methods available for this provider.
|
||||
* Only OAuth is supported - no prompts for credentials.
|
||||
*/
|
||||
methods: [
|
||||
{
|
||||
type: "oauth",
|
||||
label: "OAuth with Google (Antigravity)",
|
||||
// NO prompts - credentials come from provider.options or defaults
|
||||
// OAuth flow starts immediately when user selects this method
|
||||
|
||||
/**
|
||||
* Starts the OAuth authorization flow.
|
||||
* Opens browser for Google OAuth and waits for callback.
|
||||
*
|
||||
* @returns Authorization result with URL and callback
|
||||
*/
|
||||
authorize: async (): Promise<AuthOuathResult> => {
|
||||
const serverHandle = startCallbackServer()
|
||||
const { url, verifier } = await buildAuthURL(undefined, cachedClientId, serverHandle.port)
|
||||
|
||||
return {
|
||||
url,
|
||||
instructions:
|
||||
"Complete the sign-in in your browser. We'll automatically detect when you're done.",
|
||||
method: "auto",
|
||||
|
||||
callback: async () => {
|
||||
try {
|
||||
const result = await serverHandle.waitForCallback()
|
||||
|
||||
if (result.error) {
|
||||
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.error(`[antigravity-plugin] OAuth error: ${result.error}`)
|
||||
}
|
||||
return { type: "failed" as const }
|
||||
}
|
||||
|
||||
if (!result.code) {
|
||||
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.error("[antigravity-plugin] No authorization code received")
|
||||
}
|
||||
return { type: "failed" as const }
|
||||
}
|
||||
|
||||
const state = decodeState(result.state)
|
||||
if (state.verifier !== verifier) {
|
||||
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.error("[antigravity-plugin] PKCE verifier mismatch")
|
||||
}
|
||||
return { type: "failed" as const }
|
||||
}
|
||||
|
||||
const tokens = await exchangeCode(result.code, verifier, cachedClientId, cachedClientSecret, serverHandle.port)
|
||||
|
||||
try {
|
||||
const userInfo = await fetchUserInfo(tokens.access_token)
|
||||
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.log(`[antigravity-plugin] Authenticated as: ${userInfo.email}`)
|
||||
}
|
||||
} catch {
|
||||
// User info is optional
|
||||
}
|
||||
|
||||
const projectContext = await fetchProjectContext(tokens.access_token)
|
||||
|
||||
const formattedRefresh = formatTokenForStorage(
|
||||
tokens.refresh_token,
|
||||
projectContext.cloudaicompanionProject || "",
|
||||
projectContext.managedProjectId
|
||||
)
|
||||
|
||||
return {
|
||||
type: "success" as const,
|
||||
access: tokens.access_token,
|
||||
refresh: formattedRefresh,
|
||||
expires: Date.now() + tokens.expires_in * 1000,
|
||||
}
|
||||
} catch (error) {
|
||||
serverHandle.close()
|
||||
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.error(
|
||||
`[antigravity-plugin] OAuth flow failed: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
)
|
||||
}
|
||||
return { type: "failed" as const }
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return {
|
||||
auth: authHook,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default export for OpenCode plugin system
|
||||
*/
|
||||
export default createGoogleAntigravityAuthPlugin
|
||||
|
||||
/**
|
||||
* Named export for explicit imports
|
||||
*/
|
||||
export const GoogleAntigravityAuthPlugin = createGoogleAntigravityAuthPlugin
|
||||
@@ -1,274 +0,0 @@
|
||||
/**
|
||||
* Antigravity project context management.
|
||||
* Handles fetching GCP project ID via Google's loadCodeAssist API.
|
||||
* For FREE tier users, onboards via onboardUser API to get server-assigned managed project ID.
|
||||
* Reference: https://github.com/shekohex/opencode-google-antigravity-auth
|
||||
*/
|
||||
|
||||
import {
|
||||
ANTIGRAVITY_ENDPOINT_FALLBACKS,
|
||||
ANTIGRAVITY_API_VERSION,
|
||||
ANTIGRAVITY_HEADERS,
|
||||
ANTIGRAVITY_DEFAULT_PROJECT_ID,
|
||||
} from "./constants"
|
||||
import type {
|
||||
AntigravityProjectContext,
|
||||
AntigravityLoadCodeAssistResponse,
|
||||
AntigravityOnboardUserPayload,
|
||||
AntigravityUserTier,
|
||||
} from "./types"
|
||||
|
||||
const projectContextCache = new Map<string, AntigravityProjectContext>()
|
||||
|
||||
function debugLog(message: string): void {
|
||||
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.log(`[antigravity-project] ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const CODE_ASSIST_METADATA = {
|
||||
ideType: "IDE_UNSPECIFIED",
|
||||
platform: "PLATFORM_UNSPECIFIED",
|
||||
pluginType: "GEMINI",
|
||||
} as const
|
||||
|
||||
function extractProjectId(
|
||||
project: string | { id: string } | undefined
|
||||
): string | undefined {
|
||||
if (!project) return undefined
|
||||
if (typeof project === "string") {
|
||||
const trimmed = project.trim()
|
||||
return trimmed || undefined
|
||||
}
|
||||
if (typeof project === "object" && "id" in project) {
|
||||
const id = project.id
|
||||
if (typeof id === "string") {
|
||||
const trimmed = id.trim()
|
||||
return trimmed || undefined
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getDefaultTierId(allowedTiers?: AntigravityUserTier[]): string | undefined {
|
||||
if (!allowedTiers || allowedTiers.length === 0) return undefined
|
||||
for (const tier of allowedTiers) {
|
||||
if (tier?.isDefault) return tier.id
|
||||
}
|
||||
return allowedTiers[0]?.id
|
||||
}
|
||||
|
||||
function isFreeTier(tierId: string | undefined): boolean {
|
||||
if (!tierId) return true // No tier = assume free tier (default behavior)
|
||||
const lower = tierId.toLowerCase()
|
||||
return lower === "free" || lower === "free-tier" || lower.startsWith("free")
|
||||
}
|
||||
|
||||
function wait(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async function callLoadCodeAssistAPI(
|
||||
accessToken: string,
|
||||
projectId?: string
|
||||
): Promise<AntigravityLoadCodeAssistResponse | null> {
|
||||
const metadata: Record<string, string> = { ...CODE_ASSIST_METADATA }
|
||||
if (projectId) metadata.duetProject = projectId
|
||||
|
||||
const requestBody: Record<string, unknown> = { metadata }
|
||||
if (projectId) requestBody.cloudaicompanionProject = projectId
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": ANTIGRAVITY_HEADERS["User-Agent"],
|
||||
"X-Goog-Api-Client": ANTIGRAVITY_HEADERS["X-Goog-Api-Client"],
|
||||
"Client-Metadata": ANTIGRAVITY_HEADERS["Client-Metadata"],
|
||||
}
|
||||
|
||||
for (const baseEndpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) {
|
||||
const url = `${baseEndpoint}/${ANTIGRAVITY_API_VERSION}:loadCodeAssist`
|
||||
debugLog(`[loadCodeAssist] Trying: ${url}`)
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
if (!response.ok) {
|
||||
debugLog(`[loadCodeAssist] Failed: ${response.status} ${response.statusText}`)
|
||||
continue
|
||||
}
|
||||
const data = (await response.json()) as AntigravityLoadCodeAssistResponse
|
||||
debugLog(`[loadCodeAssist] Success: ${JSON.stringify(data)}`)
|
||||
return data
|
||||
} catch (err) {
|
||||
debugLog(`[loadCodeAssist] Error: ${err}`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
debugLog(`[loadCodeAssist] All endpoints failed`)
|
||||
return null
|
||||
}
|
||||
|
||||
async function onboardManagedProject(
|
||||
accessToken: string,
|
||||
tierId: string,
|
||||
projectId?: string,
|
||||
attempts = 10,
|
||||
delayMs = 5000
|
||||
): Promise<string | undefined> {
|
||||
debugLog(`[onboardUser] Starting with tierId=${tierId}, projectId=${projectId || "none"}`)
|
||||
|
||||
const metadata: Record<string, string> = { ...CODE_ASSIST_METADATA }
|
||||
if (projectId) metadata.duetProject = projectId
|
||||
|
||||
const requestBody: Record<string, unknown> = { tierId, metadata }
|
||||
if (!isFreeTier(tierId)) {
|
||||
if (!projectId) {
|
||||
debugLog(`[onboardUser] Non-FREE tier requires projectId, returning undefined`)
|
||||
return undefined
|
||||
}
|
||||
requestBody.cloudaicompanionProject = projectId
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": ANTIGRAVITY_HEADERS["User-Agent"],
|
||||
"X-Goog-Api-Client": ANTIGRAVITY_HEADERS["X-Goog-Api-Client"],
|
||||
"Client-Metadata": ANTIGRAVITY_HEADERS["Client-Metadata"],
|
||||
}
|
||||
|
||||
debugLog(`[onboardUser] Request body: ${JSON.stringify(requestBody)}`)
|
||||
|
||||
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||
debugLog(`[onboardUser] Attempt ${attempt + 1}/${attempts}`)
|
||||
for (const baseEndpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) {
|
||||
const url = `${baseEndpoint}/${ANTIGRAVITY_API_VERSION}:onboardUser`
|
||||
debugLog(`[onboardUser] Trying: ${url}`)
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => "")
|
||||
debugLog(`[onboardUser] Failed: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as AntigravityOnboardUserPayload
|
||||
debugLog(`[onboardUser] Response: ${JSON.stringify(payload)}`)
|
||||
const managedProjectId = payload.response?.cloudaicompanionProject?.id
|
||||
if (payload.done && managedProjectId) {
|
||||
debugLog(`[onboardUser] Success! Got managed project ID: ${managedProjectId}`)
|
||||
return managedProjectId
|
||||
}
|
||||
if (payload.done && projectId) {
|
||||
debugLog(`[onboardUser] Done but no managed ID, using original: ${projectId}`)
|
||||
return projectId
|
||||
}
|
||||
debugLog(`[onboardUser] Not done yet, payload.done=${payload.done}`)
|
||||
} catch (err) {
|
||||
debugLog(`[onboardUser] Error: ${err}`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (attempt < attempts - 1) {
|
||||
debugLog(`[onboardUser] Waiting ${delayMs}ms before next attempt...`)
|
||||
await wait(delayMs)
|
||||
}
|
||||
}
|
||||
debugLog(`[onboardUser] All attempts exhausted, returning undefined`)
|
||||
return undefined
|
||||
}
|
||||
|
||||
export async function fetchProjectContext(
|
||||
accessToken: string
|
||||
): Promise<AntigravityProjectContext> {
|
||||
debugLog(`[fetchProjectContext] Starting...`)
|
||||
|
||||
const cached = projectContextCache.get(accessToken)
|
||||
if (cached) {
|
||||
debugLog(`[fetchProjectContext] Returning cached result: ${JSON.stringify(cached)}`)
|
||||
return cached
|
||||
}
|
||||
|
||||
const loadPayload = await callLoadCodeAssistAPI(accessToken)
|
||||
|
||||
// If loadCodeAssist returns a project ID, use it directly
|
||||
if (loadPayload?.cloudaicompanionProject) {
|
||||
const projectId = extractProjectId(loadPayload.cloudaicompanionProject)
|
||||
debugLog(`[fetchProjectContext] loadCodeAssist returned project: ${projectId}`)
|
||||
if (projectId) {
|
||||
const result: AntigravityProjectContext = { cloudaicompanionProject: projectId }
|
||||
projectContextCache.set(accessToken, result)
|
||||
debugLog(`[fetchProjectContext] Using loadCodeAssist project ID: ${projectId}`)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// No project ID from loadCodeAssist - try with fallback project ID
|
||||
if (!loadPayload) {
|
||||
debugLog(`[fetchProjectContext] loadCodeAssist returned null, trying with fallback project ID`)
|
||||
const fallbackPayload = await callLoadCodeAssistAPI(accessToken, ANTIGRAVITY_DEFAULT_PROJECT_ID)
|
||||
const fallbackProjectId = extractProjectId(fallbackPayload?.cloudaicompanionProject)
|
||||
if (fallbackProjectId) {
|
||||
const result: AntigravityProjectContext = { cloudaicompanionProject: fallbackProjectId }
|
||||
projectContextCache.set(accessToken, result)
|
||||
debugLog(`[fetchProjectContext] Using fallback project ID: ${fallbackProjectId}`)
|
||||
return result
|
||||
}
|
||||
debugLog(`[fetchProjectContext] Fallback also failed, using default: ${ANTIGRAVITY_DEFAULT_PROJECT_ID}`)
|
||||
return { cloudaicompanionProject: ANTIGRAVITY_DEFAULT_PROJECT_ID }
|
||||
}
|
||||
|
||||
const currentTierId = loadPayload.currentTier?.id
|
||||
debugLog(`[fetchProjectContext] currentTier: ${currentTierId}, allowedTiers: ${JSON.stringify(loadPayload.allowedTiers)}`)
|
||||
|
||||
if (currentTierId && !isFreeTier(currentTierId)) {
|
||||
// PAID tier - still use fallback if no project provided
|
||||
debugLog(`[fetchProjectContext] PAID tier detected (${currentTierId}), using fallback: ${ANTIGRAVITY_DEFAULT_PROJECT_ID}`)
|
||||
return { cloudaicompanionProject: ANTIGRAVITY_DEFAULT_PROJECT_ID }
|
||||
}
|
||||
|
||||
const defaultTierId = getDefaultTierId(loadPayload.allowedTiers)
|
||||
const tierId = defaultTierId ?? "free-tier"
|
||||
debugLog(`[fetchProjectContext] Resolved tierId: ${tierId}`)
|
||||
|
||||
if (!isFreeTier(tierId)) {
|
||||
debugLog(`[fetchProjectContext] Non-FREE tier (${tierId}) without project, using fallback: ${ANTIGRAVITY_DEFAULT_PROJECT_ID}`)
|
||||
return { cloudaicompanionProject: ANTIGRAVITY_DEFAULT_PROJECT_ID }
|
||||
}
|
||||
|
||||
// FREE tier - onboard to get server-assigned managed project ID
|
||||
debugLog(`[fetchProjectContext] FREE tier detected (${tierId}), calling onboardUser...`)
|
||||
const managedProjectId = await onboardManagedProject(accessToken, tierId)
|
||||
if (managedProjectId) {
|
||||
const result: AntigravityProjectContext = {
|
||||
cloudaicompanionProject: managedProjectId,
|
||||
managedProjectId,
|
||||
}
|
||||
projectContextCache.set(accessToken, result)
|
||||
debugLog(`[fetchProjectContext] Got managed project ID: ${managedProjectId}`)
|
||||
return result
|
||||
}
|
||||
|
||||
debugLog(`[fetchProjectContext] Failed to get managed project ID, using fallback: ${ANTIGRAVITY_DEFAULT_PROJECT_ID}`)
|
||||
return { cloudaicompanionProject: ANTIGRAVITY_DEFAULT_PROJECT_ID }
|
||||
}
|
||||
|
||||
export function clearProjectContextCache(accessToken?: string): void {
|
||||
if (accessToken) {
|
||||
projectContextCache.delete(accessToken)
|
||||
} else {
|
||||
projectContextCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export function invalidateProjectContextByRefreshToken(_refreshToken: string): void {
|
||||
projectContextCache.clear()
|
||||
debugLog(`[invalidateProjectContextByRefreshToken] Cleared all project context cache due to refresh token invalidation`)
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
/**
|
||||
* Antigravity request transformer.
|
||||
* Transforms OpenAI-format requests to Antigravity format.
|
||||
* Does NOT handle tool normalization (handled by tools.ts in Task 9).
|
||||
*/
|
||||
|
||||
import {
|
||||
ANTIGRAVITY_API_VERSION,
|
||||
ANTIGRAVITY_ENDPOINT_FALLBACKS,
|
||||
ANTIGRAVITY_HEADERS,
|
||||
SKIP_THOUGHT_SIGNATURE_VALIDATOR,
|
||||
} from "./constants"
|
||||
import type { AntigravityRequestBody } from "./types"
|
||||
|
||||
/**
|
||||
* Result of request transformation including URL, headers, and body.
|
||||
*/
|
||||
export interface TransformedRequest {
|
||||
/** Transformed URL for Antigravity API */
|
||||
url: string
|
||||
/** Request headers including Authorization and Antigravity-specific headers */
|
||||
headers: Record<string, string>
|
||||
/** Transformed request body in Antigravity format */
|
||||
body: AntigravityRequestBody
|
||||
/** Whether this is a streaming request */
|
||||
streaming: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Antigravity-specific request headers.
|
||||
* Includes Authorization, User-Agent, X-Goog-Api-Client, and Client-Metadata.
|
||||
*
|
||||
* @param accessToken - OAuth access token for Authorization header
|
||||
* @returns Headers object with all required Antigravity headers
|
||||
*/
|
||||
export function buildRequestHeaders(accessToken: string): Record<string, string> {
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": ANTIGRAVITY_HEADERS["User-Agent"],
|
||||
"X-Goog-Api-Client": ANTIGRAVITY_HEADERS["X-Goog-Api-Client"],
|
||||
"Client-Metadata": ANTIGRAVITY_HEADERS["Client-Metadata"],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract model name from request body.
|
||||
* OpenAI-format requests include model in the body.
|
||||
*
|
||||
* @param body - Request body that may contain a model field
|
||||
* @returns Model name or undefined if not found
|
||||
*/
|
||||
export function extractModelFromBody(
|
||||
body: Record<string, unknown>
|
||||
): string | undefined {
|
||||
const model = body.model
|
||||
if (typeof model === "string" && model.trim()) {
|
||||
return model.trim()
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract model name from URL path.
|
||||
* Handles Google Generative Language API format: /models/{model}:{action}
|
||||
*
|
||||
* @param url - Request URL to parse
|
||||
* @returns Model name or undefined if not found
|
||||
*/
|
||||
export function extractModelFromUrl(url: string): string | undefined {
|
||||
// Match Google's API format: /models/gemini-3-pro:generateContent
|
||||
const match = url.match(/\/models\/([^:]+):/)
|
||||
if (match && match[1]) {
|
||||
return match[1]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the action type from the URL path.
|
||||
* E.g., generateContent, streamGenerateContent
|
||||
*
|
||||
* @param url - Request URL to parse
|
||||
* @returns Action name or undefined if not found
|
||||
*/
|
||||
export function extractActionFromUrl(url: string): string | undefined {
|
||||
// Match Google's API format: /models/gemini-3-pro:generateContent
|
||||
const match = url.match(/\/models\/[^:]+:(\w+)/)
|
||||
if (match && match[1]) {
|
||||
return match[1]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a URL is targeting Google's Generative Language API.
|
||||
*
|
||||
* @param url - URL to check
|
||||
* @returns true if this is a Google Generative Language API request
|
||||
*/
|
||||
export function isGenerativeLanguageRequest(url: string): boolean {
|
||||
return url.includes("generativelanguage.googleapis.com")
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Antigravity API URL for the given action.
|
||||
*
|
||||
* @param baseEndpoint - Base Antigravity endpoint URL (from fallbacks)
|
||||
* @param action - API action (e.g., generateContent, streamGenerateContent)
|
||||
* @param streaming - Whether to append SSE query parameter
|
||||
* @returns Formatted Antigravity API URL
|
||||
*/
|
||||
export function buildAntigravityUrl(
|
||||
baseEndpoint: string,
|
||||
action: string,
|
||||
streaming: boolean
|
||||
): string {
|
||||
const query = streaming ? "?alt=sse" : ""
|
||||
return `${baseEndpoint}/${ANTIGRAVITY_API_VERSION}:${action}${query}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first available Antigravity endpoint.
|
||||
* Can be used with fallback logic in fetch.ts.
|
||||
*
|
||||
* @returns Default (first) Antigravity endpoint
|
||||
*/
|
||||
export function getDefaultEndpoint(): string {
|
||||
return ANTIGRAVITY_ENDPOINT_FALLBACKS[0]
|
||||
}
|
||||
|
||||
function generateRequestId(): string {
|
||||
return `agent-${crypto.randomUUID()}`
|
||||
}
|
||||
|
||||
export function wrapRequestBody(
|
||||
body: Record<string, unknown>,
|
||||
projectId: string,
|
||||
modelName: string,
|
||||
sessionId: string
|
||||
): AntigravityRequestBody {
|
||||
const requestPayload = { ...body }
|
||||
delete requestPayload.model
|
||||
|
||||
return {
|
||||
project: projectId,
|
||||
model: modelName,
|
||||
userAgent: "antigravity",
|
||||
requestId: generateRequestId(),
|
||||
request: {
|
||||
...requestPayload,
|
||||
sessionId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface ContentPart {
|
||||
functionCall?: Record<string, unknown>
|
||||
thoughtSignature?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface ContentBlock {
|
||||
role?: string
|
||||
parts?: ContentPart[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
function debugLog(message: string): void {
|
||||
if (process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.log(`[antigravity-request] ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function injectThoughtSignatureIntoFunctionCalls(
|
||||
body: Record<string, unknown>,
|
||||
signature: string | undefined
|
||||
): Record<string, unknown> {
|
||||
// Always use skip validator as fallback (CLIProxyAPI approach)
|
||||
const effectiveSignature = signature || SKIP_THOUGHT_SIGNATURE_VALIDATOR
|
||||
debugLog(`[TSIG][INJECT] signature=${effectiveSignature.substring(0, 30)}... (${signature ? "provided" : "default"})`)
|
||||
debugLog(`[TSIG][INJECT] body keys: ${Object.keys(body).join(", ")}`)
|
||||
|
||||
const contents = body.contents as ContentBlock[] | undefined
|
||||
if (!contents || !Array.isArray(contents)) {
|
||||
debugLog(`[TSIG][INJECT] No contents array! Has messages: ${!!body.messages}`)
|
||||
return body
|
||||
}
|
||||
|
||||
debugLog(`[TSIG][INJECT] Found ${contents.length} content blocks`)
|
||||
let injectedCount = 0
|
||||
const modifiedContents = contents.map((content) => {
|
||||
if (!content.parts || !Array.isArray(content.parts)) {
|
||||
return content
|
||||
}
|
||||
|
||||
const modifiedParts = content.parts.map((part) => {
|
||||
if (part.functionCall && !part.thoughtSignature) {
|
||||
injectedCount++
|
||||
return {
|
||||
...part,
|
||||
thoughtSignature: effectiveSignature,
|
||||
}
|
||||
}
|
||||
return part
|
||||
})
|
||||
|
||||
return { ...content, parts: modifiedParts }
|
||||
})
|
||||
|
||||
debugLog(`[TSIG][INJECT] injected signature into ${injectedCount} functionCall(s)`)
|
||||
return { ...body, contents: modifiedContents }
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if request is for streaming.
|
||||
* Checks both action name and request body for stream flag.
|
||||
*
|
||||
* @param url - Request URL
|
||||
* @param body - Request body
|
||||
* @returns true if streaming is requested
|
||||
*/
|
||||
export function isStreamingRequest(
|
||||
url: string,
|
||||
body: Record<string, unknown>
|
||||
): boolean {
|
||||
// Check URL action
|
||||
const action = extractActionFromUrl(url)
|
||||
if (action === "streamGenerateContent") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check body for stream flag
|
||||
if (body.stream === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export interface TransformRequestOptions {
|
||||
url: string
|
||||
body: Record<string, unknown>
|
||||
accessToken: string
|
||||
projectId: string
|
||||
sessionId: string
|
||||
modelName?: string
|
||||
endpointOverride?: string
|
||||
thoughtSignature?: string
|
||||
}
|
||||
|
||||
export function transformRequest(options: TransformRequestOptions): TransformedRequest {
|
||||
const {
|
||||
url,
|
||||
body,
|
||||
accessToken,
|
||||
projectId,
|
||||
sessionId,
|
||||
modelName,
|
||||
endpointOverride,
|
||||
thoughtSignature,
|
||||
} = options
|
||||
|
||||
const effectiveModel =
|
||||
modelName || extractModelFromBody(body) || extractModelFromUrl(url) || "gemini-3-pro-high"
|
||||
|
||||
const streaming = isStreamingRequest(url, body)
|
||||
const action = streaming ? "streamGenerateContent" : "generateContent"
|
||||
|
||||
const endpoint = endpointOverride || getDefaultEndpoint()
|
||||
const transformedUrl = buildAntigravityUrl(endpoint, action, streaming)
|
||||
|
||||
const headers = buildRequestHeaders(accessToken)
|
||||
if (streaming) {
|
||||
headers["Accept"] = "text/event-stream"
|
||||
}
|
||||
|
||||
const bodyWithSignature = injectThoughtSignatureIntoFunctionCalls(body, thoughtSignature)
|
||||
const wrappedBody = wrapRequestBody(bodyWithSignature, projectId, effectiveModel, sessionId)
|
||||
|
||||
return {
|
||||
url: transformedUrl,
|
||||
headers,
|
||||
body: wrappedBody,
|
||||
streaming,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare request headers for streaming responses.
|
||||
* Adds Accept header for SSE format.
|
||||
*
|
||||
* @param headers - Existing headers object
|
||||
* @returns Headers with streaming support
|
||||
*/
|
||||
export function addStreamingHeaders(
|
||||
headers: Record<string, string>
|
||||
): Record<string, string> {
|
||||
return {
|
||||
...headers,
|
||||
Accept: "text/event-stream",
|
||||
}
|
||||
}
|
||||
@@ -1,598 +0,0 @@
|
||||
/**
|
||||
* Antigravity Response Handler
|
||||
* Transforms Antigravity/Gemini API responses to OpenAI-compatible format
|
||||
*
|
||||
* Key responsibilities:
|
||||
* - Non-streaming response transformation
|
||||
* - SSE streaming response transformation (buffered - see transformStreamingResponse)
|
||||
* - Error response handling with retry-after extraction
|
||||
* - Usage metadata extraction from x-antigravity-* headers
|
||||
*/
|
||||
|
||||
import type { AntigravityError, AntigravityUsage } from "./types"
|
||||
|
||||
/**
|
||||
* Usage metadata extracted from Antigravity response headers
|
||||
*/
|
||||
export interface AntigravityUsageMetadata {
|
||||
cachedContentTokenCount?: number
|
||||
totalTokenCount?: number
|
||||
promptTokenCount?: number
|
||||
candidatesTokenCount?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform result with response and metadata
|
||||
*/
|
||||
export interface TransformResult {
|
||||
response: Response
|
||||
usage?: AntigravityUsageMetadata
|
||||
retryAfterMs?: number
|
||||
error?: AntigravityError
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract usage metadata from Antigravity response headers
|
||||
*
|
||||
* Antigravity sets these headers:
|
||||
* - x-antigravity-cached-content-token-count
|
||||
* - x-antigravity-total-token-count
|
||||
* - x-antigravity-prompt-token-count
|
||||
* - x-antigravity-candidates-token-count
|
||||
*
|
||||
* @param headers - Response headers
|
||||
* @returns Usage metadata if found
|
||||
*/
|
||||
export function extractUsageFromHeaders(headers: Headers): AntigravityUsageMetadata | undefined {
|
||||
const cached = headers.get("x-antigravity-cached-content-token-count")
|
||||
const total = headers.get("x-antigravity-total-token-count")
|
||||
const prompt = headers.get("x-antigravity-prompt-token-count")
|
||||
const candidates = headers.get("x-antigravity-candidates-token-count")
|
||||
|
||||
// Return undefined if no usage headers found
|
||||
if (!cached && !total && !prompt && !candidates) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const usage: AntigravityUsageMetadata = {}
|
||||
|
||||
if (cached) {
|
||||
const parsed = parseInt(cached, 10)
|
||||
if (!isNaN(parsed)) {
|
||||
usage.cachedContentTokenCount = parsed
|
||||
}
|
||||
}
|
||||
|
||||
if (total) {
|
||||
const parsed = parseInt(total, 10)
|
||||
if (!isNaN(parsed)) {
|
||||
usage.totalTokenCount = parsed
|
||||
}
|
||||
}
|
||||
|
||||
if (prompt) {
|
||||
const parsed = parseInt(prompt, 10)
|
||||
if (!isNaN(parsed)) {
|
||||
usage.promptTokenCount = parsed
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates) {
|
||||
const parsed = parseInt(candidates, 10)
|
||||
if (!isNaN(parsed)) {
|
||||
usage.candidatesTokenCount = parsed
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(usage).length > 0 ? usage : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract retry-after value from error response
|
||||
*
|
||||
* Antigravity returns retry info in error.details array:
|
||||
* {
|
||||
* error: {
|
||||
* details: [{
|
||||
* "@type": "type.googleapis.com/google.rpc.RetryInfo",
|
||||
* "retryDelay": "5.123s"
|
||||
* }]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Also checks standard Retry-After header.
|
||||
*
|
||||
* @param response - Response object (for headers)
|
||||
* @param errorBody - Parsed error body (optional)
|
||||
* @returns Retry after value in milliseconds, or undefined
|
||||
*/
|
||||
export function extractRetryAfterMs(
|
||||
response: Response,
|
||||
errorBody?: Record<string, unknown>,
|
||||
): number | undefined {
|
||||
// First, check standard Retry-After header
|
||||
const retryAfterHeader = response.headers.get("Retry-After")
|
||||
if (retryAfterHeader) {
|
||||
const seconds = parseFloat(retryAfterHeader)
|
||||
if (!isNaN(seconds) && seconds > 0) {
|
||||
return Math.ceil(seconds * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// Check retry-after-ms header (set by some transformers)
|
||||
const retryAfterMsHeader = response.headers.get("retry-after-ms")
|
||||
if (retryAfterMsHeader) {
|
||||
const ms = parseInt(retryAfterMsHeader, 10)
|
||||
if (!isNaN(ms) && ms > 0) {
|
||||
return ms
|
||||
}
|
||||
}
|
||||
|
||||
// Check error body for RetryInfo
|
||||
if (!errorBody) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const error = errorBody.error as Record<string, unknown> | undefined
|
||||
if (!error?.details || !Array.isArray(error.details)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const retryInfo = (error.details as Array<Record<string, unknown>>).find(
|
||||
(detail) => detail["@type"] === "type.googleapis.com/google.rpc.RetryInfo",
|
||||
)
|
||||
|
||||
if (!retryInfo?.retryDelay || typeof retryInfo.retryDelay !== "string") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Parse retryDelay format: "5.123s"
|
||||
const match = retryInfo.retryDelay.match(/^([\d.]+)s$/)
|
||||
if (match?.[1]) {
|
||||
const seconds = parseFloat(match[1])
|
||||
if (!isNaN(seconds) && seconds > 0) {
|
||||
return Math.ceil(seconds * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse error response body and extract useful details
|
||||
*
|
||||
* @param text - Raw response text
|
||||
* @returns Parsed error or undefined
|
||||
*/
|
||||
export function parseErrorBody(text: string): AntigravityError | undefined {
|
||||
try {
|
||||
const parsed = JSON.parse(text) as Record<string, unknown>
|
||||
|
||||
// Handle error wrapper
|
||||
if (parsed.error && typeof parsed.error === "object") {
|
||||
const errorObj = parsed.error as Record<string, unknown>
|
||||
return {
|
||||
message: String(errorObj.message || "Unknown error"),
|
||||
type: errorObj.type ? String(errorObj.type) : undefined,
|
||||
code: errorObj.code as string | number | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle direct error message
|
||||
if (parsed.message && typeof parsed.message === "string") {
|
||||
return {
|
||||
message: parsed.message,
|
||||
type: parsed.type ? String(parsed.type) : undefined,
|
||||
code: parsed.code as string | number | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
} catch {
|
||||
// If not valid JSON, return generic error
|
||||
return {
|
||||
message: text || "Unknown error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a non-streaming Antigravity response to OpenAI-compatible format
|
||||
*
|
||||
* For non-streaming responses:
|
||||
* - Parses the response body
|
||||
* - Unwraps the `response` field if present (Antigravity wraps responses)
|
||||
* - Extracts usage metadata from headers
|
||||
* - Handles error responses
|
||||
*
|
||||
* Note: Does NOT handle thinking block extraction (Task 10)
|
||||
* Note: Does NOT handle tool normalization (Task 9)
|
||||
*
|
||||
* @param response - Fetch Response object
|
||||
* @returns TransformResult with transformed response and metadata
|
||||
*/
|
||||
export async function transformResponse(response: Response): Promise<TransformResult> {
|
||||
const headers = new Headers(response.headers)
|
||||
const usage = extractUsageFromHeaders(headers)
|
||||
|
||||
// Handle error responses
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
const error = parseErrorBody(text)
|
||||
const retryAfterMs = extractRetryAfterMs(response, error ? { error } : undefined)
|
||||
|
||||
// Parse to get full error body for retry-after extraction
|
||||
let errorBody: Record<string, unknown> | undefined
|
||||
try {
|
||||
errorBody = JSON.parse(text) as Record<string, unknown>
|
||||
} catch {
|
||||
errorBody = { error: { message: text } }
|
||||
}
|
||||
|
||||
const retryMs = extractRetryAfterMs(response, errorBody) ?? retryAfterMs
|
||||
|
||||
// Set retry headers if found
|
||||
if (retryMs) {
|
||||
headers.set("Retry-After", String(Math.ceil(retryMs / 1000)))
|
||||
headers.set("retry-after-ms", String(retryMs))
|
||||
}
|
||||
|
||||
return {
|
||||
response: new Response(text, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
}),
|
||||
usage,
|
||||
retryAfterMs: retryMs,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle successful response
|
||||
const contentType = response.headers.get("content-type") ?? ""
|
||||
const isJson = contentType.includes("application/json")
|
||||
|
||||
if (!isJson) {
|
||||
// Return non-JSON responses as-is
|
||||
return { response, usage }
|
||||
}
|
||||
|
||||
try {
|
||||
const text = await response.text()
|
||||
const parsed = JSON.parse(text) as Record<string, unknown>
|
||||
|
||||
// Antigravity wraps response in { response: { ... } }
|
||||
// Unwrap if present
|
||||
let transformedBody: unknown = parsed
|
||||
if (parsed.response !== undefined) {
|
||||
transformedBody = parsed.response
|
||||
}
|
||||
|
||||
return {
|
||||
response: new Response(JSON.stringify(transformedBody), {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
}),
|
||||
usage,
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, return original response
|
||||
return { response, usage }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a single SSE data line
|
||||
*
|
||||
* Antigravity SSE format:
|
||||
* data: { "response": { ... actual data ... } }
|
||||
*
|
||||
* OpenAI SSE format:
|
||||
* data: { ... actual data ... }
|
||||
*
|
||||
* @param line - SSE data line
|
||||
* @returns Transformed line
|
||||
*/
|
||||
function transformSseLine(line: string): string {
|
||||
if (!line.startsWith("data:")) {
|
||||
return line
|
||||
}
|
||||
|
||||
const json = line.slice(5).trim()
|
||||
if (!json || json === "[DONE]") {
|
||||
return line
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(json) as Record<string, unknown>
|
||||
|
||||
// Unwrap { response: { ... } } wrapper
|
||||
if (parsed.response !== undefined) {
|
||||
return `data: ${JSON.stringify(parsed.response)}`
|
||||
}
|
||||
|
||||
return line
|
||||
} catch {
|
||||
// If parsing fails, return original line
|
||||
return line
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform SSE streaming payload
|
||||
*
|
||||
* Processes each line in the SSE stream:
|
||||
* - Unwraps { response: { ... } } wrapper from data lines
|
||||
* - Preserves other SSE control lines (event:, id:, retry:, empty lines)
|
||||
*
|
||||
* Note: Does NOT extract thinking blocks (Task 10)
|
||||
*
|
||||
* @param payload - Raw SSE payload text
|
||||
* @returns Transformed SSE payload
|
||||
*/
|
||||
export function transformStreamingPayload(payload: string): string {
|
||||
return payload
|
||||
.split("\n")
|
||||
.map(transformSseLine)
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function createSseTransformStream(): TransformStream<Uint8Array, Uint8Array> {
|
||||
const decoder = new TextDecoder()
|
||||
const encoder = new TextEncoder()
|
||||
let buffer = ""
|
||||
|
||||
return new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
buffer += decoder.decode(chunk, { stream: true })
|
||||
const lines = buffer.split("\n")
|
||||
buffer = lines.pop() || ""
|
||||
|
||||
for (const line of lines) {
|
||||
const transformed = transformSseLine(line)
|
||||
controller.enqueue(encoder.encode(transformed + "\n"))
|
||||
}
|
||||
},
|
||||
flush(controller) {
|
||||
if (buffer) {
|
||||
const transformed = transformSseLine(buffer)
|
||||
controller.enqueue(encoder.encode(transformed))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a streaming SSE response from Antigravity to OpenAI format.
|
||||
*
|
||||
* Uses TransformStream to process SSE chunks incrementally as they arrive.
|
||||
* Each line is transformed immediately and yielded to the client.
|
||||
*
|
||||
* @param response - The SSE response from Antigravity API
|
||||
* @returns TransformResult with transformed streaming response
|
||||
*/
|
||||
export async function transformStreamingResponse(response: Response): Promise<TransformResult> {
|
||||
const headers = new Headers(response.headers)
|
||||
const usage = extractUsageFromHeaders(headers)
|
||||
|
||||
// Handle error responses
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
const error = parseErrorBody(text)
|
||||
|
||||
let errorBody: Record<string, unknown> | undefined
|
||||
try {
|
||||
errorBody = JSON.parse(text) as Record<string, unknown>
|
||||
} catch {
|
||||
errorBody = { error: { message: text } }
|
||||
}
|
||||
|
||||
const retryAfterMs = extractRetryAfterMs(response, errorBody)
|
||||
|
||||
if (retryAfterMs) {
|
||||
headers.set("Retry-After", String(Math.ceil(retryAfterMs / 1000)))
|
||||
headers.set("retry-after-ms", String(retryAfterMs))
|
||||
}
|
||||
|
||||
return {
|
||||
response: new Response(text, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
}),
|
||||
usage,
|
||||
retryAfterMs,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
// Check content type
|
||||
const contentType = response.headers.get("content-type") ?? ""
|
||||
const isEventStream =
|
||||
contentType.includes("text/event-stream") || response.url.includes("alt=sse")
|
||||
|
||||
if (!isEventStream) {
|
||||
// Not SSE, delegate to non-streaming transform
|
||||
// Clone response since we need to read it
|
||||
const text = await response.text()
|
||||
try {
|
||||
const parsed = JSON.parse(text) as Record<string, unknown>
|
||||
let transformedBody: unknown = parsed
|
||||
if (parsed.response !== undefined) {
|
||||
transformedBody = parsed.response
|
||||
}
|
||||
return {
|
||||
response: new Response(JSON.stringify(transformedBody), {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
}),
|
||||
usage,
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
response: new Response(text, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
}),
|
||||
usage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
return { response, usage }
|
||||
}
|
||||
|
||||
headers.delete("content-length")
|
||||
headers.delete("content-encoding")
|
||||
headers.set("content-type", "text/event-stream; charset=utf-8")
|
||||
|
||||
const transformStream = createSseTransformStream()
|
||||
const transformedBody = response.body.pipeThrough(transformStream)
|
||||
|
||||
return {
|
||||
response: new Response(transformedBody, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
}),
|
||||
usage,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if response is a streaming SSE response
|
||||
*
|
||||
* @param response - Fetch Response object
|
||||
* @returns True if response is SSE stream
|
||||
*/
|
||||
export function isStreamingResponse(response: Response): boolean {
|
||||
const contentType = response.headers.get("content-type") ?? ""
|
||||
return contentType.includes("text/event-stream") || response.url.includes("alt=sse")
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract thought signature from SSE payload text
|
||||
*
|
||||
* Looks for thoughtSignature in SSE events:
|
||||
* data: { "response": { "candidates": [{ "content": { "parts": [{ "thoughtSignature": "..." }] } }] } }
|
||||
*
|
||||
* Returns the last found signature (most recent in the stream).
|
||||
*
|
||||
* @param payload - SSE payload text
|
||||
* @returns Last thought signature if found
|
||||
*/
|
||||
export function extractSignatureFromSsePayload(payload: string): string | undefined {
|
||||
const lines = payload.split("\n")
|
||||
let lastSignature: string | undefined
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data:")) {
|
||||
continue
|
||||
}
|
||||
|
||||
const json = line.slice(5).trim()
|
||||
if (!json || json === "[DONE]") {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(json) as Record<string, unknown>
|
||||
|
||||
// Check in response wrapper (Antigravity format)
|
||||
const response = (parsed.response || parsed) as Record<string, unknown>
|
||||
const candidates = response.candidates as Array<Record<string, unknown>> | undefined
|
||||
|
||||
if (candidates && Array.isArray(candidates)) {
|
||||
for (const candidate of candidates) {
|
||||
const content = candidate.content as Record<string, unknown> | undefined
|
||||
const parts = content?.parts as Array<Record<string, unknown>> | undefined
|
||||
|
||||
if (parts && Array.isArray(parts)) {
|
||||
for (const part of parts) {
|
||||
const sig = (part.thoughtSignature || part.thought_signature) as string | undefined
|
||||
if (sig && typeof sig === "string") {
|
||||
lastSignature = sig
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Continue to next line if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
return lastSignature
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract usage from SSE payload text
|
||||
*
|
||||
* Looks for usageMetadata in SSE events:
|
||||
* data: { "usageMetadata": { ... } }
|
||||
*
|
||||
* @param payload - SSE payload text
|
||||
* @returns Usage if found
|
||||
*/
|
||||
export function extractUsageFromSsePayload(payload: string): AntigravityUsage | undefined {
|
||||
const lines = payload.split("\n")
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data:")) {
|
||||
continue
|
||||
}
|
||||
|
||||
const json = line.slice(5).trim()
|
||||
if (!json || json === "[DONE]") {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(json) as Record<string, unknown>
|
||||
|
||||
// Check for usageMetadata at top level
|
||||
if (parsed.usageMetadata && typeof parsed.usageMetadata === "object") {
|
||||
const meta = parsed.usageMetadata as Record<string, unknown>
|
||||
return {
|
||||
prompt_tokens: typeof meta.promptTokenCount === "number" ? meta.promptTokenCount : 0,
|
||||
completion_tokens:
|
||||
typeof meta.candidatesTokenCount === "number" ? meta.candidatesTokenCount : 0,
|
||||
total_tokens: typeof meta.totalTokenCount === "number" ? meta.totalTokenCount : 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Check for usage in response wrapper
|
||||
if (parsed.response && typeof parsed.response === "object") {
|
||||
const resp = parsed.response as Record<string, unknown>
|
||||
if (resp.usageMetadata && typeof resp.usageMetadata === "object") {
|
||||
const meta = resp.usageMetadata as Record<string, unknown>
|
||||
return {
|
||||
prompt_tokens: typeof meta.promptTokenCount === "number" ? meta.promptTokenCount : 0,
|
||||
completion_tokens:
|
||||
typeof meta.candidatesTokenCount === "number" ? meta.candidatesTokenCount : 0,
|
||||
total_tokens: typeof meta.totalTokenCount === "number" ? meta.totalTokenCount : 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for standard OpenAI-style usage
|
||||
if (parsed.usage && typeof parsed.usage === "object") {
|
||||
const u = parsed.usage as Record<string, unknown>
|
||||
return {
|
||||
prompt_tokens: typeof u.prompt_tokens === "number" ? u.prompt_tokens : 0,
|
||||
completion_tokens: typeof u.completion_tokens === "number" ? u.completion_tokens : 0,
|
||||
total_tokens: typeof u.total_tokens === "number" ? u.total_tokens : 0,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Continue to next line if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
@@ -1,571 +0,0 @@
|
||||
/**
|
||||
* Antigravity Thinking Block Handler (Gemini only)
|
||||
*
|
||||
* Handles extraction and transformation of thinking/reasoning blocks
|
||||
* from Gemini responses. Thinking blocks contain the model's internal
|
||||
* reasoning process, available in `-high` model variants.
|
||||
*
|
||||
* Key responsibilities:
|
||||
* - Extract thinking blocks from Gemini response format
|
||||
* - Detect thinking-capable model variants (`-high` suffix)
|
||||
* - Format thinking blocks for OpenAI-compatible output
|
||||
*
|
||||
* Note: This is Gemini-only. Claude models are NOT handled by Antigravity.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a single thinking/reasoning block extracted from Gemini response
|
||||
*/
|
||||
export interface ThinkingBlock {
|
||||
/** The thinking/reasoning text content */
|
||||
text: string
|
||||
/** Optional signature for signed thinking blocks (required for multi-turn) */
|
||||
signature?: string
|
||||
/** Index of the thinking block in sequence */
|
||||
index?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw part structure from Gemini response candidates
|
||||
*/
|
||||
export interface GeminiPart {
|
||||
/** Text content of the part */
|
||||
text?: string
|
||||
/** Whether this part is a thinking/reasoning block */
|
||||
thought?: boolean
|
||||
/** Signature for signed thinking blocks */
|
||||
thoughtSignature?: string
|
||||
/** Type field for Anthropic-style format */
|
||||
type?: string
|
||||
/** Signature field for Anthropic-style format */
|
||||
signature?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini response candidate structure
|
||||
*/
|
||||
export interface GeminiCandidate {
|
||||
/** Content containing parts */
|
||||
content?: {
|
||||
/** Role of the content (e.g., "model", "assistant") */
|
||||
role?: string
|
||||
/** Array of content parts */
|
||||
parts?: GeminiPart[]
|
||||
}
|
||||
/** Index of the candidate */
|
||||
index?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini response structure for thinking block extraction
|
||||
*/
|
||||
export interface GeminiResponse {
|
||||
/** Response ID */
|
||||
id?: string
|
||||
/** Array of response candidates */
|
||||
candidates?: GeminiCandidate[]
|
||||
/** Direct content (some responses use this instead of candidates) */
|
||||
content?: Array<{
|
||||
type?: string
|
||||
text?: string
|
||||
signature?: string
|
||||
}>
|
||||
/** Model used for response */
|
||||
model?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of thinking block extraction
|
||||
*/
|
||||
export interface ThinkingExtractionResult {
|
||||
/** Extracted thinking blocks */
|
||||
thinkingBlocks: ThinkingBlock[]
|
||||
/** Combined thinking text for convenience */
|
||||
combinedThinking: string
|
||||
/** Whether any thinking blocks were found */
|
||||
hasThinking: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Default thinking budget in tokens for thinking-enabled models
|
||||
*/
|
||||
export const DEFAULT_THINKING_BUDGET = 16000
|
||||
|
||||
/**
|
||||
* Check if a model variant should include thinking blocks
|
||||
*
|
||||
* Returns true for model variants with `-high` suffix, which have
|
||||
* extended thinking capability enabled.
|
||||
*
|
||||
* Examples:
|
||||
* - `gemini-3-pro-high` → true
|
||||
* - `gemini-2.5-pro-high` → true
|
||||
* - `gemini-3-pro-preview` → false
|
||||
* - `gemini-2.5-pro` → false
|
||||
*
|
||||
* @param model - Model identifier string
|
||||
* @returns True if model should include thinking blocks
|
||||
*/
|
||||
export function shouldIncludeThinking(model: string): boolean {
|
||||
if (!model || typeof model !== "string") {
|
||||
return false
|
||||
}
|
||||
|
||||
const lowerModel = model.toLowerCase()
|
||||
|
||||
// Check for -high suffix (primary indicator of thinking capability)
|
||||
if (lowerModel.endsWith("-high")) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Also check for explicit thinking in model name
|
||||
if (lowerModel.includes("thinking")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a model is thinking-capable (broader check)
|
||||
*
|
||||
* This is a broader check than shouldIncludeThinking - it detects models
|
||||
* that have thinking capability, even if not explicitly requesting thinking output.
|
||||
*
|
||||
* @param model - Model identifier string
|
||||
* @returns True if model supports thinking/reasoning
|
||||
*/
|
||||
export function isThinkingCapableModel(model: string): boolean {
|
||||
if (!model || typeof model !== "string") {
|
||||
return false
|
||||
}
|
||||
|
||||
const lowerModel = model.toLowerCase()
|
||||
|
||||
return (
|
||||
lowerModel.includes("thinking") ||
|
||||
lowerModel.includes("gemini-3") ||
|
||||
lowerModel.endsWith("-high")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a part is a thinking/reasoning block
|
||||
*
|
||||
* Detects both Gemini-style (thought: true) and Anthropic-style
|
||||
* (type: "thinking" or type: "reasoning") formats.
|
||||
*
|
||||
* @param part - Content part to check
|
||||
* @returns True if part is a thinking block
|
||||
*/
|
||||
function isThinkingPart(part: GeminiPart): boolean {
|
||||
// Gemini-style: thought flag
|
||||
if (part.thought === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Anthropic-style: type field
|
||||
if (part.type === "thinking" || part.type === "reasoning") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a thinking part has a valid signature
|
||||
*
|
||||
* Signatures are required for multi-turn conversations with Claude models.
|
||||
* Gemini uses `thoughtSignature`, Anthropic uses `signature`.
|
||||
*
|
||||
* @param part - Thinking part to check
|
||||
* @returns True if part has valid signature
|
||||
*/
|
||||
function hasValidSignature(part: GeminiPart): boolean {
|
||||
// Gemini-style signature
|
||||
if (part.thought === true && part.thoughtSignature) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Anthropic-style signature
|
||||
if ((part.type === "thinking" || part.type === "reasoning") && part.signature) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract thinking blocks from a Gemini response
|
||||
*
|
||||
* Parses the response structure to identify and extract all thinking/reasoning
|
||||
* content. Supports both Gemini-style (thought: true) and Anthropic-style
|
||||
* (type: "thinking") formats.
|
||||
*
|
||||
* @param response - Gemini response object
|
||||
* @returns Extraction result with thinking blocks and metadata
|
||||
*/
|
||||
export function extractThinkingBlocks(response: GeminiResponse): ThinkingExtractionResult {
|
||||
const thinkingBlocks: ThinkingBlock[] = []
|
||||
|
||||
// Handle candidates array (standard Gemini format)
|
||||
if (response.candidates && Array.isArray(response.candidates)) {
|
||||
for (const candidate of response.candidates) {
|
||||
const parts = candidate.content?.parts
|
||||
if (!parts || !Array.isArray(parts)) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
if (!part || typeof part !== "object") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (isThinkingPart(part)) {
|
||||
const block: ThinkingBlock = {
|
||||
text: part.text || "",
|
||||
index: thinkingBlocks.length,
|
||||
}
|
||||
|
||||
// Extract signature if present
|
||||
if (part.thought === true && part.thoughtSignature) {
|
||||
block.signature = part.thoughtSignature
|
||||
} else if (part.signature) {
|
||||
block.signature = part.signature
|
||||
}
|
||||
|
||||
thinkingBlocks.push(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle direct content array (Anthropic-style response)
|
||||
if (response.content && Array.isArray(response.content)) {
|
||||
for (let i = 0; i < response.content.length; i++) {
|
||||
const item = response.content[i]
|
||||
if (!item || typeof item !== "object") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (item.type === "thinking" || item.type === "reasoning") {
|
||||
thinkingBlocks.push({
|
||||
text: item.text || "",
|
||||
signature: item.signature,
|
||||
index: thinkingBlocks.length,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all thinking text
|
||||
const combinedThinking = thinkingBlocks.map((b) => b.text).join("\n\n")
|
||||
|
||||
return {
|
||||
thinkingBlocks,
|
||||
combinedThinking,
|
||||
hasThinking: thinkingBlocks.length > 0,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format thinking blocks for OpenAI-compatible output
|
||||
*
|
||||
* Converts Gemini thinking block format to OpenAI's expected structure.
|
||||
* OpenAI expects thinking content as special message blocks or annotations.
|
||||
*
|
||||
* Output format:
|
||||
* ```
|
||||
* [
|
||||
* { type: "reasoning", text: "thinking content...", signature?: "..." },
|
||||
* ...
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* @param thinking - Array of thinking blocks to format
|
||||
* @returns OpenAI-compatible formatted array
|
||||
*/
|
||||
export function formatThinkingForOpenAI(
|
||||
thinking: ThinkingBlock[],
|
||||
): Array<{ type: "reasoning"; text: string; signature?: string }> {
|
||||
if (!thinking || !Array.isArray(thinking) || thinking.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return thinking.map((block) => {
|
||||
const formatted: { type: "reasoning"; text: string; signature?: string } = {
|
||||
type: "reasoning",
|
||||
text: block.text || "",
|
||||
}
|
||||
|
||||
if (block.signature) {
|
||||
formatted.signature = block.signature
|
||||
}
|
||||
|
||||
return formatted
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform thinking parts in a candidate to OpenAI format
|
||||
*
|
||||
* Modifies candidate content parts to use OpenAI-style reasoning format
|
||||
* while preserving the rest of the response structure.
|
||||
*
|
||||
* @param candidate - Gemini candidate to transform
|
||||
* @returns Transformed candidate with reasoning-formatted thinking
|
||||
*/
|
||||
export function transformCandidateThinking(candidate: GeminiCandidate): GeminiCandidate {
|
||||
if (!candidate || typeof candidate !== "object") {
|
||||
return candidate
|
||||
}
|
||||
|
||||
const content = candidate.content
|
||||
if (!content || typeof content !== "object" || !Array.isArray(content.parts)) {
|
||||
return candidate
|
||||
}
|
||||
|
||||
const thinkingTexts: string[] = []
|
||||
const transformedParts = content.parts.map((part) => {
|
||||
if (part && typeof part === "object" && part.thought === true) {
|
||||
thinkingTexts.push(part.text || "")
|
||||
// Transform to reasoning format
|
||||
return {
|
||||
...part,
|
||||
type: "reasoning" as const,
|
||||
thought: undefined, // Remove Gemini-specific field
|
||||
}
|
||||
}
|
||||
return part
|
||||
})
|
||||
|
||||
const result: GeminiCandidate & { reasoning_content?: string } = {
|
||||
...candidate,
|
||||
content: { ...content, parts: transformedParts },
|
||||
}
|
||||
|
||||
// Add combined reasoning content for convenience
|
||||
if (thinkingTexts.length > 0) {
|
||||
result.reasoning_content = thinkingTexts.join("\n\n")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Anthropic-style thinking blocks to reasoning format
|
||||
*
|
||||
* Converts `type: "thinking"` blocks to `type: "reasoning"` for consistency.
|
||||
*
|
||||
* @param content - Array of content blocks
|
||||
* @returns Transformed content array
|
||||
*/
|
||||
export function transformAnthropicThinking(
|
||||
content: Array<{ type?: string; text?: string; signature?: string }>,
|
||||
): Array<{ type?: string; text?: string; signature?: string }> {
|
||||
if (!content || !Array.isArray(content)) {
|
||||
return content
|
||||
}
|
||||
|
||||
return content.map((block) => {
|
||||
if (block && typeof block === "object" && block.type === "thinking") {
|
||||
return {
|
||||
type: "reasoning",
|
||||
text: block.text || "",
|
||||
...(block.signature ? { signature: block.signature } : {}),
|
||||
}
|
||||
}
|
||||
return block
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out unsigned thinking blocks
|
||||
*
|
||||
* Claude API requires signed thinking blocks for multi-turn conversations.
|
||||
* This function removes thinking blocks without valid signatures.
|
||||
*
|
||||
* @param parts - Array of content parts
|
||||
* @returns Filtered array without unsigned thinking blocks
|
||||
*/
|
||||
export function filterUnsignedThinkingBlocks(parts: GeminiPart[]): GeminiPart[] {
|
||||
if (!parts || !Array.isArray(parts)) {
|
||||
return parts
|
||||
}
|
||||
|
||||
return parts.filter((part) => {
|
||||
if (!part || typeof part !== "object") {
|
||||
return true
|
||||
}
|
||||
|
||||
// If it's a thinking part, only keep it if signed
|
||||
if (isThinkingPart(part)) {
|
||||
return hasValidSignature(part)
|
||||
}
|
||||
|
||||
// Keep all non-thinking parts
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform entire response thinking parts
|
||||
*
|
||||
* Main transformation function that handles both Gemini-style and
|
||||
* Anthropic-style thinking blocks in a response.
|
||||
*
|
||||
* @param response - Response object to transform
|
||||
* @returns Transformed response with standardized reasoning format
|
||||
*/
|
||||
export function transformResponseThinking(response: GeminiResponse): GeminiResponse {
|
||||
if (!response || typeof response !== "object") {
|
||||
return response
|
||||
}
|
||||
|
||||
const result: GeminiResponse = { ...response }
|
||||
|
||||
// Transform candidates (Gemini-style)
|
||||
if (Array.isArray(result.candidates)) {
|
||||
result.candidates = result.candidates.map(transformCandidateThinking)
|
||||
}
|
||||
|
||||
// Transform direct content (Anthropic-style)
|
||||
if (Array.isArray(result.content)) {
|
||||
result.content = transformAnthropicThinking(result.content)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Thinking configuration for requests
|
||||
*/
|
||||
export interface ThinkingConfig {
|
||||
/** Token budget for thinking/reasoning */
|
||||
thinkingBudget?: number
|
||||
/** Whether to include thoughts in response */
|
||||
includeThoughts?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize thinking configuration
|
||||
*
|
||||
* Ensures thinkingConfig is valid: includeThoughts only allowed when budget > 0.
|
||||
*
|
||||
* @param config - Raw thinking configuration
|
||||
* @returns Normalized configuration or undefined
|
||||
*/
|
||||
export function normalizeThinkingConfig(config: unknown): ThinkingConfig | undefined {
|
||||
if (!config || typeof config !== "object") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const record = config as Record<string, unknown>
|
||||
const budgetRaw = record.thinkingBudget ?? record.thinking_budget
|
||||
const includeRaw = record.includeThoughts ?? record.include_thoughts
|
||||
|
||||
const thinkingBudget =
|
||||
typeof budgetRaw === "number" && Number.isFinite(budgetRaw) ? budgetRaw : undefined
|
||||
const includeThoughts = typeof includeRaw === "boolean" ? includeRaw : undefined
|
||||
|
||||
const enableThinking = thinkingBudget !== undefined && thinkingBudget > 0
|
||||
const finalInclude = enableThinking ? (includeThoughts ?? false) : false
|
||||
|
||||
// Return undefined if no meaningful config
|
||||
if (
|
||||
!enableThinking &&
|
||||
finalInclude === false &&
|
||||
thinkingBudget === undefined &&
|
||||
includeThoughts === undefined
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const normalized: ThinkingConfig = {}
|
||||
if (thinkingBudget !== undefined) {
|
||||
normalized.thinkingBudget = thinkingBudget
|
||||
}
|
||||
if (finalInclude !== undefined) {
|
||||
normalized.includeThoughts = finalInclude
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract thinking configuration from request payload
|
||||
*
|
||||
* Supports both Gemini-style thinkingConfig and Anthropic-style thinking options.
|
||||
*
|
||||
* @param requestPayload - Request body
|
||||
* @param generationConfig - Generation config from request
|
||||
* @param extraBody - Extra body options
|
||||
* @returns Extracted thinking configuration or undefined
|
||||
*/
|
||||
export function extractThinkingConfig(
|
||||
requestPayload: Record<string, unknown>,
|
||||
generationConfig?: Record<string, unknown>,
|
||||
extraBody?: Record<string, unknown>,
|
||||
): ThinkingConfig | undefined {
|
||||
// Check for explicit thinkingConfig
|
||||
const thinkingConfig =
|
||||
generationConfig?.thinkingConfig ?? extraBody?.thinkingConfig ?? requestPayload.thinkingConfig
|
||||
|
||||
if (thinkingConfig && typeof thinkingConfig === "object") {
|
||||
const config = thinkingConfig as Record<string, unknown>
|
||||
return {
|
||||
includeThoughts: Boolean(config.includeThoughts),
|
||||
thinkingBudget:
|
||||
typeof config.thinkingBudget === "number" ? config.thinkingBudget : DEFAULT_THINKING_BUDGET,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Anthropic-style "thinking" option: { type: "enabled", budgetTokens: N }
|
||||
const anthropicThinking = extraBody?.thinking ?? requestPayload.thinking
|
||||
if (anthropicThinking && typeof anthropicThinking === "object") {
|
||||
const thinking = anthropicThinking as Record<string, unknown>
|
||||
if (thinking.type === "enabled" || thinking.budgetTokens) {
|
||||
return {
|
||||
includeThoughts: true,
|
||||
thinkingBudget:
|
||||
typeof thinking.budgetTokens === "number"
|
||||
? thinking.budgetTokens
|
||||
: DEFAULT_THINKING_BUDGET,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve final thinking configuration based on model and context
|
||||
*
|
||||
* Handles special cases like Claude models requiring signed thinking blocks
|
||||
* for multi-turn conversations.
|
||||
*
|
||||
* @param userConfig - User-provided thinking configuration
|
||||
* @param isThinkingModel - Whether model supports thinking
|
||||
* @param isClaudeModel - Whether model is Claude (not used in Antigravity, but kept for compatibility)
|
||||
* @param hasAssistantHistory - Whether conversation has assistant history
|
||||
* @returns Final thinking configuration
|
||||
*/
|
||||
export function resolveThinkingConfig(
|
||||
userConfig: ThinkingConfig | undefined,
|
||||
isThinkingModel: boolean,
|
||||
isClaudeModel: boolean,
|
||||
hasAssistantHistory: boolean,
|
||||
): ThinkingConfig | undefined {
|
||||
// Claude models with history need signed thinking blocks
|
||||
// Since we can't guarantee signatures, disable thinking
|
||||
if (isClaudeModel && hasAssistantHistory) {
|
||||
return { includeThoughts: false, thinkingBudget: 0 }
|
||||
}
|
||||
|
||||
// Enable thinking by default for thinking-capable models
|
||||
if (isThinkingModel && !userConfig) {
|
||||
return { includeThoughts: true, thinkingBudget: DEFAULT_THINKING_BUDGET }
|
||||
}
|
||||
|
||||
return userConfig
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
/**
|
||||
* Thought Signature Store
|
||||
*
|
||||
* Stores and retrieves thought signatures for multi-turn conversations.
|
||||
* Gemini 3 Pro requires thought_signature on function call content blocks
|
||||
* in subsequent requests to maintain reasoning continuity.
|
||||
*
|
||||
* Key responsibilities:
|
||||
* - Store the latest thought signature per session
|
||||
* - Provide signature for injection into function call requests
|
||||
* - Clear signatures when sessions end
|
||||
*/
|
||||
|
||||
/**
|
||||
* In-memory store for thought signatures indexed by session ID
|
||||
*/
|
||||
const signatureStore = new Map<string, string>()
|
||||
|
||||
/**
|
||||
* In-memory store for session IDs per fetch instance
|
||||
* Used to maintain consistent sessionId across multi-turn conversations
|
||||
*/
|
||||
const sessionIdStore = new Map<string, string>()
|
||||
|
||||
/**
|
||||
* Store a thought signature for a session
|
||||
*
|
||||
* @param sessionKey - Unique session identifier (typically fetch instance ID)
|
||||
* @param signature - The thought signature from model response
|
||||
*/
|
||||
export function setThoughtSignature(sessionKey: string, signature: string): void {
|
||||
if (sessionKey && signature) {
|
||||
signatureStore.set(sessionKey, signature)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the stored thought signature for a session
|
||||
*
|
||||
* @param sessionKey - Unique session identifier
|
||||
* @returns The stored signature or undefined if not found
|
||||
*/
|
||||
export function getThoughtSignature(sessionKey: string): string | undefined {
|
||||
return signatureStore.get(sessionKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the thought signature for a session
|
||||
*
|
||||
* @param sessionKey - Unique session identifier
|
||||
*/
|
||||
export function clearThoughtSignature(sessionKey: string): void {
|
||||
signatureStore.delete(sessionKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store or retrieve a persistent session ID for a fetch instance
|
||||
*
|
||||
* @param fetchInstanceId - Unique identifier for the fetch instance
|
||||
* @param sessionId - Optional session ID to store (if not provided, returns existing or generates new)
|
||||
* @returns The session ID for this fetch instance
|
||||
*/
|
||||
export function getOrCreateSessionId(fetchInstanceId: string, sessionId?: string): string {
|
||||
if (sessionId) {
|
||||
sessionIdStore.set(fetchInstanceId, sessionId)
|
||||
return sessionId
|
||||
}
|
||||
|
||||
const existing = sessionIdStore.get(fetchInstanceId)
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const n = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)
|
||||
const newSessionId = `-${n}`
|
||||
sessionIdStore.set(fetchInstanceId, newSessionId)
|
||||
return newSessionId
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the session ID for a fetch instance
|
||||
*
|
||||
* @param fetchInstanceId - Unique identifier for the fetch instance
|
||||
*/
|
||||
export function clearSessionId(fetchInstanceId: string): void {
|
||||
sessionIdStore.delete(fetchInstanceId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all stored data for a fetch instance (signature + session ID)
|
||||
*
|
||||
* @param fetchInstanceId - Unique identifier for the fetch instance
|
||||
*/
|
||||
export function clearFetchInstanceData(fetchInstanceId: string): void {
|
||||
signatureStore.delete(fetchInstanceId)
|
||||
sessionIdStore.delete(fetchInstanceId)
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
import {
|
||||
ANTIGRAVITY_CLIENT_ID,
|
||||
ANTIGRAVITY_CLIENT_SECRET,
|
||||
ANTIGRAVITY_TOKEN_REFRESH_BUFFER_MS,
|
||||
GOOGLE_TOKEN_URL,
|
||||
} from "./constants"
|
||||
import type {
|
||||
AntigravityRefreshParts,
|
||||
AntigravityTokenExchangeResult,
|
||||
AntigravityTokens,
|
||||
OAuthErrorPayload,
|
||||
ParsedOAuthError,
|
||||
} from "./types"
|
||||
|
||||
export class AntigravityTokenRefreshError extends Error {
|
||||
code?: string
|
||||
description?: string
|
||||
status: number
|
||||
statusText: string
|
||||
responseBody?: string
|
||||
|
||||
constructor(options: {
|
||||
message: string
|
||||
code?: string
|
||||
description?: string
|
||||
status: number
|
||||
statusText: string
|
||||
responseBody?: string
|
||||
}) {
|
||||
super(options.message)
|
||||
this.name = "AntigravityTokenRefreshError"
|
||||
this.code = options.code
|
||||
this.description = options.description
|
||||
this.status = options.status
|
||||
this.statusText = options.statusText
|
||||
this.responseBody = options.responseBody
|
||||
}
|
||||
|
||||
get isInvalidGrant(): boolean {
|
||||
return this.code === "invalid_grant"
|
||||
}
|
||||
|
||||
get isNetworkError(): boolean {
|
||||
return this.status === 0
|
||||
}
|
||||
}
|
||||
|
||||
function parseOAuthErrorPayload(text: string | undefined): ParsedOAuthError {
|
||||
if (!text) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(text) as OAuthErrorPayload
|
||||
let code: string | undefined
|
||||
|
||||
if (typeof payload.error === "string") {
|
||||
code = payload.error
|
||||
} else if (payload.error && typeof payload.error === "object") {
|
||||
code = payload.error.status ?? payload.error.code
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
description: payload.error_description,
|
||||
}
|
||||
} catch {
|
||||
return { description: text }
|
||||
}
|
||||
}
|
||||
|
||||
export function isTokenExpired(tokens: AntigravityTokens): boolean {
|
||||
const expirationTime = tokens.timestamp + tokens.expires_in * 1000
|
||||
return Date.now() >= expirationTime - ANTIGRAVITY_TOKEN_REFRESH_BUFFER_MS
|
||||
}
|
||||
|
||||
const MAX_REFRESH_RETRIES = 3
|
||||
const INITIAL_RETRY_DELAY_MS = 1000
|
||||
|
||||
function calculateRetryDelay(attempt: number): number {
|
||||
return Math.min(INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt), 10000)
|
||||
}
|
||||
|
||||
function isRetryableError(status: number): boolean {
|
||||
if (status === 0) return true
|
||||
if (status === 429) return true
|
||||
if (status >= 500 && status < 600) return true
|
||||
return false
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(
|
||||
refreshToken: string,
|
||||
clientId: string = ANTIGRAVITY_CLIENT_ID,
|
||||
clientSecret: string = ANTIGRAVITY_CLIENT_SECRET
|
||||
): Promise<AntigravityTokenExchangeResult> {
|
||||
const params = new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
})
|
||||
|
||||
let lastError: AntigravityTokenRefreshError | undefined
|
||||
|
||||
for (let attempt = 0; attempt <= MAX_REFRESH_RETRIES; attempt++) {
|
||||
try {
|
||||
const response = await fetch(GOOGLE_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: params,
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as {
|
||||
access_token: string
|
||||
refresh_token?: string
|
||||
expires_in: number
|
||||
token_type: string
|
||||
}
|
||||
|
||||
return {
|
||||
access_token: data.access_token,
|
||||
refresh_token: data.refresh_token || refreshToken,
|
||||
expires_in: data.expires_in,
|
||||
token_type: data.token_type,
|
||||
}
|
||||
}
|
||||
|
||||
const responseBody = await response.text().catch(() => undefined)
|
||||
const parsed = parseOAuthErrorPayload(responseBody)
|
||||
|
||||
lastError = new AntigravityTokenRefreshError({
|
||||
message: parsed.description || `Token refresh failed: ${response.status} ${response.statusText}`,
|
||||
code: parsed.code,
|
||||
description: parsed.description,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
responseBody,
|
||||
})
|
||||
|
||||
if (parsed.code === "invalid_grant") {
|
||||
throw lastError
|
||||
}
|
||||
|
||||
if (!isRetryableError(response.status)) {
|
||||
throw lastError
|
||||
}
|
||||
|
||||
if (attempt < MAX_REFRESH_RETRIES) {
|
||||
const delay = calculateRetryDelay(attempt)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AntigravityTokenRefreshError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
lastError = new AntigravityTokenRefreshError({
|
||||
message: error instanceof Error ? error.message : "Network error during token refresh",
|
||||
status: 0,
|
||||
statusText: "Network Error",
|
||||
})
|
||||
|
||||
if (attempt < MAX_REFRESH_RETRIES) {
|
||||
const delay = calculateRetryDelay(attempt)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new AntigravityTokenRefreshError({
|
||||
message: "Token refresh failed after all retries",
|
||||
status: 0,
|
||||
statusText: "Max Retries Exceeded",
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a stored token string into its component parts.
|
||||
* Storage format: `refreshToken|projectId|managedProjectId`
|
||||
*
|
||||
* @param stored - The pipe-separated stored token string
|
||||
* @returns Parsed refresh parts with refreshToken, projectId, and optional managedProjectId
|
||||
*/
|
||||
export function parseStoredToken(stored: string): AntigravityRefreshParts {
|
||||
const parts = stored.split("|")
|
||||
const [refreshToken, projectId, managedProjectId] = parts
|
||||
|
||||
return {
|
||||
refreshToken: refreshToken || "",
|
||||
projectId: projectId || undefined,
|
||||
managedProjectId: managedProjectId || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format token components for storage.
|
||||
* Creates a pipe-separated string: `refreshToken|projectId|managedProjectId`
|
||||
*
|
||||
* @param refreshToken - The refresh token
|
||||
* @param projectId - The GCP project ID
|
||||
* @param managedProjectId - Optional managed project ID for enterprise users
|
||||
* @returns Formatted string for storage
|
||||
*/
|
||||
export function formatTokenForStorage(
|
||||
refreshToken: string,
|
||||
projectId: string,
|
||||
managedProjectId?: string
|
||||
): string {
|
||||
return `${refreshToken}|${projectId}|${managedProjectId || ""}`
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
/**
|
||||
* Antigravity Tool Normalization
|
||||
* Converts tools between OpenAI and Gemini formats.
|
||||
*
|
||||
* OpenAI format:
|
||||
* { "type": "function", "function": { "name": "x", "description": "...", "parameters": {...} } }
|
||||
*
|
||||
* Gemini format:
|
||||
* { "functionDeclarations": [{ "name": "x", "description": "...", "parameters": {...} }] }
|
||||
*
|
||||
* Note: This is for Gemini models ONLY. Claude models are not supported via Antigravity.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OpenAI function tool format
|
||||
*/
|
||||
export interface OpenAITool {
|
||||
type: string
|
||||
function?: {
|
||||
name: string
|
||||
description?: string
|
||||
parameters?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini function declaration format
|
||||
*/
|
||||
export interface GeminiFunctionDeclaration {
|
||||
name: string
|
||||
description?: string
|
||||
parameters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini tools format (array of functionDeclarations)
|
||||
*/
|
||||
export interface GeminiTools {
|
||||
functionDeclarations: GeminiFunctionDeclaration[]
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI tool call in response
|
||||
*/
|
||||
export interface OpenAIToolCall {
|
||||
id: string
|
||||
type: "function"
|
||||
function: {
|
||||
name: string
|
||||
arguments: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini function call in response
|
||||
*/
|
||||
export interface GeminiFunctionCall {
|
||||
name: string
|
||||
args: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini function response format
|
||||
*/
|
||||
export interface GeminiFunctionResponse {
|
||||
name: string
|
||||
response: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini tool result containing function calls
|
||||
*/
|
||||
export interface GeminiToolResult {
|
||||
functionCall?: GeminiFunctionCall
|
||||
functionResponse?: GeminiFunctionResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize OpenAI-format tools to Gemini format.
|
||||
* Converts an array of OpenAI tools to Gemini's functionDeclarations format.
|
||||
*
|
||||
* - Handles `function` type tools with name, description, parameters
|
||||
* - Logs warning for unsupported tool types (does NOT silently drop them)
|
||||
* - Creates a single object with functionDeclarations array
|
||||
*
|
||||
* @param tools - Array of OpenAI-format tools
|
||||
* @returns Gemini-format tools object with functionDeclarations, or undefined if no valid tools
|
||||
*/
|
||||
export function normalizeToolsForGemini(
|
||||
tools: OpenAITool[]
|
||||
): GeminiTools | undefined {
|
||||
if (!tools || tools.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const functionDeclarations: GeminiFunctionDeclaration[] = []
|
||||
|
||||
for (const tool of tools) {
|
||||
if (!tool || typeof tool !== "object") {
|
||||
continue
|
||||
}
|
||||
|
||||
const toolType = tool.type ?? "function"
|
||||
if (toolType === "function" && tool.function) {
|
||||
const declaration: GeminiFunctionDeclaration = {
|
||||
name: tool.function.name,
|
||||
}
|
||||
|
||||
if (tool.function.description) {
|
||||
declaration.description = tool.function.description
|
||||
}
|
||||
|
||||
if (tool.function.parameters) {
|
||||
declaration.parameters = tool.function.parameters
|
||||
} else {
|
||||
declaration.parameters = { type: "object", properties: {} }
|
||||
}
|
||||
|
||||
functionDeclarations.push(declaration)
|
||||
} else if (toolType !== "function" && process.env.ANTIGRAVITY_DEBUG === "1") {
|
||||
console.warn(
|
||||
`[antigravity-tools] Unsupported tool type: "${toolType}". Tool will be skipped.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Return undefined if no valid function declarations
|
||||
if (functionDeclarations.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return { functionDeclarations }
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Gemini tool results (functionCall) back to OpenAI tool_call format.
|
||||
* Handles both functionCall (request) and functionResponse (result) formats.
|
||||
*
|
||||
* Gemini functionCall format:
|
||||
* { "name": "tool_name", "args": { ... } }
|
||||
*
|
||||
* OpenAI tool_call format:
|
||||
* { "id": "call_xxx", "type": "function", "function": { "name": "tool_name", "arguments": "..." } }
|
||||
*
|
||||
* @param results - Array of Gemini tool results containing functionCall or functionResponse
|
||||
* @returns Array of OpenAI-format tool calls
|
||||
*/
|
||||
export function normalizeToolResultsFromGemini(
|
||||
results: GeminiToolResult[]
|
||||
): OpenAIToolCall[] {
|
||||
if (!results || results.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const toolCalls: OpenAIToolCall[] = []
|
||||
let callCounter = 0
|
||||
|
||||
for (const result of results) {
|
||||
// Handle functionCall (tool invocation from model)
|
||||
if (result.functionCall) {
|
||||
callCounter++
|
||||
const toolCall: OpenAIToolCall = {
|
||||
id: `call_${Date.now()}_${callCounter}`,
|
||||
type: "function",
|
||||
function: {
|
||||
name: result.functionCall.name,
|
||||
arguments: JSON.stringify(result.functionCall.args ?? {}),
|
||||
},
|
||||
}
|
||||
toolCalls.push(toolCall)
|
||||
}
|
||||
}
|
||||
|
||||
return toolCalls
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a single Gemini functionCall to OpenAI tool_call format.
|
||||
* Useful for streaming responses where each chunk may contain a function call.
|
||||
*
|
||||
* @param functionCall - Gemini function call
|
||||
* @param id - Optional tool call ID (generates one if not provided)
|
||||
* @returns OpenAI-format tool call
|
||||
*/
|
||||
export function convertFunctionCallToToolCall(
|
||||
functionCall: GeminiFunctionCall,
|
||||
id?: string
|
||||
): OpenAIToolCall {
|
||||
return {
|
||||
id: id ?? `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
type: "function",
|
||||
function: {
|
||||
name: functionCall.name,
|
||||
arguments: JSON.stringify(functionCall.args ?? {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool array contains any function-type tools.
|
||||
*
|
||||
* @param tools - Array of OpenAI-format tools
|
||||
* @returns true if there are function tools to normalize
|
||||
*/
|
||||
export function hasFunctionTools(tools: OpenAITool[]): boolean {
|
||||
if (!tools || tools.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return tools.some((tool) => tool.type === "function" && tool.function)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract function declarations from already-normalized Gemini tools.
|
||||
* Useful when tools may already be in Gemini format.
|
||||
*
|
||||
* @param tools - Tools that may be in Gemini or OpenAI format
|
||||
* @returns Array of function declarations
|
||||
*/
|
||||
export function extractFunctionDeclarations(
|
||||
tools: unknown
|
||||
): GeminiFunctionDeclaration[] {
|
||||
if (!tools || typeof tools !== "object") {
|
||||
return []
|
||||
}
|
||||
|
||||
// Check if already in Gemini format
|
||||
const geminiTools = tools as Record<string, unknown>
|
||||
if (
|
||||
Array.isArray(geminiTools.functionDeclarations) &&
|
||||
geminiTools.functionDeclarations.length > 0
|
||||
) {
|
||||
return geminiTools.functionDeclarations as GeminiFunctionDeclaration[]
|
||||
}
|
||||
|
||||
// Check if it's an array of OpenAI tools
|
||||
if (Array.isArray(tools)) {
|
||||
const normalized = normalizeToolsForGemini(tools as OpenAITool[])
|
||||
return normalized?.functionDeclarations ?? []
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
/**
|
||||
* Antigravity Auth Type Definitions
|
||||
* Matches cliproxyapi/sdk/auth/antigravity.go token format exactly
|
||||
*/
|
||||
|
||||
/**
|
||||
* Token storage format for Antigravity authentication
|
||||
* Matches Go metadata structure: type, access_token, refresh_token, expires_in, timestamp, email, project_id
|
||||
*/
|
||||
export interface AntigravityTokens {
|
||||
/** Always "antigravity" for this auth type */
|
||||
type: "antigravity"
|
||||
/** OAuth access token from Google */
|
||||
access_token: string
|
||||
/** OAuth refresh token from Google */
|
||||
refresh_token: string
|
||||
/** Token expiration time in seconds */
|
||||
expires_in: number
|
||||
/** Unix timestamp in milliseconds when tokens were obtained */
|
||||
timestamp: number
|
||||
/** ISO 8601 formatted expiration datetime (optional, for display) */
|
||||
expired?: string
|
||||
/** User's email address from Google userinfo */
|
||||
email?: string
|
||||
/** GCP project ID from loadCodeAssist API */
|
||||
project_id?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Project context returned from loadCodeAssist API
|
||||
* Used to get cloudaicompanionProject for API calls
|
||||
*/
|
||||
export interface AntigravityProjectContext {
|
||||
/** GCP project ID for Cloud AI Companion */
|
||||
cloudaicompanionProject?: string
|
||||
/** Managed project ID for enterprise users (optional) */
|
||||
managedProjectId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for loadCodeAssist API request
|
||||
*/
|
||||
export interface AntigravityClientMetadata {
|
||||
/** IDE type identifier */
|
||||
ideType: "IDE_UNSPECIFIED" | string
|
||||
/** Platform identifier */
|
||||
platform: "PLATFORM_UNSPECIFIED" | string
|
||||
/** Plugin type - typically "GEMINI" */
|
||||
pluginType: "GEMINI" | string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request body for loadCodeAssist API
|
||||
*/
|
||||
export interface AntigravityLoadCodeAssistRequest {
|
||||
metadata: AntigravityClientMetadata
|
||||
}
|
||||
|
||||
export interface AntigravityUserTier {
|
||||
id?: string
|
||||
isDefault?: boolean
|
||||
userDefinedCloudaicompanionProject?: boolean
|
||||
}
|
||||
|
||||
export interface AntigravityLoadCodeAssistResponse {
|
||||
cloudaicompanionProject?: string | { id: string }
|
||||
currentTier?: { id?: string }
|
||||
allowedTiers?: AntigravityUserTier[]
|
||||
}
|
||||
|
||||
export interface AntigravityOnboardUserPayload {
|
||||
done?: boolean
|
||||
response?: {
|
||||
cloudaicompanionProject?: { id?: string }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request body format for Antigravity API calls
|
||||
* Wraps the actual request with project and model context
|
||||
*/
|
||||
export interface AntigravityRequestBody {
|
||||
/** GCP project ID */
|
||||
project: string
|
||||
/** Model identifier (e.g., "gemini-3-pro-preview") */
|
||||
model: string
|
||||
/** User agent identifier */
|
||||
userAgent: string
|
||||
/** Unique request ID */
|
||||
requestId: string
|
||||
/** The actual request payload */
|
||||
request: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Response format from Antigravity API
|
||||
* Follows OpenAI-compatible structure with Gemini extensions
|
||||
*/
|
||||
export interface AntigravityResponse {
|
||||
/** Response ID */
|
||||
id?: string
|
||||
/** Object type (e.g., "chat.completion") */
|
||||
object?: string
|
||||
/** Creation timestamp */
|
||||
created?: number
|
||||
/** Model used for response */
|
||||
model?: string
|
||||
/** Response choices */
|
||||
choices?: AntigravityResponseChoice[]
|
||||
/** Token usage statistics */
|
||||
usage?: AntigravityUsage
|
||||
/** Error information if request failed */
|
||||
error?: AntigravityError
|
||||
}
|
||||
|
||||
/**
|
||||
* Single response choice in Antigravity response
|
||||
*/
|
||||
export interface AntigravityResponseChoice {
|
||||
/** Choice index */
|
||||
index: number
|
||||
/** Message content */
|
||||
message?: {
|
||||
role: "assistant"
|
||||
content?: string
|
||||
tool_calls?: AntigravityToolCall[]
|
||||
}
|
||||
/** Delta for streaming responses */
|
||||
delta?: {
|
||||
role?: "assistant"
|
||||
content?: string
|
||||
tool_calls?: AntigravityToolCall[]
|
||||
}
|
||||
/** Finish reason */
|
||||
finish_reason?: "stop" | "tool_calls" | "length" | "content_filter" | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool call in Antigravity response
|
||||
*/
|
||||
export interface AntigravityToolCall {
|
||||
id: string
|
||||
type: "function"
|
||||
function: {
|
||||
name: string
|
||||
arguments: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token usage statistics
|
||||
*/
|
||||
export interface AntigravityUsage {
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
total_tokens: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response from Antigravity API
|
||||
*/
|
||||
export interface AntigravityError {
|
||||
message: string
|
||||
type?: string
|
||||
code?: string | number
|
||||
}
|
||||
|
||||
/**
|
||||
* Token exchange result from Google OAuth
|
||||
* Matches antigravityTokenResponse in Go
|
||||
*/
|
||||
export interface AntigravityTokenExchangeResult {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
token_type: string
|
||||
}
|
||||
|
||||
/**
|
||||
* User info from Google userinfo API
|
||||
*/
|
||||
export interface AntigravityUserInfo {
|
||||
email: string
|
||||
name?: string
|
||||
picture?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsed refresh token parts
|
||||
* Format: refreshToken|projectId|managedProjectId
|
||||
*/
|
||||
export interface AntigravityRefreshParts {
|
||||
refreshToken: string
|
||||
projectId?: string
|
||||
managedProjectId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth error payload from Google
|
||||
* Google returns errors in multiple formats, this handles all of them
|
||||
*/
|
||||
export interface OAuthErrorPayload {
|
||||
error?: string | { status?: string; code?: string; message?: string }
|
||||
error_description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsed OAuth error with normalized fields
|
||||
*/
|
||||
export interface ParsedOAuthError {
|
||||
code?: string
|
||||
description?: string
|
||||
}
|
||||
@@ -2,67 +2,90 @@
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
CLI for oh-my-opencode: interactive installer, health diagnostics (doctor), runtime launcher. Entry: `bunx oh-my-opencode`.
|
||||
CLI entry point: `bunx oh-my-opencode`. Interactive installer, doctor diagnostics, session runner. Uses Commander.js + @clack/prompts TUI.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
cli/
|
||||
├── index.ts # Commander.js entry, subcommand routing
|
||||
├── install.ts # Interactive TUI installer (477 lines)
|
||||
├── config-manager.ts # JSONC parsing, env detection (669 lines)
|
||||
├── types.ts # CLI-specific types
|
||||
├── doctor/ # Health check system
|
||||
├── index.ts # Commander.js entry, 5 subcommands
|
||||
├── install.ts # Interactive TUI installer (462 lines)
|
||||
├── config-manager.ts # JSONC parsing, multi-level merge (730 lines)
|
||||
├── types.ts # InstallArgs, InstallConfig, DetectedConfig
|
||||
├── doctor/
|
||||
│ ├── index.ts # Doctor command entry
|
||||
│ ├── constants.ts # Check categories
|
||||
│ ├── types.ts # Check result interfaces
|
||||
│ └── checks/ # 17+ individual checks
|
||||
├── get-local-version/ # Version detection
|
||||
└── run/ # OpenCode session launcher
|
||||
│ ├── runner.ts # Check orchestration
|
||||
│ ├── formatter.ts # Colored output, symbols
|
||||
│ ├── constants.ts # Check IDs, categories, symbols
|
||||
│ ├── types.ts # CheckResult, CheckDefinition
|
||||
│ └── checks/ # 14 checks across 6 categories
|
||||
│ ├── version.ts # OpenCode + plugin version
|
||||
│ ├── config.ts # JSONC validity, Zod validation
|
||||
│ ├── auth.ts # Anthropic, OpenAI, Google
|
||||
│ ├── dependencies.ts # AST-Grep, Comment Checker
|
||||
│ ├── lsp.ts # LSP server connectivity
|
||||
│ ├── mcp.ts # MCP server validation
|
||||
│ └── gh.ts # GitHub CLI availability
|
||||
├── run/
|
||||
│ ├── index.ts # Run command entry
|
||||
│ └── runner.ts # Session launcher
|
||||
└── get-local-version/
|
||||
├── index.ts # Version detection
|
||||
└── formatter.ts # Version output
|
||||
```
|
||||
|
||||
## CLI COMMANDS
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `install` | Interactive setup wizard |
|
||||
| `doctor` | Environment health checks |
|
||||
| `run` | Launch OpenCode session |
|
||||
| `install` | Interactive setup, subscription detection |
|
||||
| `doctor` | 14 health checks, `--verbose`, `--json`, `--category` |
|
||||
| `run` | Launch OpenCode session with completion enforcement |
|
||||
| `get-local-version` | Version detection, update checking |
|
||||
|
||||
## DOCTOR CHECKS
|
||||
## DOCTOR CHECK CATEGORIES
|
||||
|
||||
17+ checks in `doctor/checks/`:
|
||||
- version.ts (OpenCode >= 1.0.150)
|
||||
- config.ts (plugin registered)
|
||||
- bun.ts, node.ts, git.ts
|
||||
- anthropic-auth.ts, openai-auth.ts, google-auth.ts
|
||||
- lsp-*.ts, mcp-*.ts
|
||||
|
||||
## CONFIG-MANAGER (669 lines)
|
||||
|
||||
- JSONC support (comments, trailing commas)
|
||||
- Multi-source: User (~/.config/opencode/) + Project (.opencode/)
|
||||
- Zod validation
|
||||
- Legacy format migration
|
||||
- Error aggregation for doctor
|
||||
| Category | Checks |
|
||||
|----------|--------|
|
||||
| installation | opencode, plugin registration |
|
||||
| configuration | config validity, Zod validation |
|
||||
| authentication | anthropic, openai, google |
|
||||
| dependencies | ast-grep CLI/NAPI, comment-checker |
|
||||
| tools | LSP, MCP connectivity |
|
||||
| updates | version comparison |
|
||||
|
||||
## HOW TO ADD CHECK
|
||||
|
||||
1. Create `src/cli/doctor/checks/my-check.ts`:
|
||||
```typescript
|
||||
export const myCheck: DoctorCheck = {
|
||||
name: "my-check",
|
||||
category: "environment",
|
||||
check: async () => {
|
||||
return { status: "pass" | "warn" | "fail", message: "..." }
|
||||
export function getMyCheckDefinition(): CheckDefinition {
|
||||
return {
|
||||
id: "my-check",
|
||||
name: "My Check",
|
||||
category: "configuration",
|
||||
check: async () => ({ status: "pass", message: "OK" })
|
||||
}
|
||||
}
|
||||
```
|
||||
2. Add to `src/cli/doctor/checks/index.ts`
|
||||
2. Export from `checks/index.ts`
|
||||
3. Add to `getAllCheckDefinitions()`
|
||||
|
||||
## TUI FRAMEWORK
|
||||
|
||||
- **@clack/prompts**: `select()`, `spinner()`, `intro()`, `outro()`, `note()`
|
||||
- **picocolors**: Colored terminal output
|
||||
- **Symbols**: ✓ (pass), ✗ (fail), ⚠ (warn), ○ (skip)
|
||||
|
||||
## CONFIG-MANAGER
|
||||
|
||||
- **JSONC**: Comments (`// ...`), block comments, trailing commas
|
||||
- **Multi-source**: User (`~/.config/opencode/`) + Project (`.opencode/`)
|
||||
- **Env override**: `OPENCODE_CONFIG_DIR` for profile isolation
|
||||
- **Validation**: Zod schema with error aggregation
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
- Blocking prompts in non-TTY (check `process.stdout.isTTY`)
|
||||
- Hardcoded paths (use shared utilities)
|
||||
- JSON.parse for user files (use parseJsonc)
|
||||
- Silent failures in doctor checks
|
||||
- **Blocking in non-TTY**: Check `process.stdout.isTTY`
|
||||
- **Direct JSON.parse**: Use `parseJsonc()` for config
|
||||
- **Silent failures**: Always return warn/fail in doctor
|
||||
- **Hardcoded paths**: Use `ConfigManager`
|
||||
|
||||
@@ -1,6 +1,173 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { describe, expect, test, mock, beforeEach, afterEach } from "bun:test"
|
||||
|
||||
import { ANTIGRAVITY_PROVIDER_CONFIG } from "./config-manager"
|
||||
import { ANTIGRAVITY_PROVIDER_CONFIG, getPluginNameWithVersion, fetchNpmDistTags, generateOmoConfig } from "./config-manager"
|
||||
import type { InstallConfig } from "./types"
|
||||
|
||||
describe("getPluginNameWithVersion", () => {
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
test("returns @latest when current version matches latest tag", async () => {
|
||||
// #given npm dist-tags with latest=2.14.0
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3" }),
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// #when current version is 2.14.0
|
||||
const result = await getPluginNameWithVersion("2.14.0")
|
||||
|
||||
// #then should use @latest tag
|
||||
expect(result).toBe("oh-my-opencode@latest")
|
||||
})
|
||||
|
||||
test("returns @beta when current version matches beta tag", async () => {
|
||||
// #given npm dist-tags with beta=3.0.0-beta.3
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3" }),
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// #when current version is 3.0.0-beta.3
|
||||
const result = await getPluginNameWithVersion("3.0.0-beta.3")
|
||||
|
||||
// #then should use @beta tag
|
||||
expect(result).toBe("oh-my-opencode@beta")
|
||||
})
|
||||
|
||||
test("returns @next when current version matches next tag", async () => {
|
||||
// #given npm dist-tags with next=3.1.0-next.1
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3", next: "3.1.0-next.1" }),
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// #when current version is 3.1.0-next.1
|
||||
const result = await getPluginNameWithVersion("3.1.0-next.1")
|
||||
|
||||
// #then should use @next tag
|
||||
expect(result).toBe("oh-my-opencode@next")
|
||||
})
|
||||
|
||||
test("returns pinned version when no tag matches", async () => {
|
||||
// #given npm dist-tags with beta=3.0.0-beta.3
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3" }),
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// #when current version is old beta 3.0.0-beta.2
|
||||
const result = await getPluginNameWithVersion("3.0.0-beta.2")
|
||||
|
||||
// #then should pin to specific version
|
||||
expect(result).toBe("oh-my-opencode@3.0.0-beta.2")
|
||||
})
|
||||
|
||||
test("returns pinned version when fetch fails", async () => {
|
||||
// #given network failure
|
||||
globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch
|
||||
|
||||
// #when current version is 3.0.0-beta.3
|
||||
const result = await getPluginNameWithVersion("3.0.0-beta.3")
|
||||
|
||||
// #then should fall back to pinned version
|
||||
expect(result).toBe("oh-my-opencode@3.0.0-beta.3")
|
||||
})
|
||||
|
||||
test("returns pinned version when npm returns non-ok response", async () => {
|
||||
// #given npm returns 404
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
status: 404,
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// #when current version is 2.14.0
|
||||
const result = await getPluginNameWithVersion("2.14.0")
|
||||
|
||||
// #then should fall back to pinned version
|
||||
expect(result).toBe("oh-my-opencode@2.14.0")
|
||||
})
|
||||
|
||||
test("prioritizes latest over other tags when version matches multiple", async () => {
|
||||
// #given version matches both latest and beta (during release promotion)
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ beta: "3.0.0", latest: "3.0.0", next: "3.1.0-alpha.1" }),
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// #when current version matches both
|
||||
const result = await getPluginNameWithVersion("3.0.0")
|
||||
|
||||
// #then should prioritize @latest
|
||||
expect(result).toBe("oh-my-opencode@latest")
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetchNpmDistTags", () => {
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
test("returns dist-tags on success", async () => {
|
||||
// #given npm returns dist-tags
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3" }),
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// #when fetching dist-tags
|
||||
const result = await fetchNpmDistTags("oh-my-opencode")
|
||||
|
||||
// #then should return the tags
|
||||
expect(result).toEqual({ latest: "2.14.0", beta: "3.0.0-beta.3" })
|
||||
})
|
||||
|
||||
test("returns null on network failure", async () => {
|
||||
// #given network failure
|
||||
globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch
|
||||
|
||||
// #when fetching dist-tags
|
||||
const result = await fetchNpmDistTags("oh-my-opencode")
|
||||
|
||||
// #then should return null
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
test("returns null on non-ok response", async () => {
|
||||
// #given npm returns 404
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
status: 404,
|
||||
} as Response)
|
||||
) as unknown as typeof fetch
|
||||
|
||||
// #when fetching dist-tags
|
||||
const result = await fetchNpmDistTags("oh-my-opencode")
|
||||
|
||||
// #then should return null
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
test("Gemini models include full spec (limit + modalities)", () => {
|
||||
@@ -11,11 +178,9 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
expect(models).toBeTruthy()
|
||||
|
||||
const required = [
|
||||
"gemini-3-pro-high",
|
||||
"gemini-3-pro-medium",
|
||||
"gemini-3-pro-low",
|
||||
"gemini-3-flash",
|
||||
"gemini-3-flash-lite",
|
||||
"antigravity-gemini-3-pro-high",
|
||||
"antigravity-gemini-3-pro-low",
|
||||
"antigravity-gemini-3-flash",
|
||||
]
|
||||
|
||||
for (const key of required) {
|
||||
@@ -34,3 +199,133 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("generateOmoConfig - GitHub Copilot fallback", () => {
|
||||
test("frontend-ui-ux-engineer uses Copilot when no native providers", () => {
|
||||
// #given user has only Copilot (no Claude, ChatGPT, Gemini)
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: true,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then frontend-ui-ux-engineer should use Copilot Gemini
|
||||
const agents = result.agents as Record<string, { model?: string }>
|
||||
expect(agents["frontend-ui-ux-engineer"]?.model).toBe("github-copilot/gemini-3-pro-preview")
|
||||
})
|
||||
|
||||
test("document-writer uses Copilot when no native providers", () => {
|
||||
// #given user has only Copilot
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: true,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then document-writer should use Copilot Gemini Flash
|
||||
const agents = result.agents as Record<string, { model?: string }>
|
||||
expect(agents["document-writer"]?.model).toBe("github-copilot/gemini-3-flash-preview")
|
||||
})
|
||||
|
||||
test("multimodal-looker uses Copilot when no native providers", () => {
|
||||
// #given user has only Copilot
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: true,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then multimodal-looker should use Copilot Gemini Flash
|
||||
const agents = result.agents as Record<string, { model?: string }>
|
||||
expect(agents["multimodal-looker"]?.model).toBe("github-copilot/gemini-3-flash-preview")
|
||||
})
|
||||
|
||||
test("explore uses Copilot grok-code when no native providers", () => {
|
||||
// #given user has only Copilot
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: true,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then explore should use Copilot Grok
|
||||
const agents = result.agents as Record<string, { model?: string }>
|
||||
expect(agents["explore"]?.model).toBe("github-copilot/grok-code-fast-1")
|
||||
})
|
||||
|
||||
test("native Gemini takes priority over Copilot for frontend-ui-ux-engineer", () => {
|
||||
// #given user has both Gemini and Copilot
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasGemini: true,
|
||||
hasCopilot: true,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then native Gemini should be used (NOT Copilot)
|
||||
const agents = result.agents as Record<string, { model?: string }>
|
||||
expect(agents["frontend-ui-ux-engineer"]?.model).toBe("google/antigravity-gemini-3-pro-high")
|
||||
})
|
||||
|
||||
test("native Claude takes priority over Copilot for frontend-ui-ux-engineer", () => {
|
||||
// #given user has Claude and Copilot but no Gemini
|
||||
const config: InstallConfig = {
|
||||
hasClaude: true,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: true,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then native Claude should be used (NOT Copilot)
|
||||
const agents = result.agents as Record<string, { model?: string }>
|
||||
expect(agents["frontend-ui-ux-engineer"]?.model).toBe("anthropic/claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("categories use Copilot models when no native Gemini", () => {
|
||||
// #given user has Copilot but no Gemini
|
||||
const config: InstallConfig = {
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasChatGPT: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: true,
|
||||
}
|
||||
|
||||
// #when generating config
|
||||
const result = generateOmoConfig(config)
|
||||
|
||||
// #then categories should use Copilot models
|
||||
const categories = result.categories as Record<string, { model?: string }>
|
||||
expect(categories?.["visual-engineering"]?.model).toBe("github-copilot/gemini-3-pro-preview")
|
||||
expect(categories?.["artistry"]?.model).toBe("github-copilot/gemini-3-pro-preview")
|
||||
expect(categories?.["writing"]?.model).toBe("github-copilot/gemini-3-flash-preview")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,18 +1,58 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { parseJsonc } from "../shared"
|
||||
import {
|
||||
parseJsonc,
|
||||
getOpenCodeConfigPaths,
|
||||
type OpenCodeBinaryType,
|
||||
type OpenCodeConfigPaths,
|
||||
} from "../shared"
|
||||
import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types"
|
||||
|
||||
const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode")
|
||||
const OPENCODE_JSON = join(OPENCODE_CONFIG_DIR, "opencode.json")
|
||||
const OPENCODE_JSONC = join(OPENCODE_CONFIG_DIR, "opencode.jsonc")
|
||||
const OPENCODE_PACKAGE_JSON = join(OPENCODE_CONFIG_DIR, "package.json")
|
||||
const OMO_CONFIG = join(OPENCODE_CONFIG_DIR, "oh-my-opencode.json")
|
||||
|
||||
const OPENCODE_BINARIES = ["opencode", "opencode-desktop"] as const
|
||||
|
||||
const CHATGPT_HOTFIX_REPO = "code-yeongyu/opencode-openai-codex-auth#fix/orphaned-function-call-output-with-tools"
|
||||
interface ConfigContext {
|
||||
binary: OpenCodeBinaryType
|
||||
version: string | null
|
||||
paths: OpenCodeConfigPaths
|
||||
}
|
||||
|
||||
let configContext: ConfigContext | null = null
|
||||
|
||||
export function initConfigContext(binary: OpenCodeBinaryType, version: string | null): void {
|
||||
const paths = getOpenCodeConfigPaths({ binary, version })
|
||||
configContext = { binary, version, paths }
|
||||
}
|
||||
|
||||
export function getConfigContext(): ConfigContext {
|
||||
if (!configContext) {
|
||||
const paths = getOpenCodeConfigPaths({ binary: "opencode", version: null })
|
||||
configContext = { binary: "opencode", version: null, paths }
|
||||
}
|
||||
return configContext
|
||||
}
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
configContext = null
|
||||
}
|
||||
|
||||
function getConfigDir(): string {
|
||||
return getConfigContext().paths.configDir
|
||||
}
|
||||
|
||||
function getConfigJson(): string {
|
||||
return getConfigContext().paths.configJson
|
||||
}
|
||||
|
||||
function getConfigJsonc(): string {
|
||||
return getConfigContext().paths.configJsonc
|
||||
}
|
||||
|
||||
function getPackageJson(): string {
|
||||
return getConfigContext().paths.packageJson
|
||||
}
|
||||
|
||||
function getOmoConfig(): string {
|
||||
return getConfigContext().paths.omoConfig
|
||||
}
|
||||
|
||||
const BUN_INSTALL_TIMEOUT_SECONDS = 60
|
||||
const BUN_INSTALL_TIMEOUT_MS = BUN_INSTALL_TIMEOUT_SECONDS * 1000
|
||||
@@ -68,6 +108,47 @@ export async function fetchLatestVersion(packageName: string): Promise<string |
|
||||
}
|
||||
}
|
||||
|
||||
interface NpmDistTags {
|
||||
latest?: string
|
||||
beta?: string
|
||||
next?: string
|
||||
[tag: string]: string | undefined
|
||||
}
|
||||
|
||||
const NPM_FETCH_TIMEOUT_MS = 5000
|
||||
|
||||
export async function fetchNpmDistTags(packageName: string): Promise<NpmDistTags | null> {
|
||||
try {
|
||||
const res = await fetch(`https://registry.npmjs.org/-/package/${packageName}/dist-tags`, {
|
||||
signal: AbortSignal.timeout(NPM_FETCH_TIMEOUT_MS),
|
||||
})
|
||||
if (!res.ok) return null
|
||||
const data = await res.json() as NpmDistTags
|
||||
return data
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const PACKAGE_NAME = "oh-my-opencode"
|
||||
|
||||
const PRIORITIZED_TAGS = ["latest", "beta", "next"] as const
|
||||
|
||||
export async function getPluginNameWithVersion(currentVersion: string): Promise<string> {
|
||||
const distTags = await fetchNpmDistTags(PACKAGE_NAME)
|
||||
|
||||
if (distTags) {
|
||||
const allTags = new Set([...PRIORITIZED_TAGS, ...Object.keys(distTags)])
|
||||
for (const tag of allTags) {
|
||||
if (distTags[tag] === currentVersion) {
|
||||
return `${PACKAGE_NAME}@${tag}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `${PACKAGE_NAME}@${currentVersion}`
|
||||
}
|
||||
|
||||
type ConfigFormat = "json" | "jsonc" | "none"
|
||||
|
||||
interface OpenCodeConfig {
|
||||
@@ -76,13 +157,16 @@ interface OpenCodeConfig {
|
||||
}
|
||||
|
||||
export function detectConfigFormat(): { format: ConfigFormat; path: string } {
|
||||
if (existsSync(OPENCODE_JSONC)) {
|
||||
return { format: "jsonc", path: OPENCODE_JSONC }
|
||||
const configJsonc = getConfigJsonc()
|
||||
const configJson = getConfigJson()
|
||||
|
||||
if (existsSync(configJsonc)) {
|
||||
return { format: "jsonc", path: configJsonc }
|
||||
}
|
||||
if (existsSync(OPENCODE_JSON)) {
|
||||
return { format: "json", path: OPENCODE_JSON }
|
||||
if (existsSync(configJson)) {
|
||||
return { format: "json", path: configJson }
|
||||
}
|
||||
return { format: "none", path: OPENCODE_JSON }
|
||||
return { format: "none", path: configJson }
|
||||
}
|
||||
|
||||
interface ParseConfigResult {
|
||||
@@ -129,24 +213,25 @@ function parseConfigWithError(path: string): ParseConfigResult {
|
||||
}
|
||||
|
||||
function ensureConfigDir(): void {
|
||||
if (!existsSync(OPENCODE_CONFIG_DIR)) {
|
||||
mkdirSync(OPENCODE_CONFIG_DIR, { recursive: true })
|
||||
const configDir = getConfigDir()
|
||||
if (!existsSync(configDir)) {
|
||||
mkdirSync(configDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
export function addPluginToOpenCodeConfig(): ConfigMergeResult {
|
||||
export async function addPluginToOpenCodeConfig(currentVersion: string): Promise<ConfigMergeResult> {
|
||||
try {
|
||||
ensureConfigDir()
|
||||
} catch (err) {
|
||||
return { success: false, configPath: OPENCODE_CONFIG_DIR, error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
return { success: false, configPath: getConfigDir(), error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
const pluginName = "oh-my-opencode"
|
||||
const pluginEntry = await getPluginNameWithVersion(currentVersion)
|
||||
|
||||
try {
|
||||
if (format === "none") {
|
||||
const config: OpenCodeConfig = { plugin: [pluginName] }
|
||||
const config: OpenCodeConfig = { plugin: [pluginEntry] }
|
||||
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
||||
return { success: true, configPath: path }
|
||||
}
|
||||
@@ -158,11 +243,18 @@ export function addPluginToOpenCodeConfig(): ConfigMergeResult {
|
||||
|
||||
const config = parseResult.config
|
||||
const plugins = config.plugin ?? []
|
||||
if (plugins.some((p) => p.startsWith(pluginName))) {
|
||||
return { success: true, configPath: path }
|
||||
const existingIndex = plugins.findIndex((p) => p === PACKAGE_NAME || p.startsWith(`${PACKAGE_NAME}@`))
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
if (plugins[existingIndex] === pluginEntry) {
|
||||
return { success: true, configPath: path }
|
||||
}
|
||||
plugins[existingIndex] = pluginEntry
|
||||
} else {
|
||||
plugins.push(pluginEntry)
|
||||
}
|
||||
|
||||
config.plugin = [...plugins, pluginName]
|
||||
config.plugin = plugins
|
||||
|
||||
if (format === "jsonc") {
|
||||
const content = readFileSync(path, "utf-8")
|
||||
@@ -170,14 +262,11 @@ export function addPluginToOpenCodeConfig(): ConfigMergeResult {
|
||||
const match = content.match(pluginArrayRegex)
|
||||
|
||||
if (match) {
|
||||
const arrayContent = match[1].trim()
|
||||
const newArrayContent = arrayContent
|
||||
? `${arrayContent},\n "${pluginName}"`
|
||||
: `"${pluginName}"`
|
||||
const newContent = content.replace(pluginArrayRegex, `"plugin": [\n ${newArrayContent}\n ]`)
|
||||
const formattedPlugins = plugins.map((p) => `"${p}"`).join(",\n ")
|
||||
const newContent = content.replace(pluginArrayRegex, `"plugin": [\n ${formattedPlugins}\n ]`)
|
||||
writeFileSync(path, newContent)
|
||||
} else {
|
||||
const newContent = content.replace(/^(\s*\{)/, `$1\n "plugin": ["${pluginName}"],`)
|
||||
const newContent = content.replace(/^(\s*\{)/, `$1\n "plugin": ["${pluginEntry}"],`)
|
||||
writeFileSync(path, newContent)
|
||||
}
|
||||
} else {
|
||||
@@ -222,47 +311,74 @@ export function generateOmoConfig(installConfig: InstallConfig): Record<string,
|
||||
$schema: "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
}
|
||||
|
||||
if (installConfig.hasGemini) {
|
||||
config.google_auth = false
|
||||
}
|
||||
|
||||
const agents: Record<string, Record<string, unknown>> = {}
|
||||
|
||||
if (!installConfig.hasClaude) {
|
||||
agents["Sisyphus"] = { model: "opencode/big-pickle" }
|
||||
}
|
||||
|
||||
if (installConfig.hasGemini) {
|
||||
agents["librarian"] = { model: "google/gemini-3-flash" }
|
||||
agents["explore"] = { model: "google/gemini-3-flash" }
|
||||
} else if (installConfig.hasClaude && installConfig.isMax20) {
|
||||
agents["explore"] = { model: "anthropic/claude-haiku-4-5" }
|
||||
} else {
|
||||
agents["librarian"] = { model: "opencode/big-pickle" }
|
||||
agents["explore"] = { model: "opencode/big-pickle" }
|
||||
}
|
||||
|
||||
if (!installConfig.hasChatGPT) {
|
||||
agents["oracle"] = {
|
||||
model: installConfig.hasClaude ? "anthropic/claude-opus-4-5" : "opencode/big-pickle",
|
||||
agents["Sisyphus"] = {
|
||||
model: installConfig.hasCopilot ? "github-copilot/claude-opus-4.5" : "opencode/glm-4.7-free",
|
||||
}
|
||||
}
|
||||
|
||||
agents["librarian"] = { model: "opencode/glm-4.7-free" }
|
||||
|
||||
// Gemini models use `antigravity-` prefix for explicit Antigravity quota routing
|
||||
// @see ANTIGRAVITY_PROVIDER_CONFIG comments for rationale
|
||||
if (installConfig.hasGemini) {
|
||||
agents["frontend-ui-ux-engineer"] = { model: "google/gemini-3-pro-high" }
|
||||
agents["document-writer"] = { model: "google/gemini-3-flash" }
|
||||
agents["multimodal-looker"] = { model: "google/gemini-3-flash" }
|
||||
agents["explore"] = { model: "google/antigravity-gemini-3-flash" }
|
||||
} else if (installConfig.hasClaude && installConfig.isMax20) {
|
||||
agents["explore"] = { model: "anthropic/claude-haiku-4-5" }
|
||||
} else if (installConfig.hasCopilot) {
|
||||
agents["explore"] = { model: "github-copilot/grok-code-fast-1" }
|
||||
} else {
|
||||
const fallbackModel = installConfig.hasClaude ? "anthropic/claude-opus-4-5" : "opencode/big-pickle"
|
||||
agents["frontend-ui-ux-engineer"] = { model: fallbackModel }
|
||||
agents["document-writer"] = { model: fallbackModel }
|
||||
agents["multimodal-looker"] = { model: fallbackModel }
|
||||
agents["explore"] = { model: "opencode/glm-4.7-free" }
|
||||
}
|
||||
|
||||
if (!installConfig.hasChatGPT) {
|
||||
const oracleFallback = installConfig.hasCopilot
|
||||
? "github-copilot/gpt-5.2"
|
||||
: installConfig.hasClaude
|
||||
? "anthropic/claude-opus-4-5"
|
||||
: "opencode/glm-4.7-free"
|
||||
agents["oracle"] = { model: oracleFallback }
|
||||
}
|
||||
|
||||
if (installConfig.hasGemini) {
|
||||
agents["frontend-ui-ux-engineer"] = { model: "google/antigravity-gemini-3-pro-high" }
|
||||
agents["document-writer"] = { model: "google/antigravity-gemini-3-flash" }
|
||||
agents["multimodal-looker"] = { model: "google/antigravity-gemini-3-flash" }
|
||||
} else if (installConfig.hasClaude) {
|
||||
agents["frontend-ui-ux-engineer"] = { model: "anthropic/claude-opus-4-5" }
|
||||
agents["document-writer"] = { model: "anthropic/claude-opus-4-5" }
|
||||
agents["multimodal-looker"] = { model: "anthropic/claude-opus-4-5" }
|
||||
} else if (installConfig.hasCopilot) {
|
||||
agents["frontend-ui-ux-engineer"] = { model: "github-copilot/gemini-3-pro-preview" }
|
||||
agents["document-writer"] = { model: "github-copilot/gemini-3-flash-preview" }
|
||||
agents["multimodal-looker"] = { model: "github-copilot/gemini-3-flash-preview" }
|
||||
} else {
|
||||
agents["frontend-ui-ux-engineer"] = { model: "opencode/glm-4.7-free" }
|
||||
agents["document-writer"] = { model: "opencode/glm-4.7-free" }
|
||||
agents["multimodal-looker"] = { model: "opencode/glm-4.7-free" }
|
||||
}
|
||||
|
||||
if (Object.keys(agents).length > 0) {
|
||||
config.agents = agents
|
||||
}
|
||||
|
||||
// Categories: override model for Antigravity auth or GitHub Copilot fallback
|
||||
if (installConfig.hasGemini) {
|
||||
config.categories = {
|
||||
"visual-engineering": { model: "google/gemini-3-pro-high" },
|
||||
artistry: { model: "google/gemini-3-pro-high" },
|
||||
writing: { model: "google/gemini-3-flash-high" },
|
||||
}
|
||||
} else if (installConfig.hasCopilot) {
|
||||
config.categories = {
|
||||
"visual-engineering": { model: "github-copilot/gemini-3-pro-preview" },
|
||||
artistry: { model: "github-copilot/gemini-3-pro-preview" },
|
||||
writing: { model: "github-copilot/gemini-3-flash-preview" },
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -270,50 +386,51 @@ export function writeOmoConfig(installConfig: InstallConfig): ConfigMergeResult
|
||||
try {
|
||||
ensureConfigDir()
|
||||
} catch (err) {
|
||||
return { success: false, configPath: OPENCODE_CONFIG_DIR, error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
return { success: false, configPath: getConfigDir(), error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
}
|
||||
|
||||
const omoConfigPath = getOmoConfig()
|
||||
|
||||
try {
|
||||
const newConfig = generateOmoConfig(installConfig)
|
||||
|
||||
if (existsSync(OMO_CONFIG)) {
|
||||
if (existsSync(omoConfigPath)) {
|
||||
try {
|
||||
const stat = statSync(OMO_CONFIG)
|
||||
const content = readFileSync(OMO_CONFIG, "utf-8")
|
||||
const stat = statSync(omoConfigPath)
|
||||
const content = readFileSync(omoConfigPath, "utf-8")
|
||||
|
||||
if (stat.size === 0 || isEmptyOrWhitespace(content)) {
|
||||
writeFileSync(OMO_CONFIG, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
return { success: true, configPath: OMO_CONFIG }
|
||||
writeFileSync(omoConfigPath, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
return { success: true, configPath: omoConfigPath }
|
||||
}
|
||||
|
||||
const existing = parseJsonc<Record<string, unknown>>(content)
|
||||
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
||||
writeFileSync(OMO_CONFIG, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
return { success: true, configPath: OMO_CONFIG }
|
||||
writeFileSync(omoConfigPath, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
return { success: true, configPath: omoConfigPath }
|
||||
}
|
||||
|
||||
delete existing.agents
|
||||
const merged = deepMerge(existing, newConfig)
|
||||
writeFileSync(OMO_CONFIG, JSON.stringify(merged, null, 2) + "\n")
|
||||
writeFileSync(omoConfigPath, JSON.stringify(merged, null, 2) + "\n")
|
||||
} catch (parseErr) {
|
||||
if (parseErr instanceof SyntaxError) {
|
||||
writeFileSync(OMO_CONFIG, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
return { success: true, configPath: OMO_CONFIG }
|
||||
writeFileSync(omoConfigPath, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
return { success: true, configPath: omoConfigPath }
|
||||
}
|
||||
throw parseErr
|
||||
}
|
||||
} else {
|
||||
writeFileSync(OMO_CONFIG, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
writeFileSync(omoConfigPath, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
}
|
||||
|
||||
return { success: true, configPath: OMO_CONFIG }
|
||||
return { success: true, configPath: omoConfigPath }
|
||||
} catch (err) {
|
||||
return { success: false, configPath: OMO_CONFIG, error: formatErrorWithSuggestion(err, "write oh-my-opencode config") }
|
||||
return { success: false, configPath: omoConfigPath, error: formatErrorWithSuggestion(err, "write oh-my-opencode config") }
|
||||
}
|
||||
}
|
||||
|
||||
interface OpenCodeBinaryResult {
|
||||
binary: string
|
||||
binary: OpenCodeBinaryType
|
||||
version: string
|
||||
}
|
||||
|
||||
@@ -327,7 +444,9 @@ async function findOpenCodeBinaryWithVersion(): Promise<OpenCodeBinaryResult | n
|
||||
const output = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
if (proc.exitCode === 0) {
|
||||
return { binary, version: output.trim() }
|
||||
const version = output.trim()
|
||||
initConfigContext(binary, version)
|
||||
return { binary, version }
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
@@ -350,7 +469,7 @@ export async function addAuthPlugins(config: InstallConfig): Promise<ConfigMerge
|
||||
try {
|
||||
ensureConfigDir()
|
||||
} catch (err) {
|
||||
return { success: false, configPath: OPENCODE_CONFIG_DIR, error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
return { success: false, configPath: getConfigDir(), error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
@@ -376,11 +495,7 @@ export async function addAuthPlugins(config: InstallConfig): Promise<ConfigMerge
|
||||
}
|
||||
}
|
||||
|
||||
if (config.hasChatGPT) {
|
||||
if (!plugins.some((p) => p.startsWith("opencode-openai-codex-auth"))) {
|
||||
plugins.push("opencode-openai-codex-auth")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const newConfig = { ...(existingConfig ?? {}), plugin: plugins }
|
||||
writeFileSync(path, JSON.stringify(newConfig, null, 2) + "\n")
|
||||
@@ -390,46 +505,6 @@ export async function addAuthPlugins(config: InstallConfig): Promise<ConfigMerge
|
||||
}
|
||||
}
|
||||
|
||||
export function setupChatGPTHotfix(): ConfigMergeResult {
|
||||
try {
|
||||
ensureConfigDir()
|
||||
} catch (err) {
|
||||
return { success: false, configPath: OPENCODE_CONFIG_DIR, error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
}
|
||||
|
||||
try {
|
||||
let packageJson: Record<string, unknown> = {}
|
||||
if (existsSync(OPENCODE_PACKAGE_JSON)) {
|
||||
try {
|
||||
const stat = statSync(OPENCODE_PACKAGE_JSON)
|
||||
const content = readFileSync(OPENCODE_PACKAGE_JSON, "utf-8")
|
||||
|
||||
if (stat.size > 0 && !isEmptyOrWhitespace(content)) {
|
||||
packageJson = JSON.parse(content)
|
||||
if (typeof packageJson !== "object" || packageJson === null || Array.isArray(packageJson)) {
|
||||
packageJson = {}
|
||||
}
|
||||
}
|
||||
} catch (parseErr) {
|
||||
if (parseErr instanceof SyntaxError) {
|
||||
packageJson = {}
|
||||
} else {
|
||||
throw parseErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deps = (packageJson.dependencies ?? {}) as Record<string, string>
|
||||
deps["opencode-openai-codex-auth"] = CHATGPT_HOTFIX_REPO
|
||||
packageJson.dependencies = deps
|
||||
|
||||
writeFileSync(OPENCODE_PACKAGE_JSON, JSON.stringify(packageJson, null, 2) + "\n")
|
||||
return { success: true, configPath: OPENCODE_PACKAGE_JSON }
|
||||
} catch (err) {
|
||||
return { success: false, configPath: OPENCODE_PACKAGE_JSON, error: formatErrorWithSuggestion(err, "setup ChatGPT hotfix in package.json") }
|
||||
}
|
||||
}
|
||||
|
||||
export interface BunInstallResult {
|
||||
success: boolean
|
||||
timedOut?: boolean
|
||||
@@ -444,7 +519,7 @@ export async function runBunInstall(): Promise<boolean> {
|
||||
export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||
try {
|
||||
const proc = Bun.spawn(["bun", "install"], {
|
||||
cwd: OPENCODE_CONFIG_DIR,
|
||||
cwd: getConfigDir(),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
@@ -488,67 +563,55 @@ export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Antigravity Provider Configuration
|
||||
*
|
||||
* IMPORTANT: Model names MUST use `antigravity-` prefix for stability.
|
||||
*
|
||||
* The opencode-antigravity-auth plugin supports two naming conventions:
|
||||
* - `antigravity-gemini-3-pro-high` (RECOMMENDED, explicit Antigravity quota routing)
|
||||
* - `gemini-3-pro-high` (LEGACY, backward compatible but may break in future)
|
||||
*
|
||||
* Legacy names rely on Gemini CLI using `-preview` suffix for disambiguation.
|
||||
* If Google removes `-preview`, legacy names may route to wrong quota.
|
||||
*
|
||||
* @see https://github.com/NoeFabris/opencode-antigravity-auth#migration-guide-v127
|
||||
*/
|
||||
export const ANTIGRAVITY_PROVIDER_CONFIG = {
|
||||
google: {
|
||||
name: "Google",
|
||||
// NOTE: opencode-antigravity-auth expects full model specs (name/limit/modalities).
|
||||
// If these are incomplete, models may appear but fail at runtime (e.g. 404).
|
||||
models: {
|
||||
"gemini-3-pro-high": {
|
||||
"antigravity-gemini-3-pro-high": {
|
||||
name: "Gemini 3 Pro High (Antigravity)",
|
||||
thinking: true,
|
||||
attachment: true,
|
||||
limit: { context: 1048576, output: 65535 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
"gemini-3-pro-medium": {
|
||||
name: "Gemini 3 Pro Medium (Antigravity)",
|
||||
thinking: true,
|
||||
attachment: true,
|
||||
limit: { context: 1048576, output: 65535 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
"gemini-3-pro-low": {
|
||||
"antigravity-gemini-3-pro-low": {
|
||||
name: "Gemini 3 Pro Low (Antigravity)",
|
||||
thinking: true,
|
||||
attachment: true,
|
||||
limit: { context: 1048576, output: 65535 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
"gemini-3-flash": {
|
||||
"antigravity-gemini-3-flash": {
|
||||
name: "Gemini 3 Flash (Antigravity)",
|
||||
attachment: true,
|
||||
limit: { context: 1048576, output: 65536 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
"gemini-3-flash-lite": {
|
||||
name: "Gemini 3 Flash Lite (Antigravity)",
|
||||
attachment: true,
|
||||
limit: { context: 1048576, output: 65536 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const CODEX_PROVIDER_CONFIG = {
|
||||
openai: {
|
||||
name: "OpenAI",
|
||||
api: "codex",
|
||||
models: {
|
||||
"gpt-5.2": { name: "GPT-5.2" },
|
||||
"o3": { name: "o3", thinking: true },
|
||||
"o4-mini": { name: "o4-mini", thinking: true },
|
||||
"codex-1": { name: "Codex-1" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
export function addProviderConfig(config: InstallConfig): ConfigMergeResult {
|
||||
try {
|
||||
ensureConfigDir()
|
||||
} catch (err) {
|
||||
return { success: false, configPath: OPENCODE_CONFIG_DIR, error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
return { success: false, configPath: getConfigDir(), error: formatErrorWithSuggestion(err, "create config directory") }
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
@@ -572,10 +635,6 @@ export function addProviderConfig(config: InstallConfig): ConfigMergeResult {
|
||||
providers.google = ANTIGRAVITY_PROVIDER_CONFIG.google
|
||||
}
|
||||
|
||||
if (config.hasChatGPT) {
|
||||
providers.openai = CODEX_PROVIDER_CONFIG.openai
|
||||
}
|
||||
|
||||
if (Object.keys(providers).length > 0) {
|
||||
newConfig.provider = providers
|
||||
}
|
||||
@@ -588,7 +647,6 @@ export function addProviderConfig(config: InstallConfig): ConfigMergeResult {
|
||||
}
|
||||
|
||||
interface OmoConfigData {
|
||||
google_auth?: boolean
|
||||
agents?: Record<string, { model?: string }>
|
||||
}
|
||||
|
||||
@@ -599,6 +657,7 @@ export function detectCurrentConfig(): DetectedConfig {
|
||||
isMax20: true,
|
||||
hasChatGPT: true,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
}
|
||||
|
||||
const { format, path } = detectConfigFormat()
|
||||
@@ -620,19 +679,19 @@ export function detectCurrentConfig(): DetectedConfig {
|
||||
}
|
||||
|
||||
result.hasGemini = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
|
||||
result.hasChatGPT = plugins.some((p) => p.startsWith("opencode-openai-codex-auth"))
|
||||
|
||||
if (!existsSync(OMO_CONFIG)) {
|
||||
const omoConfigPath = getOmoConfig()
|
||||
if (!existsSync(omoConfigPath)) {
|
||||
return result
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = statSync(OMO_CONFIG)
|
||||
const stat = statSync(omoConfigPath)
|
||||
if (stat.size === 0) {
|
||||
return result
|
||||
}
|
||||
|
||||
const content = readFileSync(OMO_CONFIG, "utf-8")
|
||||
const content = readFileSync(omoConfigPath, "utf-8")
|
||||
if (isEmptyOrWhitespace(content)) {
|
||||
return result
|
||||
}
|
||||
@@ -644,23 +703,25 @@ export function detectCurrentConfig(): DetectedConfig {
|
||||
|
||||
const agents = omoConfig.agents ?? {}
|
||||
|
||||
if (agents["Sisyphus"]?.model === "opencode/big-pickle") {
|
||||
if (agents["Sisyphus"]?.model === "opencode/glm-4.7-free") {
|
||||
result.hasClaude = false
|
||||
result.isMax20 = false
|
||||
} else if (agents["librarian"]?.model === "opencode/big-pickle") {
|
||||
} else if (agents["librarian"]?.model === "opencode/glm-4.7-free") {
|
||||
result.hasClaude = true
|
||||
result.isMax20 = false
|
||||
}
|
||||
|
||||
if (agents["oracle"]?.model?.startsWith("anthropic/")) {
|
||||
result.hasChatGPT = false
|
||||
} else if (agents["oracle"]?.model === "opencode/big-pickle") {
|
||||
} else if (agents["oracle"]?.model === "opencode/glm-4.7-free") {
|
||||
result.hasChatGPT = false
|
||||
}
|
||||
|
||||
if (omoConfig.google_auth === false) {
|
||||
result.hasGemini = plugins.some((p) => p.startsWith("opencode-antigravity-auth"))
|
||||
}
|
||||
const hasAnyCopilotModel = Object.values(agents).some(
|
||||
(agent) => agent?.model?.startsWith("github-copilot/")
|
||||
)
|
||||
result.hasCopilot = hasAnyCopilotModel
|
||||
|
||||
} catch {
|
||||
/* intentionally empty - malformed omo config returns defaults from opencode config detection */
|
||||
}
|
||||
|
||||
@@ -3,15 +3,60 @@ import * as gh from "./gh"
|
||||
|
||||
describe("gh cli check", () => {
|
||||
describe("getGhCliInfo", () => {
|
||||
it("returns gh cli info structure", async () => {
|
||||
// #given
|
||||
// #when checking gh cli info
|
||||
const info = await gh.getGhCliInfo()
|
||||
function createProc(opts: { stdout?: string; stderr?: string; exitCode?: number }) {
|
||||
const stdoutText = opts.stdout ?? ""
|
||||
const stderrText = opts.stderr ?? ""
|
||||
const exitCode = opts.exitCode ?? 0
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
// #then should return valid info structure
|
||||
expect(typeof info.installed).toBe("boolean")
|
||||
expect(info.authenticated === true || info.authenticated === false).toBe(true)
|
||||
expect(Array.isArray(info.scopes)).toBe(true)
|
||||
return {
|
||||
stdout: new ReadableStream({
|
||||
start(controller) {
|
||||
if (stdoutText) controller.enqueue(encoder.encode(stdoutText))
|
||||
controller.close()
|
||||
},
|
||||
}),
|
||||
stderr: new ReadableStream({
|
||||
start(controller) {
|
||||
if (stderrText) controller.enqueue(encoder.encode(stderrText))
|
||||
controller.close()
|
||||
},
|
||||
}),
|
||||
exited: Promise.resolve(exitCode),
|
||||
exitCode,
|
||||
} as unknown as ReturnType<typeof Bun.spawn>
|
||||
}
|
||||
|
||||
it("returns gh cli info structure", async () => {
|
||||
const spawnSpy = spyOn(Bun, "spawn").mockImplementation((cmd) => {
|
||||
if (Array.isArray(cmd) && cmd[0] === "which" && cmd[1] === "gh") {
|
||||
return createProc({ stdout: "/usr/bin/gh\n" })
|
||||
}
|
||||
|
||||
if (Array.isArray(cmd) && cmd[0] === "gh" && cmd[1] === "--version") {
|
||||
return createProc({ stdout: "gh version 2.40.0\n" })
|
||||
}
|
||||
|
||||
if (Array.isArray(cmd) && cmd[0] === "gh" && cmd[1] === "auth" && cmd[2] === "status") {
|
||||
return createProc({
|
||||
exitCode: 0,
|
||||
stderr: "Logged in to github.com account octocat (keyring)\nToken scopes: 'repo', 'read:org'\n",
|
||||
})
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected Bun.spawn call: ${Array.isArray(cmd) ? cmd.join(" ") : String(cmd)}`)
|
||||
})
|
||||
|
||||
try {
|
||||
const info = await gh.getGhCliInfo()
|
||||
|
||||
expect(info.installed).toBe(true)
|
||||
expect(info.version).toBe("2.40.0")
|
||||
expect(typeof info.authenticated).toBe("boolean")
|
||||
expect(Array.isArray(info.scopes)).toBe(true)
|
||||
} finally {
|
||||
spawnSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -17,6 +17,23 @@ describe("lsp check", () => {
|
||||
expect(Array.isArray(s.extensions)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it("does not spawn 'which' command (windows compatibility)", async () => {
|
||||
// #given
|
||||
const spawnSpy = spyOn(Bun, "spawn")
|
||||
|
||||
try {
|
||||
// #when getting servers info
|
||||
await lsp.getLspServersInfo()
|
||||
|
||||
// #then should not spawn which
|
||||
const calls = spawnSpy.mock.calls
|
||||
const whichCalls = calls.filter((c) => Array.isArray(c) && Array.isArray(c[0]) && c[0][0] === "which")
|
||||
expect(whichCalls.length).toBe(0)
|
||||
} finally {
|
||||
spawnSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("getLspServerStats", () => {
|
||||
|
||||
@@ -12,21 +12,13 @@ const DEFAULT_LSP_SERVERS: Array<{
|
||||
{ id: "gopls", binary: "gopls", extensions: [".go"] },
|
||||
]
|
||||
|
||||
async function checkBinaryExists(binary: string): Promise<boolean> {
|
||||
try {
|
||||
const proc = Bun.spawn(["which", binary], { stdout: "pipe", stderr: "pipe" })
|
||||
await proc.exited
|
||||
return proc.exitCode === 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
import { isServerInstalled } from "../../../tools/lsp/config"
|
||||
|
||||
export async function getLspServersInfo(): Promise<LspServerInfo[]> {
|
||||
const servers: LspServerInfo[] = []
|
||||
|
||||
for (const server of DEFAULT_LSP_SERVERS) {
|
||||
const installed = await checkBinaryExists(server.binary)
|
||||
const installed = isServerInstalled([server.binary])
|
||||
servers.push({
|
||||
id: server.id,
|
||||
installed,
|
||||
|
||||
@@ -9,11 +9,10 @@ describe("mcp check", () => {
|
||||
const servers = mcp.getBuiltinMcpInfo()
|
||||
|
||||
// #then should include expected servers
|
||||
expect(servers.length).toBe(3)
|
||||
expect(servers.length).toBe(2)
|
||||
expect(servers.every((s) => s.type === "builtin")).toBe(true)
|
||||
expect(servers.every((s) => s.enabled === true)).toBe(true)
|
||||
expect(servers.map((s) => s.id)).toContain("context7")
|
||||
expect(servers.map((s) => s.id)).toContain("websearch_exa")
|
||||
expect(servers.map((s) => s.id)).toContain("grep_app")
|
||||
})
|
||||
})
|
||||
@@ -37,7 +36,7 @@ describe("mcp check", () => {
|
||||
|
||||
// #then should pass
|
||||
expect(result.status).toBe("pass")
|
||||
expect(result.message).toContain("3")
|
||||
expect(result.message).toContain("2")
|
||||
expect(result.message).toContain("enabled")
|
||||
})
|
||||
|
||||
@@ -48,7 +47,6 @@ describe("mcp check", () => {
|
||||
|
||||
// #then should list servers
|
||||
expect(result.details?.some((d) => d.includes("context7"))).toBe(true)
|
||||
expect(result.details?.some((d) => d.includes("websearch_exa"))).toBe(true)
|
||||
expect(result.details?.some((d) => d.includes("grep_app"))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { CheckResult, CheckDefinition, McpServerInfo } from "../types"
|
||||
import { CHECK_IDS, CHECK_NAMES } from "../constants"
|
||||
import { parseJsonc } from "../../../shared"
|
||||
|
||||
const BUILTIN_MCP_SERVERS = ["context7", "websearch_exa", "grep_app"]
|
||||
const BUILTIN_MCP_SERVERS = ["context7", "grep_app"]
|
||||
|
||||
const MCP_CONFIG_PATHS = [
|
||||
join(homedir(), ".claude", ".mcp.json"),
|
||||
|
||||
@@ -43,6 +43,94 @@ describe("opencode check", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("command helpers", () => {
|
||||
it("selects where on Windows", () => {
|
||||
// #given win32 platform
|
||||
// #when selecting lookup command
|
||||
// #then should use where
|
||||
expect(opencode.getBinaryLookupCommand("win32")).toBe("where")
|
||||
})
|
||||
|
||||
it("selects which on non-Windows", () => {
|
||||
// #given linux platform
|
||||
// #when selecting lookup command
|
||||
// #then should use which
|
||||
expect(opencode.getBinaryLookupCommand("linux")).toBe("which")
|
||||
expect(opencode.getBinaryLookupCommand("darwin")).toBe("which")
|
||||
})
|
||||
|
||||
it("parses command output into paths", () => {
|
||||
// #given raw output with multiple lines and spaces
|
||||
const output = "C:\\\\bin\\\\opencode.ps1\r\nC:\\\\bin\\\\opencode.exe\n\n"
|
||||
|
||||
// #when parsing
|
||||
const paths = opencode.parseBinaryPaths(output)
|
||||
|
||||
// #then should return trimmed, non-empty paths
|
||||
expect(paths).toEqual(["C:\\\\bin\\\\opencode.ps1", "C:\\\\bin\\\\opencode.exe"])
|
||||
})
|
||||
|
||||
it("prefers exe/cmd/bat over ps1 on Windows", () => {
|
||||
// #given windows paths
|
||||
const paths = [
|
||||
"C:\\\\bin\\\\opencode.ps1",
|
||||
"C:\\\\bin\\\\opencode.cmd",
|
||||
"C:\\\\bin\\\\opencode.exe",
|
||||
]
|
||||
|
||||
// #when selecting binary
|
||||
const selected = opencode.selectBinaryPath(paths, "win32")
|
||||
|
||||
// #then should prefer exe
|
||||
expect(selected).toBe("C:\\\\bin\\\\opencode.exe")
|
||||
})
|
||||
|
||||
it("falls back to ps1 when it is the only Windows candidate", () => {
|
||||
// #given only ps1 path
|
||||
const paths = ["C:\\\\bin\\\\opencode.ps1"]
|
||||
|
||||
// #when selecting binary
|
||||
const selected = opencode.selectBinaryPath(paths, "win32")
|
||||
|
||||
// #then should return ps1 path
|
||||
expect(selected).toBe("C:\\\\bin\\\\opencode.ps1")
|
||||
})
|
||||
|
||||
it("builds PowerShell command for ps1 on Windows", () => {
|
||||
// #given a ps1 path on Windows
|
||||
const command = opencode.buildVersionCommand(
|
||||
"C:\\\\bin\\\\opencode.ps1",
|
||||
"win32"
|
||||
)
|
||||
|
||||
// #when building command
|
||||
// #then should use PowerShell
|
||||
expect(command).toEqual([
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
"C:\\\\bin\\\\opencode.ps1",
|
||||
"--version",
|
||||
])
|
||||
})
|
||||
|
||||
it("builds direct command for non-ps1 binaries", () => {
|
||||
// #given an exe on Windows and a binary on linux
|
||||
const winCommand = opencode.buildVersionCommand(
|
||||
"C:\\\\bin\\\\opencode.exe",
|
||||
"win32"
|
||||
)
|
||||
const linuxCommand = opencode.buildVersionCommand("opencode", "linux")
|
||||
|
||||
// #when building commands
|
||||
// #then should execute directly
|
||||
expect(winCommand).toEqual(["C:\\\\bin\\\\opencode.exe", "--version"])
|
||||
expect(linuxCommand).toEqual(["opencode", "--version"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("getOpenCodeInfo", () => {
|
||||
it("returns installed: false when binary not found", async () => {
|
||||
// #given no opencode binary
|
||||
|
||||
@@ -1,14 +1,70 @@
|
||||
import type { CheckResult, CheckDefinition, OpenCodeInfo } from "../types"
|
||||
import { CHECK_IDS, CHECK_NAMES, MIN_OPENCODE_VERSION, OPENCODE_BINARIES } from "../constants"
|
||||
|
||||
const WINDOWS_EXECUTABLE_EXTS = [".exe", ".cmd", ".bat", ".ps1"]
|
||||
|
||||
export function getBinaryLookupCommand(platform: NodeJS.Platform): "which" | "where" {
|
||||
return platform === "win32" ? "where" : "which"
|
||||
}
|
||||
|
||||
export function parseBinaryPaths(output: string): string[] {
|
||||
return output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
}
|
||||
|
||||
export function selectBinaryPath(
|
||||
paths: string[],
|
||||
platform: NodeJS.Platform
|
||||
): string | null {
|
||||
if (paths.length === 0) return null
|
||||
if (platform !== "win32") return paths[0]
|
||||
|
||||
const normalized = paths.map((path) => path.toLowerCase())
|
||||
for (const ext of WINDOWS_EXECUTABLE_EXTS) {
|
||||
const index = normalized.findIndex((path) => path.endsWith(ext))
|
||||
if (index !== -1) return paths[index]
|
||||
}
|
||||
|
||||
return paths[0]
|
||||
}
|
||||
|
||||
export function buildVersionCommand(
|
||||
binaryPath: string,
|
||||
platform: NodeJS.Platform
|
||||
): string[] {
|
||||
if (
|
||||
platform === "win32" &&
|
||||
binaryPath.toLowerCase().endsWith(".ps1")
|
||||
) {
|
||||
return [
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
binaryPath,
|
||||
"--version",
|
||||
]
|
||||
}
|
||||
|
||||
return [binaryPath, "--version"]
|
||||
}
|
||||
|
||||
export async function findOpenCodeBinary(): Promise<{ binary: string; path: string } | null> {
|
||||
for (const binary of OPENCODE_BINARIES) {
|
||||
try {
|
||||
const proc = Bun.spawn(["which", binary], { stdout: "pipe", stderr: "pipe" })
|
||||
const lookupCommand = getBinaryLookupCommand(process.platform)
|
||||
const proc = Bun.spawn([lookupCommand, binary], { stdout: "pipe", stderr: "pipe" })
|
||||
const output = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
if (proc.exitCode === 0) {
|
||||
return { binary, path: output.trim() }
|
||||
const paths = parseBinaryPaths(output)
|
||||
const selectedPath = selectBinaryPath(paths, process.platform)
|
||||
if (selectedPath) {
|
||||
return { binary, path: selectedPath }
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
@@ -17,9 +73,13 @@ export async function findOpenCodeBinary(): Promise<{ binary: string; path: stri
|
||||
return null
|
||||
}
|
||||
|
||||
export async function getOpenCodeVersion(binary: string): Promise<string | null> {
|
||||
export async function getOpenCodeVersion(
|
||||
binaryPath: string,
|
||||
platform: NodeJS.Platform = process.platform
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const proc = Bun.spawn([binary, "--version"], { stdout: "pipe", stderr: "pipe" })
|
||||
const command = buildVersionCommand(binaryPath, platform)
|
||||
const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" })
|
||||
const output = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
if (proc.exitCode === 0) {
|
||||
@@ -61,7 +121,7 @@ export async function getOpenCodeInfo(): Promise<OpenCodeInfo> {
|
||||
}
|
||||
}
|
||||
|
||||
const version = await getOpenCodeVersion(binaryInfo.binary)
|
||||
const version = await getOpenCodeVersion(binaryInfo.path ?? binaryInfo.binary)
|
||||
|
||||
return {
|
||||
installed: true,
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import type { CheckResult, CheckDefinition, PluginInfo } from "../types"
|
||||
import { CHECK_IDS, CHECK_NAMES, PACKAGE_NAME } from "../constants"
|
||||
import { parseJsonc } from "../../../shared"
|
||||
|
||||
const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode")
|
||||
const OPENCODE_JSON = join(OPENCODE_CONFIG_DIR, "opencode.json")
|
||||
const OPENCODE_JSONC = join(OPENCODE_CONFIG_DIR, "opencode.jsonc")
|
||||
import { parseJsonc, getOpenCodeConfigPaths } from "../../../shared"
|
||||
|
||||
function detectConfigPath(): { path: string; format: "json" | "jsonc" } | null {
|
||||
if (existsSync(OPENCODE_JSONC)) {
|
||||
return { path: OPENCODE_JSONC, format: "jsonc" }
|
||||
const paths = getOpenCodeConfigPaths({ binary: "opencode", version: null })
|
||||
|
||||
if (existsSync(paths.configJsonc)) {
|
||||
return { path: paths.configJsonc, format: "jsonc" }
|
||||
}
|
||||
if (existsSync(OPENCODE_JSON)) {
|
||||
return { path: OPENCODE_JSON, format: "json" }
|
||||
if (existsSync(paths.configJson)) {
|
||||
return { path: paths.configJson, format: "json" }
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -81,13 +77,14 @@ export async function checkPluginRegistration(): Promise<CheckResult> {
|
||||
const info = getPluginInfo()
|
||||
|
||||
if (!info.configPath) {
|
||||
const expectedPaths = getOpenCodeConfigPaths({ binary: "opencode", version: null })
|
||||
return {
|
||||
name: CHECK_NAMES[CHECK_IDS.PLUGIN_REGISTRATION],
|
||||
status: "fail",
|
||||
message: "OpenCode config file not found",
|
||||
details: [
|
||||
"Run: bunx oh-my-opencode install",
|
||||
`Expected: ${OPENCODE_JSON} or ${OPENCODE_JSONC}`,
|
||||
`Expected: ${expectedPaths.configJson} or ${expectedPaths.configJsonc}`,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,9 @@ export async function getVersionInfo(): Promise<VersionCheckInfo> {
|
||||
}
|
||||
|
||||
const currentVersion = getCachedVersion()
|
||||
const latestVersion = await getLatestVersion()
|
||||
const { extractChannel } = await import("../../../hooks/auto-update-checker/index")
|
||||
const channel = extractChannel(pluginInfo?.pinnedVersion ?? currentVersion)
|
||||
const latestVersion = await getLatestVersion(channel)
|
||||
|
||||
const isUpToDate =
|
||||
!currentVersion ||
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("runner", () => {
|
||||
name: "Test Check",
|
||||
category: "installation",
|
||||
check: async () => {
|
||||
await new Promise((r) => setTimeout(r, 10))
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
return { name: "Test", status: "pass", message: "OK" }
|
||||
},
|
||||
}
|
||||
|
||||
@@ -54,7 +54,9 @@ export async function getLocalVersion(options: GetLocalVersionOptions = {}): Pro
|
||||
return 1
|
||||
}
|
||||
|
||||
const latestVersion = await getLatestVersion()
|
||||
const { extractChannel } = await import("../../hooks/auto-update-checker/index")
|
||||
const channel = extractChannel(pluginInfo?.pinnedVersion ?? currentVersion)
|
||||
const latestVersion = await getLatestVersion(channel)
|
||||
|
||||
if (!latestVersion) {
|
||||
const info: VersionInfo = {
|
||||
|
||||
@@ -8,8 +8,8 @@ import type { InstallArgs } from "./types"
|
||||
import type { RunOptions } from "./run"
|
||||
import type { GetLocalVersionOptions } from "./get-local-version/types"
|
||||
import type { DoctorOptions } from "./doctor"
|
||||
import packageJson from "../../package.json" with { type: "json" }
|
||||
|
||||
const packageJson = await import("../../package.json")
|
||||
const VERSION = packageJson.version
|
||||
|
||||
const program = new Command()
|
||||
@@ -26,12 +26,13 @@ program
|
||||
.option("--claude <value>", "Claude subscription: no, yes, max20")
|
||||
.option("--chatgpt <value>", "ChatGPT subscription: no, yes")
|
||||
.option("--gemini <value>", "Gemini integration: no, yes")
|
||||
.option("--copilot <value>", "GitHub Copilot subscription: no, yes")
|
||||
.option("--skip-auth", "Skip authentication setup hints")
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ bunx oh-my-opencode install
|
||||
$ bunx oh-my-opencode install --no-tui --claude=max20 --chatgpt=yes --gemini=yes
|
||||
$ bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no
|
||||
$ bunx oh-my-opencode install --no-tui --claude=max20 --chatgpt=yes --gemini=yes --copilot=no
|
||||
$ bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no --copilot=yes
|
||||
|
||||
Model Providers:
|
||||
Claude Required for Sisyphus (main orchestrator) and Librarian agents
|
||||
@@ -44,6 +45,7 @@ Model Providers:
|
||||
claude: options.claude,
|
||||
chatgpt: options.chatgpt,
|
||||
gemini: options.gemini,
|
||||
copilot: options.copilot,
|
||||
skipAuth: options.skipAuth ?? false,
|
||||
}
|
||||
const exitCode = await install(args)
|
||||
|
||||
@@ -7,11 +7,12 @@ import {
|
||||
isOpenCodeInstalled,
|
||||
getOpenCodeVersion,
|
||||
addAuthPlugins,
|
||||
setupChatGPTHotfix,
|
||||
runBunInstall,
|
||||
addProviderConfig,
|
||||
detectCurrentConfig,
|
||||
} from "./config-manager"
|
||||
import packageJson from "../../package.json" with { type: "json" }
|
||||
|
||||
const VERSION = packageJson.version
|
||||
|
||||
const SYMBOLS = {
|
||||
check: color.green("✓"),
|
||||
@@ -40,6 +41,7 @@ function formatConfigSummary(config: InstallConfig): string {
|
||||
lines.push(formatProvider("Claude", config.hasClaude, claudeDetail))
|
||||
lines.push(formatProvider("ChatGPT", config.hasChatGPT))
|
||||
lines.push(formatProvider("Gemini", config.hasGemini))
|
||||
lines.push(formatProvider("GitHub Copilot", config.hasCopilot, "fallback provider"))
|
||||
|
||||
lines.push("")
|
||||
lines.push(color.dim("─".repeat(40)))
|
||||
@@ -48,10 +50,10 @@ function formatConfigSummary(config: InstallConfig): string {
|
||||
lines.push(color.bold(color.white("Agent Configuration")))
|
||||
lines.push("")
|
||||
|
||||
const sisyphusModel = config.hasClaude ? "claude-opus-4-5" : "big-pickle"
|
||||
const oracleModel = config.hasChatGPT ? "gpt-5.2" : (config.hasClaude ? "claude-opus-4-5" : "big-pickle")
|
||||
const librarianModel = config.hasClaude && config.isMax20 ? "claude-sonnet-4-5" : "big-pickle"
|
||||
const frontendModel = config.hasGemini ? "gemini-3-pro-high" : (config.hasClaude ? "claude-opus-4-5" : "big-pickle")
|
||||
const sisyphusModel = config.hasClaude ? "claude-opus-4-5" : (config.hasCopilot ? "github-copilot/claude-opus-4.5" : "glm-4.7-free")
|
||||
const oracleModel = config.hasChatGPT ? "gpt-5.2" : (config.hasCopilot ? "github-copilot/gpt-5.2" : (config.hasClaude ? "claude-opus-4-5" : "glm-4.7-free"))
|
||||
const librarianModel = "glm-4.7-free"
|
||||
const frontendModel = config.hasGemini ? "antigravity-gemini-3-pro-high" : (config.hasClaude ? "claude-opus-4-5" : "glm-4.7-free")
|
||||
|
||||
lines.push(` ${SYMBOLS.bullet} Sisyphus ${SYMBOLS.arrow} ${color.cyan(sisyphusModel)}`)
|
||||
lines.push(` ${SYMBOLS.bullet} Oracle ${SYMBOLS.arrow} ${color.cyan(oracleModel)}`)
|
||||
@@ -132,6 +134,12 @@ function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: string
|
||||
errors.push(`Invalid --gemini value: ${args.gemini} (expected: no, yes)`)
|
||||
}
|
||||
|
||||
if (args.copilot === undefined) {
|
||||
errors.push("--copilot is required (values: no, yes)")
|
||||
} else if (!["no", "yes"].includes(args.copilot)) {
|
||||
errors.push(`Invalid --copilot value: ${args.copilot} (expected: no, yes)`)
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors }
|
||||
}
|
||||
|
||||
@@ -141,10 +149,11 @@ function argsToConfig(args: InstallArgs): InstallConfig {
|
||||
isMax20: args.claude === "max20",
|
||||
hasChatGPT: args.chatgpt === "yes",
|
||||
hasGemini: args.gemini === "yes",
|
||||
hasCopilot: args.copilot === "yes",
|
||||
}
|
||||
}
|
||||
|
||||
function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubscription; chatgpt: BooleanArg; gemini: BooleanArg } {
|
||||
function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubscription; chatgpt: BooleanArg; gemini: BooleanArg; copilot: BooleanArg } {
|
||||
let claude: ClaudeSubscription = "no"
|
||||
if (detected.hasClaude) {
|
||||
claude = detected.isMax20 ? "max20" : "yes"
|
||||
@@ -154,6 +163,7 @@ function detectedToInitialValues(detected: DetectedConfig): { claude: ClaudeSubs
|
||||
claude,
|
||||
chatgpt: detected.hasChatGPT ? "yes" : "no",
|
||||
gemini: detected.hasGemini ? "yes" : "no",
|
||||
copilot: detected.hasCopilot ? "yes" : "no",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +173,7 @@ async function runTuiMode(detected: DetectedConfig): Promise<InstallConfig | nul
|
||||
const claude = await p.select({
|
||||
message: "Do you have a Claude Pro/Max subscription?",
|
||||
options: [
|
||||
{ value: "no" as const, label: "No", hint: "Will use opencode/big-pickle as fallback" },
|
||||
{ value: "no" as const, label: "No", hint: "Will use opencode/glm-4.7-free as fallback" },
|
||||
{ value: "yes" as const, label: "Yes (standard)", hint: "Claude Opus 4.5 for orchestration" },
|
||||
{ value: "max20" as const, label: "Yes (max20 mode)", hint: "Full power with Claude Sonnet 4.5 for Librarian" },
|
||||
],
|
||||
@@ -203,11 +213,26 @@ async function runTuiMode(detected: DetectedConfig): Promise<InstallConfig | nul
|
||||
return null
|
||||
}
|
||||
|
||||
const copilot = await p.select({
|
||||
message: "Do you have a GitHub Copilot subscription?",
|
||||
options: [
|
||||
{ value: "no" as const, label: "No", hint: "Only native providers will be used" },
|
||||
{ value: "yes" as const, label: "Yes", hint: "Fallback option when native providers unavailable" },
|
||||
],
|
||||
initialValue: initial.copilot,
|
||||
})
|
||||
|
||||
if (p.isCancel(copilot)) {
|
||||
p.cancel("Installation cancelled.")
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
hasClaude: claude !== "no",
|
||||
isMax20: claude === "max20",
|
||||
hasChatGPT: chatgpt === "yes",
|
||||
hasGemini: gemini === "yes",
|
||||
hasCopilot: copilot === "yes",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +245,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
|
||||
console.log(` ${SYMBOLS.bullet} ${err}`)
|
||||
}
|
||||
console.log()
|
||||
printInfo("Usage: bunx oh-my-opencode install --no-tui --claude=<no|yes|max20> --chatgpt=<no|yes> --gemini=<no|yes>")
|
||||
printInfo("Usage: bunx oh-my-opencode install --no-tui --claude=<no|yes|max20> --chatgpt=<no|yes> --gemini=<no|yes> --copilot=<no|yes>")
|
||||
console.log()
|
||||
return 1
|
||||
}
|
||||
@@ -252,14 +277,14 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
|
||||
const config = argsToConfig(args)
|
||||
|
||||
printStep(step++, totalSteps, "Adding oh-my-opencode plugin...")
|
||||
const pluginResult = addPluginToOpenCodeConfig()
|
||||
const pluginResult = await addPluginToOpenCodeConfig(VERSION)
|
||||
if (!pluginResult.success) {
|
||||
printError(`Failed: ${pluginResult.error}`)
|
||||
return 1
|
||||
}
|
||||
printSuccess(`Plugin ${isUpdate ? "verified" : "added"} ${SYMBOLS.arrow} ${color.dim(pluginResult.configPath)}`)
|
||||
|
||||
if (config.hasGemini || config.hasChatGPT) {
|
||||
if (config.hasGemini) {
|
||||
printStep(step++, totalSteps, "Adding auth plugins...")
|
||||
const authResult = await addAuthPlugins(config)
|
||||
if (!authResult.success) {
|
||||
@@ -279,26 +304,6 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
|
||||
step += 2
|
||||
}
|
||||
|
||||
if (config.hasChatGPT) {
|
||||
printStep(step++, totalSteps, "Setting up ChatGPT hotfix...")
|
||||
const hotfixResult = setupChatGPTHotfix()
|
||||
if (!hotfixResult.success) {
|
||||
printError(`Failed: ${hotfixResult.error}`)
|
||||
return 1
|
||||
}
|
||||
printSuccess(`Hotfix configured ${SYMBOLS.arrow} ${color.dim(hotfixResult.configPath)}`)
|
||||
|
||||
printInfo("Installing dependencies with bun...")
|
||||
const bunSuccess = await runBunInstall()
|
||||
if (bunSuccess) {
|
||||
printSuccess("Dependencies installed")
|
||||
} else {
|
||||
printWarning("bun install failed - run manually: cd ~/.config/opencode && bun i")
|
||||
}
|
||||
} else {
|
||||
step++
|
||||
}
|
||||
|
||||
printStep(step++, totalSteps, "Writing oh-my-opencode configuration...")
|
||||
const omoResult = writeOmoConfig(config)
|
||||
if (!omoResult.success) {
|
||||
@@ -309,23 +314,8 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
|
||||
|
||||
printBox(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
|
||||
|
||||
if (!config.hasClaude && !config.hasChatGPT && !config.hasGemini) {
|
||||
printWarning("No model providers configured. Using opencode/big-pickle as fallback.")
|
||||
}
|
||||
|
||||
if ((config.hasClaude || config.hasChatGPT || config.hasGemini) && !args.skipAuth) {
|
||||
console.log(color.bold("Next Steps - Authenticate your providers:"))
|
||||
console.log()
|
||||
if (config.hasClaude) {
|
||||
console.log(` ${SYMBOLS.arrow} ${color.dim("opencode auth login")} ${color.gray("(select Anthropic → Claude Pro/Max)")}`)
|
||||
}
|
||||
if (config.hasChatGPT) {
|
||||
console.log(` ${SYMBOLS.arrow} ${color.dim("opencode auth login")} ${color.gray("(select OpenAI → ChatGPT Plus/Pro)")}`)
|
||||
}
|
||||
if (config.hasGemini) {
|
||||
console.log(` ${SYMBOLS.arrow} ${color.dim("opencode auth login")} ${color.gray("(select Google → OAuth with Antigravity)")}`)
|
||||
}
|
||||
console.log()
|
||||
if (!config.hasClaude && !config.hasChatGPT && !config.hasGemini && !config.hasCopilot) {
|
||||
printWarning("No model providers configured. Using opencode/glm-4.7-free as fallback.")
|
||||
}
|
||||
|
||||
console.log(`${SYMBOLS.star} ${color.bold(color.green(isUpdate ? "Configuration updated!" : "Installation complete!"))}`)
|
||||
@@ -345,6 +335,17 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
|
||||
console.log(color.dim("oMoMoMoMo... Enjoy!"))
|
||||
console.log()
|
||||
|
||||
if ((config.hasClaude || config.hasChatGPT || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||
printBox(
|
||||
`Run ${color.cyan("opencode auth login")} and select your provider:\n` +
|
||||
(config.hasClaude ? ` ${SYMBOLS.bullet} Anthropic ${color.gray("→ Claude Pro/Max")}\n` : "") +
|
||||
(config.hasChatGPT ? ` ${SYMBOLS.bullet} OpenAI ${color.gray("→ ChatGPT Plus/Pro")}\n` : "") +
|
||||
(config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\n` : "") +
|
||||
(config.hasCopilot ? ` ${SYMBOLS.bullet} GitHub ${color.gray("→ Copilot")}` : ""),
|
||||
"🔐 Authenticate Your Providers"
|
||||
)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -382,7 +383,7 @@ export async function install(args: InstallArgs): Promise<number> {
|
||||
if (!config) return 1
|
||||
|
||||
s.start("Adding oh-my-opencode to OpenCode config")
|
||||
const pluginResult = addPluginToOpenCodeConfig()
|
||||
const pluginResult = await addPluginToOpenCodeConfig(VERSION)
|
||||
if (!pluginResult.success) {
|
||||
s.stop(`Failed to add plugin: ${pluginResult.error}`)
|
||||
p.outro(color.red("Installation failed."))
|
||||
@@ -390,7 +391,7 @@ export async function install(args: InstallArgs): Promise<number> {
|
||||
}
|
||||
s.stop(`Plugin added to ${color.cyan(pluginResult.configPath)}`)
|
||||
|
||||
if (config.hasGemini || config.hasChatGPT) {
|
||||
if (config.hasGemini) {
|
||||
s.start("Adding auth plugins (fetching latest versions)")
|
||||
const authResult = await addAuthPlugins(config)
|
||||
if (!authResult.success) {
|
||||
@@ -410,25 +411,6 @@ export async function install(args: InstallArgs): Promise<number> {
|
||||
s.stop(`Provider config added to ${color.cyan(providerResult.configPath)}`)
|
||||
}
|
||||
|
||||
if (config.hasChatGPT) {
|
||||
s.start("Setting up ChatGPT hotfix")
|
||||
const hotfixResult = setupChatGPTHotfix()
|
||||
if (!hotfixResult.success) {
|
||||
s.stop(`Failed to setup hotfix: ${hotfixResult.error}`)
|
||||
p.outro(color.red("Installation failed."))
|
||||
return 1
|
||||
}
|
||||
s.stop(`Hotfix configured in ${color.cyan(hotfixResult.configPath)}`)
|
||||
|
||||
s.start("Installing dependencies with bun")
|
||||
const bunSuccess = await runBunInstall()
|
||||
if (bunSuccess) {
|
||||
s.stop("Dependencies installed")
|
||||
} else {
|
||||
s.stop(color.yellow("bun install failed - run manually: cd ~/.config/opencode && bun i"))
|
||||
}
|
||||
}
|
||||
|
||||
s.start("Writing oh-my-opencode configuration")
|
||||
const omoResult = writeOmoConfig(config)
|
||||
if (!omoResult.success) {
|
||||
@@ -438,26 +420,12 @@ export async function install(args: InstallArgs): Promise<number> {
|
||||
}
|
||||
s.stop(`Config written to ${color.cyan(omoResult.configPath)}`)
|
||||
|
||||
if (!config.hasClaude && !config.hasChatGPT && !config.hasGemini) {
|
||||
p.log.warn("No model providers configured. Using opencode/big-pickle as fallback.")
|
||||
if (!config.hasClaude && !config.hasChatGPT && !config.hasGemini && !config.hasCopilot) {
|
||||
p.log.warn("No model providers configured. Using opencode/glm-4.7-free as fallback.")
|
||||
}
|
||||
|
||||
p.note(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
|
||||
|
||||
if ((config.hasClaude || config.hasChatGPT || config.hasGemini) && !args.skipAuth) {
|
||||
const steps: string[] = []
|
||||
if (config.hasClaude) {
|
||||
steps.push(`${color.dim("opencode auth login")} ${color.gray("(select Anthropic → Claude Pro/Max)")}`)
|
||||
}
|
||||
if (config.hasChatGPT) {
|
||||
steps.push(`${color.dim("opencode auth login")} ${color.gray("(select OpenAI → ChatGPT Plus/Pro)")}`)
|
||||
}
|
||||
if (config.hasGemini) {
|
||||
steps.push(`${color.dim("opencode auth login")} ${color.gray("(select Google → OAuth with Antigravity)")}`)
|
||||
}
|
||||
p.note(steps.join("\n"), "Next Steps - Authenticate your providers")
|
||||
}
|
||||
|
||||
p.log.success(color.bold(isUpdate ? "Configuration updated!" : "Installation complete!"))
|
||||
p.log.message(`Run ${color.cyan("opencode")} to start!`)
|
||||
|
||||
@@ -473,5 +441,22 @@ export async function install(args: InstallArgs): Promise<number> {
|
||||
|
||||
p.outro(color.green("oMoMoMoMo... Enjoy!"))
|
||||
|
||||
if ((config.hasClaude || config.hasChatGPT || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||
const providers: string[] = []
|
||||
if (config.hasClaude) providers.push(`Anthropic ${color.gray("→ Claude Pro/Max")}`)
|
||||
if (config.hasChatGPT) providers.push(`OpenAI ${color.gray("→ ChatGPT Plus/Pro")}`)
|
||||
if (config.hasGemini) providers.push(`Google ${color.gray("→ OAuth with Antigravity")}`)
|
||||
if (config.hasCopilot) providers.push(`GitHub ${color.gray("→ Copilot")}`)
|
||||
|
||||
console.log()
|
||||
console.log(color.bold("🔐 Authenticate Your Providers"))
|
||||
console.log()
|
||||
console.log(` Run ${color.cyan("opencode auth login")} and select:`)
|
||||
for (const provider of providers) {
|
||||
console.log(` ${SYMBOLS.bullet} ${provider}`)
|
||||
}
|
||||
console.log()
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { createEventState, type EventState } from "./events"
|
||||
import { createEventState, serializeError, type EventState } from "./events"
|
||||
import type { RunContext, EventPayload } from "./types"
|
||||
|
||||
const createMockContext = (sessionID: string = "test-session"): RunContext => ({
|
||||
@@ -15,6 +15,63 @@ async function* toAsyncIterable<T>(items: T[]): AsyncIterable<T> {
|
||||
}
|
||||
}
|
||||
|
||||
describe("serializeError", () => {
|
||||
it("returns 'Unknown error' for null/undefined", () => {
|
||||
// #given / #when / #then
|
||||
expect(serializeError(null)).toBe("Unknown error")
|
||||
expect(serializeError(undefined)).toBe("Unknown error")
|
||||
})
|
||||
|
||||
it("returns message from Error instance", () => {
|
||||
// #given
|
||||
const error = new Error("Something went wrong")
|
||||
|
||||
// #when / #then
|
||||
expect(serializeError(error)).toBe("Something went wrong")
|
||||
})
|
||||
|
||||
it("returns string as-is", () => {
|
||||
// #given / #when / #then
|
||||
expect(serializeError("Direct error message")).toBe("Direct error message")
|
||||
})
|
||||
|
||||
it("extracts message from plain object", () => {
|
||||
// #given
|
||||
const errorObj = { message: "Object error message", code: "ERR_001" }
|
||||
|
||||
// #when / #then
|
||||
expect(serializeError(errorObj)).toBe("Object error message")
|
||||
})
|
||||
|
||||
it("extracts message from nested error object", () => {
|
||||
// #given
|
||||
const errorObj = { error: { message: "Nested error message" } }
|
||||
|
||||
// #when / #then
|
||||
expect(serializeError(errorObj)).toBe("Nested error message")
|
||||
})
|
||||
|
||||
it("extracts message from data.message path", () => {
|
||||
// #given
|
||||
const errorObj = { data: { message: "Data error message" } }
|
||||
|
||||
// #when / #then
|
||||
expect(serializeError(errorObj)).toBe("Data error message")
|
||||
})
|
||||
|
||||
it("JSON stringifies object without message property", () => {
|
||||
// #given
|
||||
const errorObj = { code: "ERR_001", status: 500 }
|
||||
|
||||
// #when
|
||||
const result = serializeError(errorObj)
|
||||
|
||||
// #then
|
||||
expect(result).toContain("ERR_001")
|
||||
expect(result).toContain("500")
|
||||
})
|
||||
})
|
||||
|
||||
describe("createEventState", () => {
|
||||
it("creates initial state with correct defaults", () => {
|
||||
// #given / #when
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user