Compare commits
172 Commits
v3.0.0-bet
...
v3.1.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c8527f29b | ||
|
|
cd4da93bf2 | ||
|
|
71b2f1518a | ||
|
|
dcda8769cc | ||
|
|
a94fbadd57 | ||
|
|
23b49c4a5c | ||
|
|
b4973954e3 | ||
|
|
6d50fbe563 | ||
|
|
9850dd0f6e | ||
|
|
34aaef2219 | ||
|
|
faca80caa9 | ||
|
|
0c3fbd724b | ||
|
|
c7455708f8 | ||
|
|
bffa1ad43d | ||
|
|
6560dedd4c | ||
|
|
b7e32a99f2 | ||
|
|
a06e656565 | ||
|
|
30ed086c40 | ||
|
|
7c15b06da7 | ||
|
|
0e7ee2ac30 | ||
|
|
ca93d2f0fe | ||
|
|
3ab4529bc7 | ||
|
|
9d3e152b19 | ||
|
|
68c8f3dda7 | ||
|
|
03f6e72c9b | ||
|
|
4fd9f0fd04 | ||
|
|
4413336724 | ||
|
|
895f366a11 | ||
|
|
acc19fcd41 | ||
|
|
68e0a32183 | ||
|
|
dee89c1556 | ||
|
|
315c75c51e | ||
|
|
3dd80889a5 | ||
|
|
8f6ed5b20f | ||
|
|
01500f1ebe | ||
|
|
48f6c5e06d | ||
|
|
3e32afe646 | ||
|
|
d11c4a1f81 | ||
|
|
5558ddf468 | ||
|
|
aa03d9b811 | ||
|
|
28a0dd06c7 | ||
|
|
995b7751af | ||
|
|
5087788f66 | ||
|
|
19524c8a27 | ||
|
|
fbb4d46945 | ||
|
|
5dc8d577a4 | ||
|
|
c249763d7e | ||
|
|
b2d618e851 | ||
|
|
6f348a8a5c | ||
|
|
1da0adcbe8 | ||
|
|
8a9d966a3d | ||
|
|
76f8c500cb | ||
|
|
388516bcc5 | ||
|
|
8dff875929 | ||
|
|
966cc90a02 | ||
|
|
1d27d78127 | ||
|
|
38156d49f3 | ||
|
|
897eea0263 | ||
|
|
9b59ef66e4 | ||
|
|
0d938059f9 | ||
|
|
9d35f23725 | ||
|
|
aa1646f82c | ||
|
|
e47ab084fd | ||
|
|
baf6358736 | ||
|
|
488c89156b | ||
|
|
c4957a469d | ||
|
|
d481c596bd | ||
|
|
655d511294 | ||
|
|
7dedd6cf90 | ||
|
|
bd18f231f5 | ||
|
|
de439edc22 | ||
|
|
04500bae7d | ||
|
|
1cb6b3de7d | ||
|
|
912a56db85 | ||
|
|
a5d9929c0a | ||
|
|
7f43f160b5 | ||
|
|
af67bc8592 | ||
|
|
c74d79e28a | ||
|
|
fc5298d778 | ||
|
|
3e8e3db961 | ||
|
|
6fa5cac616 | ||
|
|
158ccabf24 | ||
|
|
2efbf2650f | ||
|
|
acded4ba2a | ||
|
|
911e43445f | ||
|
|
3049e1ebfb | ||
|
|
62921b9e44 | ||
|
|
cd23f7ab7d | ||
|
|
518dceac72 | ||
|
|
19f43e30c8 | ||
|
|
b3be9f33c6 | ||
|
|
430098856a | ||
|
|
5932f5f94f | ||
|
|
fcf2e32071 | ||
|
|
19827dac70 | ||
|
|
3ed1c6644e | ||
|
|
cf6e714946 | ||
|
|
383f43548b | ||
|
|
26b1c67964 | ||
|
|
7e065dfe12 | ||
|
|
8429da02b8 | ||
|
|
ab51f5d39f | ||
|
|
3ee519c7b0 | ||
|
|
c9b86b7815 | ||
|
|
9b6d8f629a | ||
|
|
6a2f43858a | ||
|
|
601ea32a1c | ||
|
|
8f31211c75 | ||
|
|
04f2b513c6 | ||
|
|
8ebc933118 | ||
|
|
a67a35aea8 | ||
|
|
9d66b80709 | ||
|
|
5c7eb02d5b | ||
|
|
68aa913499 | ||
|
|
3a79b8761b | ||
|
|
da416b362b | ||
|
|
90054b28ad | ||
|
|
892b245779 | ||
|
|
aead4aebd2 | ||
|
|
bccc943173 | ||
|
|
05904ca617 | ||
|
|
3af30b0a21 | ||
|
|
b55fd8d76f | ||
|
|
208af055ef | ||
|
|
0aa8f486af | ||
|
|
a5db86ee15 | ||
|
|
14f450bd25 | ||
|
|
5a1da39def | ||
|
|
24d065c43a | ||
|
|
fd72ce5ce7 | ||
|
|
043b1a3377 | ||
|
|
512952f66d | ||
|
|
d9723e76ab | ||
|
|
212baa6674 | ||
|
|
1c76e0513a | ||
|
|
c8cc94cd3c | ||
|
|
20cca35157 | ||
|
|
81d27afadb | ||
|
|
6cb2f3031c | ||
|
|
f116ea1d43 | ||
|
|
6aa0674000 | ||
|
|
2b828624a0 | ||
|
|
e60ccb93fb | ||
|
|
aa244e8098 | ||
|
|
6f60f03433 | ||
|
|
b8a0eee92d | ||
|
|
1486ebbc87 | ||
|
|
063c759275 | ||
|
|
6e9ebaf3ee | ||
|
|
0e1d4e52e1 | ||
|
|
c0fb4b79bd | ||
|
|
ec32dd65c2 | ||
|
|
04fb339622 | ||
|
|
3a22c24cf4 | ||
|
|
cf2320480f | ||
|
|
9532680879 | ||
|
|
2a945ddbf5 | ||
|
|
58bb92134d | ||
|
|
f1a279a10a | ||
|
|
faf172a91d | ||
|
|
04633ba208 | ||
|
|
58459e692b | ||
|
|
894a0fa849 | ||
|
|
21c7d29c1d | ||
|
|
ba93c42943 | ||
|
|
5c7dd40751 | ||
|
|
acc7b8b2f7 | ||
|
|
8c90838f3b | ||
|
|
0b784d24f2 | ||
|
|
444fbe396a | ||
|
|
ad86e58077 | ||
|
|
7ed7bf5c66 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -14,6 +14,8 @@ body:
|
||||
label: Prerequisites
|
||||
description: Please confirm the following before submitting
|
||||
options:
|
||||
- label: I will write this issue in English (see our [Language Policy](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/CONTRIBUTING.md#language-policy))
|
||||
required: true
|
||||
- label: I have searched existing issues to avoid duplicates
|
||||
required: true
|
||||
- label: I am using the latest version of oh-my-opencode
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -14,6 +14,8 @@ body:
|
||||
label: Prerequisites
|
||||
description: Please confirm the following before submitting
|
||||
options:
|
||||
- label: I will write this issue in English (see our [Language Policy](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/CONTRIBUTING.md#language-policy))
|
||||
required: true
|
||||
- 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)
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/general.yml
vendored
2
.github/ISSUE_TEMPLATE/general.yml
vendored
@@ -14,6 +14,8 @@ body:
|
||||
label: Prerequisites
|
||||
description: Please confirm the following before submitting
|
||||
options:
|
||||
- label: I will write this issue in English (see our [Language Policy](https://github.com/code-yeongyu/oh-my-opencode/blob/dev/CONTRIBUTING.md#language-policy))
|
||||
required: true
|
||||
- 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)
|
||||
|
||||
51
.github/workflows/ci.yml
vendored
51
.github/workflows/ci.yml
vendored
@@ -4,13 +4,32 @@ on:
|
||||
push:
|
||||
branches: [master, dev]
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
branches: [master, dev]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Block PRs targeting master branch
|
||||
block-master-pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Check PR target branch
|
||||
run: |
|
||||
if [ "${{ github.base_ref }}" = "master" ]; then
|
||||
echo "::error::PRs to master branch are not allowed. Please target the 'dev' branch instead."
|
||||
echo ""
|
||||
echo "PULL REQUESTS TO MASTER ARE BLOCKED"
|
||||
echo ""
|
||||
echo "All PRs must target the 'dev' branch."
|
||||
echo "Please close this PR and create a new one targeting 'dev'."
|
||||
exit 1
|
||||
else
|
||||
echo "PR targets '${{ github.base_ref }}' branch - OK"
|
||||
fi
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -25,8 +44,34 @@ jobs:
|
||||
env:
|
||||
BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi"
|
||||
|
||||
- name: Run tests
|
||||
run: bun test
|
||||
- name: Run mock-heavy tests (isolated)
|
||||
run: |
|
||||
# These files use mock.module() which pollutes module cache
|
||||
# Run them in separate processes to prevent cross-file contamination
|
||||
bun test src/plugin-handlers
|
||||
bun test src/hooks/atlas
|
||||
bun test src/hooks/compaction-context-injector
|
||||
bun test src/features/tmux-subagent
|
||||
|
||||
- name: Run remaining tests
|
||||
run: |
|
||||
# Run all other tests (mock-heavy ones are re-run but that's acceptable)
|
||||
bun test bin script src/cli src/config src/mcp src/index.test.ts \
|
||||
src/agents src/tools src/shared \
|
||||
src/hooks/anthropic-context-window-limit-recovery \
|
||||
src/hooks/claude-code-compatibility \
|
||||
src/hooks/context-injection \
|
||||
src/hooks/provider-toast \
|
||||
src/hooks/session-notification \
|
||||
src/hooks/sisyphus \
|
||||
src/hooks/todo-continuation-enforcer \
|
||||
src/features/background-agent \
|
||||
src/features/builtin-commands \
|
||||
src/features/builtin-skills \
|
||||
src/features/claude-code-session-state \
|
||||
src/features/hook-message-injector \
|
||||
src/features/opencode-skill-loader \
|
||||
src/features/skill-mcp-manager
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
path-to-signatures: 'signatures/cla.json'
|
||||
path-to-document: 'https://github.com/code-yeongyu/oh-my-opencode/blob/master/CLA.md'
|
||||
branch: 'dev'
|
||||
allowlist: bot*,dependabot*,github-actions*,*[bot],sisyphus-dev-ai
|
||||
allowlist: code-yeongyu,bot*,dependabot*,github-actions*,*[bot],sisyphus-dev-ai
|
||||
custom-notsigned-prcomment: |
|
||||
Thank you for your contribution! Before we can merge this PR, we need you to sign our [Contributor License Agreement (CLA)](https://github.com/code-yeongyu/oh-my-opencode/blob/master/CLA.md).
|
||||
|
||||
|
||||
30
.github/workflows/publish.yml
vendored
30
.github/workflows/publish.yml
vendored
@@ -45,8 +45,34 @@ jobs:
|
||||
env:
|
||||
BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/napi"
|
||||
|
||||
- name: Run tests
|
||||
run: bun test
|
||||
- name: Run mock-heavy tests (isolated)
|
||||
run: |
|
||||
# These files use mock.module() which pollutes module cache
|
||||
# Run them in separate processes to prevent cross-file contamination
|
||||
bun test src/plugin-handlers
|
||||
bun test src/hooks/atlas
|
||||
bun test src/hooks/compaction-context-injector
|
||||
bun test src/features/tmux-subagent
|
||||
|
||||
- name: Run remaining tests
|
||||
run: |
|
||||
# Run all other tests (mock-heavy ones are re-run but that's acceptable)
|
||||
bun test bin script src/cli src/config src/mcp src/index.test.ts \
|
||||
src/agents src/tools src/shared \
|
||||
src/hooks/anthropic-context-window-limit-recovery \
|
||||
src/hooks/claude-code-compatibility \
|
||||
src/hooks/context-injection \
|
||||
src/hooks/provider-toast \
|
||||
src/hooks/session-notification \
|
||||
src/hooks/sisyphus \
|
||||
src/hooks/todo-continuation-enforcer \
|
||||
src/features/background-agent \
|
||||
src/features/builtin-commands \
|
||||
src/features/builtin-skills \
|
||||
src/features/claude-code-session-state \
|
||||
src/features/hook-message-injector \
|
||||
src/features/opencode-skill-loader \
|
||||
src/features/skill-mcp-manager
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
38
.github/workflows/sisyphus-agent.yml
vendored
38
.github/workflows/sisyphus-agent.yml
vendored
@@ -152,6 +152,41 @@ jobs:
|
||||
"limit": { "context": 200000, "output": 64000 }
|
||||
}
|
||||
}
|
||||
} |
|
||||
.provider["zai-coding-plan"] = {
|
||||
"name": "Z.AI Coding Plan",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"options": {
|
||||
"baseURL": "https://api.z.ai/api/paas/v4"
|
||||
},
|
||||
"models": {
|
||||
"glm-4.7": {
|
||||
"id": "glm-4.7",
|
||||
"name": "GLM 4.7",
|
||||
"limit": { "context": 128000, "output": 16000 }
|
||||
},
|
||||
"glm-4.6v": {
|
||||
"id": "glm-4.6v",
|
||||
"name": "GLM 4.6 Vision",
|
||||
"limit": { "context": 128000, "output": 16000 }
|
||||
}
|
||||
}
|
||||
} |
|
||||
.provider.openai = {
|
||||
"name": "OpenAI",
|
||||
"npm": "@ai-sdk/openai",
|
||||
"models": {
|
||||
"gpt-5.2": {
|
||||
"id": "gpt-5.2",
|
||||
"name": "GPT-5.2",
|
||||
"limit": { "context": 128000, "output": 16000 }
|
||||
},
|
||||
"gpt-5.2-codex": {
|
||||
"id": "gpt-5.2-codex",
|
||||
"name": "GPT-5.2 Codex",
|
||||
"limit": { "context": 128000, "output": 32000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
' "$OPENCODE_JSON" > /tmp/oc.json && mv /tmp/oc.json "$OPENCODE_JSON"
|
||||
|
||||
@@ -287,6 +322,9 @@ jobs:
|
||||
)
|
||||
jq --arg append "$PROMPT_APPEND" '.agents.Sisyphus.prompt_append = $append' "$OMO_JSON" > /tmp/omo.json && mv /tmp/omo.json "$OMO_JSON"
|
||||
|
||||
# Add categories configuration for unspecified-low to use GLM 4.7
|
||||
jq '.categories["unspecified-low"] = { "model": "zai-coding-plan/glm-4.7" }' "$OMO_JSON" > /tmp/omo.json && mv /tmp/omo.json "$OMO_JSON"
|
||||
|
||||
mkdir -p ~/.local/share/opencode
|
||||
echo "$OPENCODE_AUTH_JSON" > ~/.local/share/opencode/auth.json
|
||||
chmod 600 ~/.local/share/opencode/auth.json
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,3 +33,4 @@ yarn.lock
|
||||
test-injection/
|
||||
notepad.md
|
||||
oauth-success.html
|
||||
.188e87dbff6e7fd9-00000000.bun-build
|
||||
|
||||
342
.opencode/command/remove-deadcode.md
Normal file
342
.opencode/command/remove-deadcode.md
Normal file
@@ -0,0 +1,342 @@
|
||||
---
|
||||
description: Remove unused code from this project with ultrawork mode, LSP-verified safety, atomic commits
|
||||
---
|
||||
|
||||
<command-instruction>
|
||||
You are a dead code removal specialist. Execute the FULL dead code removal workflow using ultrawork mode.
|
||||
|
||||
Your core weapon: **LSP FindReferences**. If a symbol has ZERO external references, it's dead. Remove it.
|
||||
|
||||
## CRITICAL RULES
|
||||
|
||||
1. **LSP is law.** Never guess. Always verify with `LspFindReferences` before removing ANYTHING.
|
||||
2. **One removal = one commit.** Every dead code removal gets its own atomic commit.
|
||||
3. **Test after every removal.** Run `bun test` after each. If it fails, REVERT and skip.
|
||||
4. **Leaf-first order.** Remove deepest unused symbols first, then work up the dependency chain. Removing a leaf may expose new dead code upstream.
|
||||
5. **Never remove entry points.** `src/index.ts`, `src/cli/index.ts`, test files, config files, and files in `packages/` are off-limits unless explicitly targeted.
|
||||
|
||||
---
|
||||
|
||||
## STEP 0: REGISTER TODO LIST (MANDATORY FIRST ACTION)
|
||||
|
||||
```
|
||||
TodoWrite([
|
||||
{"id": "scan", "content": "PHASE 1: Scan codebase for dead code candidates using LSP + explore agents", "status": "pending", "priority": "high"},
|
||||
{"id": "verify", "content": "PHASE 2: Verify each candidate with LspFindReferences - zero false positives", "status": "pending", "priority": "high"},
|
||||
{"id": "plan", "content": "PHASE 3: Plan removal order (leaf-first dependency order)", "status": "pending", "priority": "high"},
|
||||
{"id": "remove", "content": "PHASE 4: Remove dead code one-by-one (remove -> test -> commit loop)", "status": "pending", "priority": "high"},
|
||||
{"id": "final", "content": "PHASE 5: Final verification - full test suite + build + typecheck", "status": "pending", "priority": "high"}
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1: SCAN FOR DEAD CODE CANDIDATES
|
||||
|
||||
**Mark scan as in_progress.**
|
||||
|
||||
### 1.1: Launch Parallel Explore Agents (ALL BACKGROUND)
|
||||
|
||||
Fire ALL simultaneously:
|
||||
|
||||
```
|
||||
// Agent 1: Find all exported symbols
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
prompt="Find ALL exported functions, classes, types, interfaces, and constants across src/.
|
||||
List each with: file path, line number, symbol name, export type (named/default).
|
||||
EXCLUDE: src/index.ts root exports, test files.
|
||||
Return as structured list.")
|
||||
|
||||
// Agent 2: Find potentially unused files
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
prompt="Find files in src/ that are NOT imported by any other file.
|
||||
Check import/require statements across the entire codebase.
|
||||
EXCLUDE: index.ts files, test files, entry points, config files, .md files.
|
||||
Return list of potentially orphaned files.")
|
||||
|
||||
// Agent 3: Find unused imports within files
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
prompt="Find unused imports across src/**/*.ts files.
|
||||
Look for import statements where the imported symbol is never referenced in the file body.
|
||||
Return: file path, line number, imported symbol name.")
|
||||
|
||||
// Agent 4: Find functions/variables only used in their own declaration
|
||||
delegate_task(subagent_type="explore", run_in_background=true,
|
||||
prompt="Find private/non-exported functions, variables, and types in src/**/*.ts that appear
|
||||
to have zero usage beyond their declaration. Return: file path, line number, symbol name.")
|
||||
```
|
||||
|
||||
### 1.2: Direct AST-Grep Scans (WHILE AGENTS RUN)
|
||||
|
||||
```typescript
|
||||
// Find unused imports pattern
|
||||
ast_grep_search(pattern="import { $NAME } from '$PATH'", lang="typescript", paths=["src/"])
|
||||
|
||||
// Find empty export objects
|
||||
ast_grep_search(pattern="export {}", lang="typescript", paths=["src/"])
|
||||
```
|
||||
|
||||
### 1.3: Collect All Results
|
||||
|
||||
Collect background agent results. Compile into a master candidate list:
|
||||
|
||||
```
|
||||
## DEAD CODE CANDIDATES
|
||||
|
||||
| # | File | Line | Symbol | Type | Confidence |
|
||||
|---|------|------|--------|------|------------|
|
||||
| 1 | src/foo.ts | 42 | unusedFunc | function | HIGH |
|
||||
| 2 | src/bar.ts | 10 | OldType | type | MEDIUM |
|
||||
```
|
||||
|
||||
**Mark scan as completed.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2: VERIFY WITH LSP (ZERO FALSE POSITIVES)
|
||||
|
||||
**Mark verify as in_progress.**
|
||||
|
||||
For EVERY candidate from Phase 1, run this verification:
|
||||
|
||||
### 2.1: The LSP Verification Protocol
|
||||
|
||||
For each candidate symbol:
|
||||
|
||||
```typescript
|
||||
// Step 1: Find the symbol's exact position
|
||||
LspDocumentSymbols(filePath) // Get line/character of the symbol
|
||||
|
||||
// Step 2: Find ALL references across the ENTIRE workspace
|
||||
LspFindReferences(filePath, line, character, includeDeclaration=false)
|
||||
// includeDeclaration=false → only counts USAGES, not the definition itself
|
||||
|
||||
// Step 3: Evaluate
|
||||
// 0 references → CONFIRMED DEAD CODE
|
||||
// 1+ references → NOT dead, remove from candidate list
|
||||
```
|
||||
|
||||
### 2.2: False Positive Guards
|
||||
|
||||
**NEVER mark as dead code if:**
|
||||
- Symbol is in `src/index.ts` (package entry point)
|
||||
- Symbol is in any `index.ts` that re-exports (barrel file check: look if it's re-exported)
|
||||
- Symbol is referenced in test files (tests are valid consumers)
|
||||
- Symbol has `@public` or `@api` JSDoc tags
|
||||
- Symbol is in a file listed in `package.json` exports
|
||||
- Symbol is a hook factory (`createXXXHook`) registered in `src/index.ts`
|
||||
- Symbol is a tool factory (`createXXXTool`) registered in tool loading
|
||||
- Symbol is an agent definition registered in `agentSources`
|
||||
- File is a command template, skill definition, or MCP config
|
||||
|
||||
### 2.3: Build Confirmed Dead Code List
|
||||
|
||||
After verification, produce:
|
||||
|
||||
```
|
||||
## CONFIRMED DEAD CODE (LSP-verified, 0 external references)
|
||||
|
||||
| # | File | Line | Symbol | Type | Safe to Remove |
|
||||
|---|------|------|--------|------|----------------|
|
||||
| 1 | src/foo.ts | 42 | unusedFunc | function | YES |
|
||||
```
|
||||
|
||||
**If ZERO confirmed dead code found: Report "No dead code found" and STOP.**
|
||||
|
||||
**Mark verify as completed.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 3: PLAN REMOVAL ORDER
|
||||
|
||||
**Mark plan as in_progress.**
|
||||
|
||||
### 3.1: Dependency Analysis
|
||||
|
||||
For each confirmed dead symbol:
|
||||
1. Check if removing it would expose other dead code
|
||||
2. Check if other dead symbols depend on this one
|
||||
3. Build removal dependency graph
|
||||
|
||||
### 3.2: Order by Leaf-First
|
||||
|
||||
```
|
||||
Removal Order:
|
||||
1. [Leaf symbols - no other dead code depends on them]
|
||||
2. [Intermediate symbols - depended on only by already-removed dead code]
|
||||
3. [Dead files - entire files with no live exports]
|
||||
```
|
||||
|
||||
### 3.3: Register Granular Todos
|
||||
|
||||
Create one todo per removal:
|
||||
|
||||
```
|
||||
TodoWrite([
|
||||
{"id": "remove-1", "content": "Remove unusedFunc from src/foo.ts:42", "status": "pending", "priority": "high"},
|
||||
{"id": "remove-2", "content": "Remove OldType from src/bar.ts:10", "status": "pending", "priority": "high"},
|
||||
// ... one per confirmed dead symbol
|
||||
])
|
||||
```
|
||||
|
||||
**Mark plan as completed.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 4: ITERATIVE REMOVAL LOOP
|
||||
|
||||
**Mark remove as in_progress.**
|
||||
|
||||
For EACH dead code item, execute this exact loop:
|
||||
|
||||
### 4.1: Pre-Removal Check
|
||||
|
||||
```typescript
|
||||
// Re-verify it's still dead (previous removals may have changed things)
|
||||
LspFindReferences(filePath, line, character, includeDeclaration=false)
|
||||
// If references > 0 now → SKIP (previous removal exposed a new consumer)
|
||||
```
|
||||
|
||||
### 4.2: Remove the Dead Code
|
||||
|
||||
Use appropriate tool:
|
||||
|
||||
**For unused imports:**
|
||||
```typescript
|
||||
Edit(filePath, oldString="import { deadSymbol } from '...';\n", newString="")
|
||||
// Or if it's one of many imports, remove just the symbol from the import list
|
||||
```
|
||||
|
||||
**For unused functions/classes/types:**
|
||||
```typescript
|
||||
// Read the full symbol extent first
|
||||
Read(filePath, offset=startLine, limit=endLine-startLine+1)
|
||||
// Then remove it
|
||||
Edit(filePath, oldString="[full symbol text]", newString="")
|
||||
```
|
||||
|
||||
**For dead files:**
|
||||
```bash
|
||||
# Only after confirming ZERO imports point to this file
|
||||
rm "path/to/dead-file.ts"
|
||||
```
|
||||
|
||||
**After removal, also clean up:**
|
||||
- Remove any imports that were ONLY used by the removed code
|
||||
- Remove any now-empty import statements
|
||||
- Fix any trailing whitespace / double blank lines left behind
|
||||
|
||||
### 4.3: Post-Removal Verification
|
||||
|
||||
```typescript
|
||||
// 1. LSP diagnostics on changed file
|
||||
LspDiagnostics(filePath, severity="error")
|
||||
// Must be clean (or only pre-existing errors)
|
||||
|
||||
// 2. Run tests
|
||||
bash("bun test")
|
||||
// Must pass
|
||||
|
||||
// 3. Typecheck
|
||||
bash("bun run typecheck")
|
||||
// Must pass
|
||||
```
|
||||
|
||||
### 4.4: Handle Failures
|
||||
|
||||
If ANY verification fails:
|
||||
1. **REVERT** the change immediately (`git checkout -- [file]`)
|
||||
2. Mark this removal todo as `cancelled` with note: "Removal caused [error]. Skipped."
|
||||
3. Proceed to next item
|
||||
|
||||
### 4.5: Commit
|
||||
|
||||
```bash
|
||||
git add [changed-files]
|
||||
git commit -m "refactor: remove unused [symbolType] [symbolName] from [filePath]"
|
||||
```
|
||||
|
||||
Mark this removal todo as `completed`.
|
||||
|
||||
### 4.6: Re-scan After Removal
|
||||
|
||||
After removing a symbol, check if its removal exposed NEW dead code:
|
||||
- Were there imports that only existed to serve the removed symbol?
|
||||
- Are there other symbols in the same file now unreferenced?
|
||||
|
||||
If new dead code is found, add it to the removal queue.
|
||||
|
||||
**Repeat 4.1-4.6 for every item. Mark remove as completed when done.**
|
||||
|
||||
---
|
||||
|
||||
## PHASE 5: FINAL VERIFICATION
|
||||
|
||||
**Mark final as in_progress.**
|
||||
|
||||
### 5.1: Full Test Suite
|
||||
```bash
|
||||
bun test
|
||||
```
|
||||
|
||||
### 5.2: Full Typecheck
|
||||
```bash
|
||||
bun run typecheck
|
||||
```
|
||||
|
||||
### 5.3: Full Build
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
### 5.4: Summary Report
|
||||
|
||||
```markdown
|
||||
## Dead Code Removal Complete
|
||||
|
||||
### Removed
|
||||
| # | Symbol | File | Type | Commit |
|
||||
|---|--------|------|------|--------|
|
||||
| 1 | unusedFunc | src/foo.ts | function | abc1234 |
|
||||
|
||||
### Skipped (caused failures)
|
||||
| # | Symbol | File | Reason |
|
||||
|---|--------|------|--------|
|
||||
| 1 | riskyFunc | src/bar.ts | Test failure: [details] |
|
||||
|
||||
### Verification
|
||||
- Tests: PASSED (X/Y passing)
|
||||
- Typecheck: CLEAN
|
||||
- Build: SUCCESS
|
||||
- Total dead code removed: N symbols across M files
|
||||
- Total commits: K atomic commits
|
||||
```
|
||||
|
||||
**Mark final as completed.**
|
||||
|
||||
---
|
||||
|
||||
## SCOPE CONTROL
|
||||
|
||||
**If $ARGUMENTS is provided**, narrow the scan to the specified scope:
|
||||
- File path: Only scan that file
|
||||
- Directory: Only scan that directory
|
||||
- Symbol name: Only check that specific symbol
|
||||
- "all" or empty: Full project scan (default)
|
||||
|
||||
## ABORT CONDITIONS
|
||||
|
||||
**STOP and report to user if:**
|
||||
- 3 consecutive removals cause test failures
|
||||
- Build breaks and cannot be fixed by reverting
|
||||
- More than 50 candidates found (ask user to narrow scope)
|
||||
|
||||
## LANGUAGE
|
||||
|
||||
Use English for commit messages and technical output.
|
||||
|
||||
</command-instruction>
|
||||
|
||||
<user-request>
|
||||
$ARGUMENTS
|
||||
</user-request>
|
||||
55
AGENTS.md
55
AGENTS.md
@@ -1,12 +1,24 @@
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
**Generated:** 2026-01-23T15:59:00+09:00
|
||||
**Commit:** 599fad0e
|
||||
**Generated:** 2026-01-26T14:50:00+09:00
|
||||
**Commit:** 9d66b807
|
||||
**Branch:** dev
|
||||
|
||||
---
|
||||
|
||||
## **IMPORTANT: PULL REQUEST TARGET BRANCH**
|
||||
|
||||
> **ALL PULL REQUESTS MUST TARGET THE `dev` BRANCH.**
|
||||
>
|
||||
> **DO NOT CREATE PULL REQUESTS TARGETING `master` BRANCH.**
|
||||
>
|
||||
> PRs to `master` will be automatically rejected by CI.
|
||||
|
||||
---
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
OpenCode plugin: multi-model agent orchestration (Claude Opus 4.5, GPT-5.2, Gemini 3 Flash, Grok Code, GLM-4.7). 31 lifecycle hooks, 20+ tools (LSP, AST-Grep, delegation), 10 specialized agents, full Claude Code compatibility. "oh-my-zsh" for OpenCode.
|
||||
OpenCode plugin: multi-model agent orchestration (Claude Opus 4.5, GPT-5.2, Gemini 3 Flash, Grok Code). 32 lifecycle hooks, 20+ tools (LSP, AST-Grep, delegation), 10 specialized agents, full Claude Code compatibility. "oh-my-zsh" for OpenCode.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
@@ -14,14 +26,14 @@ OpenCode plugin: multi-model agent orchestration (Claude Opus 4.5, GPT-5.2, Gemi
|
||||
oh-my-opencode/
|
||||
├── src/
|
||||
│ ├── agents/ # 10 AI agents - see src/agents/AGENTS.md
|
||||
│ ├── hooks/ # 31 lifecycle hooks - see src/hooks/AGENTS.md
|
||||
│ ├── hooks/ # 32 lifecycle hooks - see src/hooks/AGENTS.md
|
||||
│ ├── tools/ # 20+ tools - see src/tools/AGENTS.md
|
||||
│ ├── features/ # Background agents, Claude Code compat - see src/features/AGENTS.md
|
||||
│ ├── shared/ # 50 cross-cutting utilities - see src/shared/AGENTS.md
|
||||
│ ├── shared/ # 55 cross-cutting utilities - see src/shared/AGENTS.md
|
||||
│ ├── cli/ # CLI installer, doctor - see src/cli/AGENTS.md
|
||||
│ ├── mcp/ # Built-in MCPs - see src/mcp/AGENTS.md
|
||||
│ ├── config/ # Zod schema, TypeScript types
|
||||
│ └── index.ts # Main plugin entry (593 lines)
|
||||
│ └── index.ts # Main plugin entry (672 lines)
|
||||
├── script/ # build-schema.ts, build-binaries.ts
|
||||
├── packages/ # 7 platform-specific binaries
|
||||
└── dist/ # Build output (ESM + .d.ts)
|
||||
@@ -36,9 +48,10 @@ oh-my-opencode/
|
||||
| Add tool | `src/tools/` | Dir with index/types/constants/tools.ts |
|
||||
| Add MCP | `src/mcp/` | Create config, add to index.ts |
|
||||
| Add skill | `src/features/builtin-skills/` | Create dir with SKILL.md |
|
||||
| Add command | `src/features/builtin-commands/` | Add template + register in commands.ts |
|
||||
| Config schema | `src/config/schema.ts` | Zod schema, run `bun run build:schema` |
|
||||
| Background agents | `src/features/background-agent/` | manager.ts (1335 lines) |
|
||||
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (773 lines) |
|
||||
| Background agents | `src/features/background-agent/` | manager.ts (1377 lines) |
|
||||
| Orchestrator | `src/hooks/atlas/` | Main orchestration hook (752 lines) |
|
||||
|
||||
## TDD (Test-Driven Development)
|
||||
|
||||
@@ -50,8 +63,8 @@ oh-my-opencode/
|
||||
**Rules:**
|
||||
- NEVER write implementation before test
|
||||
- NEVER delete failing tests - fix the code
|
||||
- Test file: `*.test.ts` alongside source
|
||||
- BDD comments: `#given`, `#when`, `#then`
|
||||
- Test file: `*.test.ts` alongside source (100 test files)
|
||||
- BDD comments: `//#given`, `//#when`, `//#then`
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
@@ -60,7 +73,7 @@ oh-my-opencode/
|
||||
- **Build**: `bun build` (ESM) + `tsc --emitDeclarationOnly`
|
||||
- **Exports**: Barrel pattern via index.ts
|
||||
- **Naming**: kebab-case dirs, `createXXXHook`/`createXXXTool` factories
|
||||
- **Testing**: BDD comments, 90 test files
|
||||
- **Testing**: BDD comments, 100 test files
|
||||
- **Temperature**: 0.1 for code agents, max 0.3
|
||||
|
||||
## ANTI-PATTERNS
|
||||
@@ -88,9 +101,9 @@ oh-my-opencode/
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | Primary orchestrator |
|
||||
| Atlas | anthropic/claude-opus-4-5 | Master orchestrator |
|
||||
| oracle | openai/gpt-5.2 | Consultation, debugging |
|
||||
| librarian | opencode/glm-4.7-free | Docs, GitHub search |
|
||||
| explore | opencode/grok-code | Fast codebase grep |
|
||||
| multimodal-looker | google/gemini-3-flash-preview | PDF/image analysis |
|
||||
| librarian | opencode/big-pickle | Docs, GitHub search |
|
||||
| explore | opencode/gpt-5-nano | Fast codebase grep |
|
||||
| multimodal-looker | google/gemini-3-flash | PDF/image analysis |
|
||||
| Prometheus | anthropic/claude-opus-4-5 | Strategic planning |
|
||||
|
||||
## COMMANDS
|
||||
@@ -99,7 +112,7 @@ oh-my-opencode/
|
||||
bun run typecheck # Type check
|
||||
bun run build # ESM + declarations + schema
|
||||
bun run rebuild # Clean + Build
|
||||
bun test # 90 test files
|
||||
bun test # 100 test files
|
||||
```
|
||||
|
||||
## DEPLOYMENT
|
||||
@@ -113,12 +126,14 @@ bun test # 90 test files
|
||||
|
||||
| File | Lines | Description |
|
||||
|------|-------|-------------|
|
||||
| `src/features/background-agent/manager.ts` | 1335 | Task lifecycle, concurrency |
|
||||
| `src/features/builtin-skills/skills.ts` | 1203 | Skill definitions |
|
||||
| `src/features/builtin-skills/skills.ts` | 1729 | Skill definitions |
|
||||
| `src/features/background-agent/manager.ts` | 1377 | Task lifecycle, concurrency |
|
||||
| `src/agents/prometheus-prompt.ts` | 1196 | Planning agent |
|
||||
| `src/tools/delegate-task/tools.ts` | 1039 | Category-based delegation |
|
||||
| `src/hooks/atlas/index.ts` | 773 | Orchestrator hook |
|
||||
| `src/cli/config-manager.ts` | 641 | JSONC config parsing |
|
||||
| `src/tools/delegate-task/tools.ts` | 1070 | Category-based delegation |
|
||||
| `src/hooks/atlas/index.ts` | 752 | Orchestrator hook |
|
||||
| `src/cli/config-manager.ts` | 664 | JSONC config parsing |
|
||||
| `src/index.ts` | 672 | Main plugin entry |
|
||||
| `src/features/builtin-commands/templates/refactor.ts` | 619 | Refactor command template |
|
||||
|
||||
## MCP ARCHITECTURE
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
|
||||
> > **オーケストレーターがベータ版で利用可能になりました。`oh-my-opencode@3.0.0-beta.10`を使用してインストールしてください。**
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
|
||||
> > **Oh My OpenCode 3.0が正式リリースされました!`oh-my-opencode@latest`を使用してインストールしてください。**
|
||||
>
|
||||
> 一緒に歩みましょう!
|
||||
>
|
||||
@@ -73,7 +73,9 @@
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/issues)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/blob/master/LICENSE.md)
|
||||
|
||||
[English](README.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
[English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
|
||||
[](https://deepwiki.com/code-yeongyu/oh-my-opencode)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
>
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
|
||||
> > **오케스트레이터가 베타 버전으로 사용 가능합니다. 설치하려면 `oh-my-opencode@3.0.0-beta.10`을 사용하세요.**
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
|
||||
> > **Oh My OpenCode 3.0이 정식 출시되었습니다! `oh-my-opencode@latest`를 사용하여 설치하세요.**
|
||||
>
|
||||
> 함께해요!
|
||||
>
|
||||
@@ -73,10 +73,11 @@
|
||||
[](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)
|
||||
|
||||
[](https://deepwiki.com/code-yeongyu/oh-my-opencode)
|
||||
|
||||
</div>
|
||||
|
||||
<!-- </CENTERED SECTION FOR GITHUB DISPLAY> -->
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
|
||||
> > **The Orchestrator is now available in beta. Use `oh-my-opencode@3.0.0-beta.10` to install it.**
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
|
||||
> > **Oh My OpenCode 3.0 is now stable! Use `oh-my-opencode@latest` to install it.**
|
||||
>
|
||||
> Be with us!
|
||||
>
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0-beta.10)
|
||||
> > **Orchestrator 现已进入测试阶段。使用 `oh-my-opencode@3.0.0-beta.10` 安装。**
|
||||
> [](https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v3.0.0)
|
||||
> > **Oh My OpenCode 3.0 正式发布!使用 `oh-my-opencode@latest` 安装。**
|
||||
>
|
||||
> 加入我们!
|
||||
>
|
||||
@@ -74,7 +74,9 @@
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/issues)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/blob/master/LICENSE.md)
|
||||
|
||||
[English](README.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
[English](README.md) | [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中文](README.zh-cn.md)
|
||||
|
||||
[](https://deepwiki.com/code-yeongyu/oh-my-opencode)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"playwright",
|
||||
"agent-browser",
|
||||
"frontend-ui-ux",
|
||||
"git-master"
|
||||
]
|
||||
@@ -70,12 +71,14 @@
|
||||
"interactive-bash-session",
|
||||
"thinking-block-validator",
|
||||
"ralph-loop",
|
||||
"category-skill-reminder",
|
||||
"compaction-context-injector",
|
||||
"claude-code-hooks",
|
||||
"auto-slash-command",
|
||||
"edit-error-recovery",
|
||||
"delegate-task-retry",
|
||||
"prometheus-md-only",
|
||||
"sisyphus-junior-notepad",
|
||||
"start-work",
|
||||
"atlas"
|
||||
]
|
||||
@@ -217,6 +220,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -343,6 +391,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -469,6 +562,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -595,6 +733,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -721,6 +904,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -847,6 +1075,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -973,6 +1246,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1099,6 +1417,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1225,6 +1588,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1351,6 +1759,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1477,6 +1930,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1603,6 +2101,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1729,6 +2272,51 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"providerOptions": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"type": "string"
|
||||
},
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1787,7 +2375,8 @@
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
"high",
|
||||
"xhigh"
|
||||
]
|
||||
},
|
||||
"textVerbosity": {
|
||||
@@ -2170,6 +2759,100 @@
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"browser_automation_engine": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"default": "playwright",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"playwright",
|
||||
"agent-browser",
|
||||
"dev-browser"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tmux": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"layout": {
|
||||
"default": "main-vertical",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"main-horizontal",
|
||||
"main-vertical",
|
||||
"tiled",
|
||||
"even-horizontal",
|
||||
"even-vertical"
|
||||
]
|
||||
},
|
||||
"main_pane_size": {
|
||||
"default": 60,
|
||||
"type": "number",
|
||||
"minimum": 20,
|
||||
"maximum": 80
|
||||
},
|
||||
"main_pane_min_width": {
|
||||
"default": 120,
|
||||
"type": "number",
|
||||
"minimum": 40
|
||||
},
|
||||
"agent_pane_min_width": {
|
||||
"default": 40,
|
||||
"type": "number",
|
||||
"minimum": 20
|
||||
}
|
||||
}
|
||||
},
|
||||
"sisyphus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tasks": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"storage_path": {
|
||||
"default": ".sisyphus/tasks",
|
||||
"type": "string"
|
||||
},
|
||||
"claude_code_compat": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"swarm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"storage_path": {
|
||||
"default": ".sisyphus/teams",
|
||||
"type": "string"
|
||||
},
|
||||
"ui_mode": {
|
||||
"default": "toast",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"toast",
|
||||
"tmux",
|
||||
"both"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
bun.lock
31
bun.lock
@@ -18,6 +18,7 @@
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.2",
|
||||
"vscode-jsonrpc": "^8.2.0",
|
||||
"zod": "^4.1.8",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -27,13 +28,13 @@
|
||||
"typescript": "^5.7.3",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-darwin-x64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-linux-arm64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.0.0-beta.11",
|
||||
"oh-my-opencode-linux-x64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-linux-x64-musl": "3.0.0-beta.11",
|
||||
"oh-my-opencode-windows-x64": "3.0.0-beta.11",
|
||||
"oh-my-opencode-darwin-arm64": "3.1.6",
|
||||
"oh-my-opencode-darwin-x64": "3.1.6",
|
||||
"oh-my-opencode-linux-arm64": "3.1.6",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.1.6",
|
||||
"oh-my-opencode-linux-x64": "3.1.6",
|
||||
"oh-my-opencode-linux-x64-musl": "3.1.6",
|
||||
"oh-my-opencode-windows-x64": "3.1.6",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -225,19 +226,19 @@
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.0.0-beta.11", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-7cFv2bbz9HTY7sshgVTu+IhvYf7CT0czDYqHEB+dYfEqFU6TaoSMimq6uHqcWegUUR1T7PNmc0dyjYVw69FeVA=="],
|
||||
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.1.6", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-KK+ptnkBigvDYbRtF/B5izEC4IoXDS8mAnRHWFBSCINhzQR2No6AtEcwijd6vKBPR+/r71ofq/8mTsIeb1PEVQ=="],
|
||||
|
||||
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.0.0-beta.11", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-rGAbDdUySWITIdm2yiuNFB9lFYaSXT8LMtg97LTlOO5vZbI3M+obIS3QlIkBtAhgOTIPB7Ni+T0W44OmJpHoYA=="],
|
||||
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.1.6", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-UkPI/RUi7INarFasBUZ4Rous6RUQXsU2nr0V8KFJp+70END43D/96dDUwX+zmPtpDhD+DfWkejuwzqfkZJ2ZDQ=="],
|
||||
|
||||
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.0.0-beta.11", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-F9dqwWwGAdqeSkE7Tre5DmHQXwDpU2Z8Jk0lwTJMLj+kMqYFDVPjLPo4iVUdwPpxpmm0pR84u/oonG/2+84/zw=="],
|
||||
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.1.6", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-gvmvgh7WtTtcHiCbG7z43DOYfY/jrf2S6TX/jBMX2/e1AGkcLKwz30NjGhZxeK5SyzxRVypgfZZK1IuriRgbdA=="],
|
||||
|
||||
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.0.0-beta.11", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-H+zOtHkHd+TmdPj64M1A0zLOk7OHIK4C8yqfLFhfizOIBffT1yOhAs6EpK3EqPhfPLu54ADgcQcu8W96VP24UA=="],
|
||||
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.1.6", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-j3R76pmQ4HGVGFJUMMCeF/1lO3Jg7xFdpcBUKCeFh42N1jMgn1aeyxkAaJYB9RwCF/p6+P8B6gVDLCEDu2mxjA=="],
|
||||
|
||||
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.0.0-beta.11", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-IG+KODTJ8rs6cEJ2wN6Zpr6YtvCS5OpYP6jBdGJltmUpjQdMhdMsaY3ysZk+9Vxpx2KC3xj5KLHV1USg3uBTeg=="],
|
||||
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.1.6", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-VDdo0tHCOr5nm7ajd652u798nPNOLRSTcPOnVh6vIPddkZ+ujRke+enOKOw9Pd5e+4AkthqHBwFXNm2VFgnEKg=="],
|
||||
|
||||
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.0.0-beta.11", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-irV+AuWrHqNm7VT7HO56qgymR0+vEfJbtB3vCq68kprH2V4NQmGp2MNKIYPnUCYL7NEK3H2NX+h06YFZJ/8ELQ=="],
|
||||
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.1.6", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-hBG/dhsr8PZelUlYsPBruSLnelB9ocB7H92I+S9svTpDVo67rAmXOoR04twKQ9TeCO4ShOa6hhMhbQnuI8fgNw=="],
|
||||
|
||||
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.0.0-beta.11", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-exZ/NEwGBlxyWszN7dvOfzbYX0cuhBZXftqAAFOlVP26elDHdo+AmSmLR/4cJyzpR9nCWz4xvl/RYF84bY6OEA=="],
|
||||
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.1.6", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-c8Awp03p2DsbS0G589nzveRCeJPgJRJ0vQrha4ChRmmo31Qc5OSmJ5xuMaF8L4nM+/trbTgAQMFMtCMLgtC8IQ=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
@@ -303,6 +304,8 @@
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
@@ -21,13 +21,13 @@ A Category is an agent configuration preset optimized for specific domains.
|
||||
|
||||
| Category | Default Model | Use Cases |
|
||||
|----------|---------------|-----------|
|
||||
| `visual-engineering` | `google/gemini-3-pro-preview` | Frontend, UI/UX, design, styling, animation |
|
||||
| `visual-engineering` | `google/gemini-3-pro` | Frontend, UI/UX, design, styling, animation |
|
||||
| `ultrabrain` | `openai/gpt-5.2-codex` (xhigh) | Deep logical reasoning, complex architecture decisions requiring extensive analysis |
|
||||
| `artistry` | `google/gemini-3-pro-preview` (max) | Highly creative/artistic tasks, novel ideas |
|
||||
| `artistry` | `google/gemini-3-pro` (max) | Highly creative/artistic tasks, novel ideas |
|
||||
| `quick` | `anthropic/claude-haiku-4-5` | Trivial tasks - single file changes, typo fixes, simple modifications |
|
||||
| `unspecified-low` | `anthropic/claude-sonnet-4-5` | Tasks that don't fit other categories, low effort required |
|
||||
| `unspecified-high` | `anthropic/claude-opus-4-5` (max) | Tasks that don't fit other categories, high effort required |
|
||||
| `writing` | `google/gemini-3-flash-preview` | Documentation, prose, technical writing |
|
||||
| `writing` | `google/gemini-3-flash` | Documentation, prose, technical writing |
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -70,12 +70,12 @@ A Skill is a mechanism that injects **specialized knowledge (Context)** and **to
|
||||
|
||||
### Usage
|
||||
|
||||
Add desired skill names to the `skills` array.
|
||||
Add desired skill names to the `load_skills` array.
|
||||
|
||||
```typescript
|
||||
delegate_task(
|
||||
category="quick",
|
||||
skills=["git-master"],
|
||||
load_skills=["git-master"],
|
||||
prompt="Commit current changes. Follow commit message style."
|
||||
)
|
||||
```
|
||||
@@ -110,17 +110,17 @@ You can create powerful specialized agents by combining Categories and Skills.
|
||||
|
||||
### 🎨 The Designer (UI Implementation)
|
||||
- **Category**: `visual-engineering`
|
||||
- **Skills**: `["frontend-ui-ux", "playwright"]`
|
||||
- **load_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)
|
||||
- **load_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"]`
|
||||
- **load_skills**: `["git-master"]`
|
||||
- **Effect**: Uses cost-effective models to quickly fix code and generate clean commits.
|
||||
|
||||
---
|
||||
@@ -131,7 +131,7 @@ When delegating, **clear and specific** prompts are essential. Include these 7 e
|
||||
|
||||
1. **TASK**: What needs to be done? (single objective)
|
||||
2. **EXPECTED OUTCOME**: What is the deliverable?
|
||||
3. **REQUIRED SKILLS**: Which skills should be used?
|
||||
3. **REQUIRED SKILLS**: Which skills should be loaded via `load_skills`?
|
||||
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
|
||||
@@ -177,7 +177,7 @@ You can fine-tune categories in `oh-my-opencode.json`.
|
||||
"categories": {
|
||||
// 1. Define new custom category
|
||||
"korean-writer": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
"temperature": 0.5,
|
||||
"prompt_append": "You are a Korean technical writer. Maintain a friendly and clear tone."
|
||||
},
|
||||
|
||||
@@ -134,7 +134,41 @@ bunx oh-my-opencode run [prompt]
|
||||
|
||||
---
|
||||
|
||||
## 6. `auth` - Authentication Management
|
||||
## 6. `mcp oauth` - MCP OAuth Management
|
||||
|
||||
Manages OAuth 2.1 authentication for remote MCP servers.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Login to an OAuth-protected MCP server
|
||||
bunx oh-my-opencode mcp oauth login <server-name> --server-url https://api.example.com
|
||||
|
||||
# Login with explicit client ID and scopes
|
||||
bunx oh-my-opencode mcp oauth login my-api --server-url https://api.example.com --client-id my-client --scopes "read,write"
|
||||
|
||||
# Remove stored OAuth tokens
|
||||
bunx oh-my-opencode mcp oauth logout <server-name>
|
||||
|
||||
# Check OAuth token status
|
||||
bunx oh-my-opencode mcp oauth status [server-name]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--server-url <url>` | MCP server URL (required for login) |
|
||||
| `--client-id <id>` | OAuth client ID (optional if server supports Dynamic Client Registration) |
|
||||
| `--scopes <scopes>` | Comma-separated OAuth scopes |
|
||||
|
||||
### Token Storage
|
||||
|
||||
Tokens are stored in `~/.config/opencode/mcp-oauth.json` with `0600` permissions (owner read/write only). Key format: `{serverHost}/{resource}`.
|
||||
|
||||
---
|
||||
|
||||
## 7. `auth` - Authentication Management
|
||||
|
||||
Manages Google Antigravity OAuth authentication. Required for using Gemini models.
|
||||
|
||||
@@ -153,7 +187,7 @@ bunx oh-my-opencode auth status
|
||||
|
||||
---
|
||||
|
||||
## 7. Configuration Files
|
||||
## 8. Configuration Files
|
||||
|
||||
The CLI searches for configuration files in the following locations (in priority order):
|
||||
|
||||
@@ -175,7 +209,7 @@ Configuration files support **JSONC (JSON with Comments)** format. You can use c
|
||||
/* Category customization */
|
||||
"categories": {
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -183,7 +217,7 @@ Configuration files support **JSONC (JSON with Comments)** format. You can use c
|
||||
|
||||
---
|
||||
|
||||
## 8. Troubleshooting
|
||||
## 9. Troubleshooting
|
||||
|
||||
### "OpenCode version too old" Error
|
||||
|
||||
@@ -213,7 +247,7 @@ bunx oh-my-opencode doctor --category authentication
|
||||
|
||||
---
|
||||
|
||||
## 9. Non-Interactive Mode
|
||||
## 10. Non-Interactive Mode
|
||||
|
||||
Use the `--no-tui` option for CI/CD environments.
|
||||
|
||||
@@ -227,7 +261,7 @@ bunx oh-my-opencode doctor --json > doctor-report.json
|
||||
|
||||
---
|
||||
|
||||
## 10. Developer Information
|
||||
## 11. Developer Information
|
||||
|
||||
### CLI Structure
|
||||
|
||||
|
||||
@@ -22,13 +22,13 @@ It asks about your providers (Claude, OpenAI, Gemini, etc.) and generates optima
|
||||
"agents": {
|
||||
"oracle": { "model": "openai/gpt-5.2" }, // Use GPT for debugging
|
||||
"librarian": { "model": "zai-coding-plan/glm-4.7" }, // Cheap model for research
|
||||
"explore": { "model": "opencode/grok-code" } // Free model for grep
|
||||
"explore": { "model": "opencode/gpt-5-nano" } // Free model for grep
|
||||
},
|
||||
|
||||
// Override category models (used by delegate_task)
|
||||
"categories": {
|
||||
"quick": { "model": "opencode/grok-code" }, // Fast/cheap for trivial tasks
|
||||
"visual-engineering": { "model": "google/gemini-3-pro-preview" } // Gemini for UI
|
||||
"quick": { "model": "opencode/gpt-5-nano" }, // Fast/cheap for trivial tasks
|
||||
"visual-engineering": { "model": "google/gemini-3-pro" } // Gemini for UI
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -75,7 +75,7 @@ When both `oh-my-opencode.jsonc` and `oh-my-opencode.json` files exist, `.jsonc`
|
||||
"model": "openai/gpt-5.2" // GPT for strategic reasoning
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code" // Free & fast for exploration
|
||||
"model": "opencode/gpt-5-nano" // Free & fast for exploration
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -83,7 +83,67 @@ When both `oh-my-opencode.jsonc` and `oh-my-opencode.json` files exist, `.jsonc`
|
||||
|
||||
## Google Auth
|
||||
|
||||
**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](../README.md#google-gemini-antigravity-oauth).
|
||||
**Recommended**: For Google Gemini authentication, install the [`opencode-antigravity-auth`](https://github.com/NoeFabris/opencode-antigravity-auth) plugin (`@latest`). It provides multi-account load balancing, variant-based thinking levels, dual quota system (Antigravity + Gemini CLI), and active maintenance. See [Installation > Google Gemini](docs/guide/installation.md#google-gemini-antigravity-oauth).
|
||||
|
||||
## Ollama Provider
|
||||
|
||||
**IMPORTANT**: When using Ollama as a provider, you **must** disable streaming to avoid JSON parsing errors.
|
||||
|
||||
### Required Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"explore": {
|
||||
"model": "ollama/qwen3-coder",
|
||||
"stream": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Why `stream: false` is Required
|
||||
|
||||
Ollama returns NDJSON (newline-delimited JSON) when streaming is enabled, but Claude Code SDK expects a single JSON object. This causes `JSON Parse error: Unexpected EOF` when agents attempt tool calls.
|
||||
|
||||
**Example of the problem**:
|
||||
```json
|
||||
// Ollama streaming response (NDJSON - multiple lines)
|
||||
{"message":{"tool_calls":[...]}, "done":false}
|
||||
{"message":{"content":""}, "done":true}
|
||||
|
||||
// Claude Code SDK expects (single JSON object)
|
||||
{"message":{"tool_calls":[...], "content":""}, "done":true}
|
||||
```
|
||||
|
||||
### Supported Models
|
||||
|
||||
Common Ollama models that work with oh-my-opencode:
|
||||
|
||||
| Model | Best For | Configuration |
|
||||
|-------|----------|---------------|
|
||||
| `ollama/qwen3-coder` | Code generation, build fixes | `{"model": "ollama/qwen3-coder", "stream": false}` |
|
||||
| `ollama/ministral-3:14b` | Exploration, codebase search | `{"model": "ollama/ministral-3:14b", "stream": false}` |
|
||||
| `ollama/lfm2.5-thinking` | Documentation, writing | `{"model": "ollama/lfm2.5-thinking", "stream": false}` |
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If you encounter `JSON Parse error: Unexpected EOF`:
|
||||
|
||||
1. **Verify `stream: false` is set** in your agent configuration
|
||||
2. **Check Ollama is running**: `curl http://localhost:11434/api/tags`
|
||||
3. **Test with curl**:
|
||||
```bash
|
||||
curl -s http://localhost:11434/api/chat \
|
||||
-d '{"model": "qwen3-coder", "messages": [{"role": "user", "content": "Hello"}], "stream": false}'
|
||||
```
|
||||
4. **See detailed troubleshooting**: [docs/troubleshooting/ollama-streaming-issue.md](troubleshooting/ollama-streaming-issue.md)
|
||||
|
||||
### Future SDK Fix
|
||||
|
||||
The proper long-term fix requires Claude Code SDK to parse NDJSON responses correctly. Until then, use `stream: false` as a workaround.
|
||||
|
||||
**Tracking**: https://github.com/code-yeongyu/oh-my-opencode/issues/1124
|
||||
|
||||
## Agents
|
||||
|
||||
@@ -103,7 +163,39 @@ Override built-in agent settings:
|
||||
}
|
||||
```
|
||||
|
||||
Each agent supports: `model`, `temperature`, `top_p`, `prompt`, `prompt_append`, `tools`, `disable`, `description`, `mode`, `color`, `permission`.
|
||||
Each agent supports: `model`, `temperature`, `top_p`, `prompt`, `prompt_append`, `tools`, `disable`, `description`, `mode`, `color`, `permission`, `category`, `variant`, `maxTokens`, `thinking`, `reasoningEffort`, `textVerbosity`, `providerOptions`.
|
||||
|
||||
### Additional Agent Options
|
||||
|
||||
| Option | Type | Description |
|
||||
| ------------------- | ------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `category` | string | Category name to inherit model and other settings from category defaults |
|
||||
| `variant` | string | Model variant (e.g., `max`, `high`, `medium`, `low`, `xhigh`) |
|
||||
| `maxTokens` | number | Maximum tokens for response. Passed directly to OpenCode SDK. |
|
||||
| `thinking` | object | Extended thinking configuration for Anthropic models. See [Thinking Options](#thinking-options) below. |
|
||||
| `reasoningEffort` | string | OpenAI reasoning effort level. Values: `low`, `medium`, `high`, `xhigh`. |
|
||||
| `textVerbosity` | string | Text verbosity level. Values: `low`, `medium`, `high`. |
|
||||
| `providerOptions` | object | Provider-specific options passed directly to OpenCode SDK. |
|
||||
|
||||
#### Thinking Options (Anthropic)
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"oracle": {
|
||||
"thinking": {
|
||||
"type": "enabled",
|
||||
"budgetTokens": 200000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------- | ------- | ------- | -------------------------------------------- |
|
||||
| `type` | string | - | `enabled` or `disabled` |
|
||||
| `budgetTokens`| number | - | Maximum budget tokens for extended thinking |
|
||||
|
||||
Use `prompt_append` to add extra instructions without replacing the default system prompt:
|
||||
|
||||
@@ -153,14 +245,14 @@ Or disable via `disabled_agents` in `~/.config/opencode/oh-my-opencode.json` or
|
||||
}
|
||||
```
|
||||
|
||||
Available agents: `oracle`, `librarian`, `explore`, `multimodal-looker`
|
||||
Available agents: `sisyphus`, `prometheus`, `oracle`, `librarian`, `explore`, `multimodal-looker`, `metis`, `momus`, `atlas`
|
||||
|
||||
## Built-in Skills
|
||||
|
||||
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.
|
||||
- **playwright** (default) / **agent-browser**: Browser automation for web scraping, testing, screenshots, and browser interactions. See [Browser Automation](#browser-automation) for switching between providers.
|
||||
- **git-master**: Git expert for atomic commits, rebase/squash, and history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with `delegate_task(category='quick', load_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`:
|
||||
|
||||
@@ -170,7 +262,330 @@ Disable built-in skills via `disabled_skills` in `~/.config/opencode/oh-my-openc
|
||||
}
|
||||
```
|
||||
|
||||
Available built-in skills: `playwright`, `git-master`
|
||||
Available built-in skills: `playwright`, `agent-browser`, `git-master`
|
||||
|
||||
## Skills Configuration
|
||||
|
||||
Configure advanced skills settings including custom skill sources, enabling/disabling specific skills, and defining custom skills.
|
||||
|
||||
```json
|
||||
{
|
||||
"skills": {
|
||||
"sources": [
|
||||
{ "path": "./custom-skills", "recursive": true },
|
||||
"https://example.com/skill.yaml"
|
||||
],
|
||||
"enable": ["my-custom-skill"],
|
||||
"disable": ["other-skill"],
|
||||
"my-skill": {
|
||||
"description": "Custom skill description",
|
||||
"template": "Custom prompt template",
|
||||
"from": "source-file.ts",
|
||||
"model": "custom/model",
|
||||
"agent": "custom-agent",
|
||||
"subtask": true,
|
||||
"argument-hint": "usage hint",
|
||||
"license": "MIT",
|
||||
"compatibility": ">= 3.0.0",
|
||||
"metadata": {
|
||||
"author": "Your Name"
|
||||
},
|
||||
"allowed-tools": ["tool1", "tool2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sources
|
||||
|
||||
Load skills from local directories or remote URLs:
|
||||
|
||||
```json
|
||||
{
|
||||
"skills": {
|
||||
"sources": [
|
||||
{ "path": "./custom-skills", "recursive": true },
|
||||
{ "path": "./single-skill.yaml" },
|
||||
"https://example.com/skill.yaml",
|
||||
"https://raw.githubusercontent.com/user/repo/main/skills/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ----------- | ------- | ---------------------------------------------- |
|
||||
| `path` | - | Local file/directory path or remote URL |
|
||||
| `recursive` | `false` | Recursively load from directory |
|
||||
| `glob` | - | Glob pattern for file selection |
|
||||
|
||||
### Enable/Disable Skills
|
||||
|
||||
```json
|
||||
{
|
||||
"skills": {
|
||||
"enable": ["skill-1", "skill-2"],
|
||||
"disable": ["disabled-skill"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Skill Definition
|
||||
|
||||
Define custom skills directly in your config:
|
||||
|
||||
| Option | Default | Description |
|
||||
| ---------------- | ------- | ------------------------------------------------------------------------------------ |
|
||||
| `description` | - | Human-readable description of the skill |
|
||||
| `template` | - | Custom prompt template for the skill |
|
||||
| `from` | - | Source file to load template from |
|
||||
| `model` | - | Override model for this skill |
|
||||
| `agent` | - | Override agent for this skill |
|
||||
| `subtask` | `false` | Whether to run as a subtask |
|
||||
| `argument-hint` | - | Hint for how to use the skill |
|
||||
| `license` | - | Skill license |
|
||||
| `compatibility` | - | Required oh-my-opencode version compatibility |
|
||||
| `metadata` | - | Additional metadata as key-value pairs |
|
||||
| `allowed-tools` | - | Array of tools this skill is allowed to use |
|
||||
|
||||
**Example: Custom skill**
|
||||
|
||||
```json
|
||||
{
|
||||
"skills": {
|
||||
"data-analyst": {
|
||||
"description": "Specialized for data analysis tasks",
|
||||
"template": "You are a data analyst. Focus on statistical analysis, visualization, and data interpretation.",
|
||||
"model": "openai/gpt-5.2",
|
||||
"allowed-tools": ["read", "bash", "lsp_diagnostics"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Browser Automation
|
||||
|
||||
Choose between two browser automation providers:
|
||||
|
||||
| Provider | Interface | Features | Installation |
|
||||
|----------|-----------|----------|--------------|
|
||||
| **playwright** (default) | MCP tools | Playwright MCP server with structured tool calls | Auto-installed via npx |
|
||||
| **agent-browser** | Bash CLI | Vercel's CLI with session management, parallel browsers | Requires `bun add -g agent-browser` |
|
||||
|
||||
**Switch providers** via `browser_automation_engine` in `oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"browser_automation_engine": {
|
||||
"provider": "agent-browser"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Playwright (Default)
|
||||
|
||||
Uses the official Playwright MCP server (`@playwright/mcp`). Browser automation happens through structured MCP tool calls.
|
||||
|
||||
### agent-browser
|
||||
|
||||
Uses [Vercel's agent-browser CLI](https://github.com/vercel-labs/agent-browser). Key advantages:
|
||||
- **Session management**: Run multiple isolated browser instances with `--session` flag
|
||||
- **Persistent profiles**: Keep browser state across restarts with `--profile`
|
||||
- **Snapshot-based workflow**: Get element refs via `snapshot -i`, interact with `@e1`, `@e2`, etc.
|
||||
- **CLI-first**: All commands via Bash - great for scripting
|
||||
|
||||
**Installation required**:
|
||||
```bash
|
||||
bun add -g agent-browser
|
||||
agent-browser install # Download Chromium
|
||||
```
|
||||
|
||||
**Example workflow**:
|
||||
```bash
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i # Get interactive elements with refs
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser click @e2
|
||||
agent-browser screenshot result.png
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
## Tmux Integration
|
||||
|
||||
Run background subagents in separate tmux panes for **visual multi-agent execution**. See your agents working in parallel, each in their own terminal pane.
|
||||
|
||||
**Enable tmux integration** via `tmux` in `oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"tmux": {
|
||||
"enabled": true,
|
||||
"layout": "main-vertical",
|
||||
"main_pane_size": 60,
|
||||
"main_pane_min_width": 120,
|
||||
"agent_pane_min_width": 40
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `enabled` | `false` | Enable tmux subagent pane spawning. Only works when running inside an existing tmux session. |
|
||||
| `layout` | `main-vertical` | Tmux layout for agent panes. See [Layout Options](#layout-options) below. |
|
||||
| `main_pane_size` | `60` | Main pane size as percentage (20-80). |
|
||||
| `main_pane_min_width` | `120` | Minimum width for main pane in columns. |
|
||||
| `agent_pane_min_width` | `40` | Minimum width for each agent pane in columns. |
|
||||
|
||||
### Layout Options
|
||||
|
||||
| Layout | Description |
|
||||
|--------|-------------|
|
||||
| `main-vertical` | Main pane left, agent panes stacked on right (default) |
|
||||
| `main-horizontal` | Main pane top, agent panes stacked bottom |
|
||||
| `tiled` | All panes in equal-sized grid |
|
||||
| `even-horizontal` | All panes in horizontal row |
|
||||
| `even-vertical` | All panes in vertical stack |
|
||||
|
||||
### Requirements
|
||||
|
||||
1. **Must run inside tmux**: The feature only activates when OpenCode is already running inside a tmux session
|
||||
2. **Tmux installed**: Requires tmux to be available in PATH
|
||||
3. **Server mode**: OpenCode must run with `--port` flag to enable subagent pane spawning
|
||||
|
||||
### How It Works
|
||||
|
||||
When `tmux.enabled` is `true` and you're inside a tmux session:
|
||||
- Background agents (via `delegate_task(run_in_background=true)`) spawn in new tmux panes
|
||||
- Each pane shows the subagent's real-time output
|
||||
- Panes are automatically closed when the subagent completes
|
||||
- Layout is automatically adjusted based on your configuration
|
||||
|
||||
### Running OpenCode with Tmux Subagent Support
|
||||
|
||||
To enable tmux subagent panes, OpenCode must run in **server mode** with the `--port` flag. This starts an HTTP server that subagent panes connect to via `opencode attach`.
|
||||
|
||||
**Basic setup**:
|
||||
```bash
|
||||
# Start tmux session
|
||||
tmux new -s dev
|
||||
|
||||
# Run OpenCode with server mode (port 4096)
|
||||
opencode --port 4096
|
||||
|
||||
# Now background agents will appear in separate panes
|
||||
```
|
||||
|
||||
**Recommended: Shell Function**
|
||||
|
||||
For convenience, create a shell function that automatically handles tmux sessions and port allocation. Here's an example for Fish shell:
|
||||
|
||||
```fish
|
||||
# ~/.config/fish/config.fish
|
||||
function oc
|
||||
set base_name (basename (pwd))
|
||||
set path_hash (echo (pwd) | md5 | cut -c1-4)
|
||||
set session_name "$base_name-$path_hash"
|
||||
|
||||
# Find available port starting from 4096
|
||||
function __oc_find_port
|
||||
set port 4096
|
||||
while test $port -lt 5096
|
||||
if not lsof -i :$port >/dev/null 2>&1
|
||||
echo $port
|
||||
return 0
|
||||
end
|
||||
set port (math $port + 1)
|
||||
end
|
||||
echo 4096
|
||||
end
|
||||
|
||||
set oc_port (__oc_find_port)
|
||||
set -x OPENCODE_PORT $oc_port
|
||||
|
||||
if set -q TMUX
|
||||
# Already inside tmux - just run with port
|
||||
opencode --port $oc_port $argv
|
||||
else
|
||||
# Create tmux session and run opencode
|
||||
set oc_cmd "OPENCODE_PORT=$oc_port opencode --port $oc_port $argv; exec fish"
|
||||
if tmux has-session -t "$session_name" 2>/dev/null
|
||||
tmux new-window -t "$session_name" -c (pwd) "$oc_cmd"
|
||||
tmux attach-session -t "$session_name"
|
||||
else
|
||||
tmux new-session -s "$session_name" -c (pwd) "$oc_cmd"
|
||||
end
|
||||
end
|
||||
|
||||
functions -e __oc_find_port
|
||||
end
|
||||
```
|
||||
|
||||
**Bash/Zsh equivalent**:
|
||||
|
||||
```bash
|
||||
# ~/.bashrc or ~/.zshrc
|
||||
oc() {
|
||||
local base_name=$(basename "$PWD")
|
||||
local path_hash=$(echo "$PWD" | md5sum | cut -c1-4)
|
||||
local session_name="${base_name}-${path_hash}"
|
||||
|
||||
# Find available port
|
||||
local port=4096
|
||||
while [ $port -lt 5096 ]; do
|
||||
if ! lsof -i :$port >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
port=$((port + 1))
|
||||
done
|
||||
|
||||
export OPENCODE_PORT=$port
|
||||
|
||||
if [ -n "$TMUX" ]; then
|
||||
opencode --port $port "$@"
|
||||
else
|
||||
local oc_cmd="OPENCODE_PORT=$port opencode --port $port $*; exec $SHELL"
|
||||
if tmux has-session -t "$session_name" 2>/dev/null; then
|
||||
tmux new-window -t "$session_name" -c "$PWD" "$oc_cmd"
|
||||
tmux attach-session -t "$session_name"
|
||||
else
|
||||
tmux new-session -s "$session_name" -c "$PWD" "$oc_cmd"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
**How subagent panes work**:
|
||||
|
||||
1. Main OpenCode starts HTTP server on specified port (e.g., `http://localhost:4096`)
|
||||
2. When a background agent spawns, Oh My OpenCode creates a new tmux pane
|
||||
3. The pane runs: `opencode attach http://localhost:4096 --session <session-id>`
|
||||
4. Each subagent pane shows real-time streaming output
|
||||
5. Panes are automatically closed when the subagent completes
|
||||
|
||||
**Environment variables**:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `OPENCODE_PORT` | Default port for the HTTP server (used if `--port` not specified) |
|
||||
|
||||
### Server Mode Reference
|
||||
|
||||
OpenCode's server mode exposes an HTTP API for programmatic interaction:
|
||||
|
||||
```bash
|
||||
# Standalone server (no TUI)
|
||||
opencode serve --port 4096
|
||||
|
||||
# TUI with server (recommended for tmux integration)
|
||||
opencode --port 4096
|
||||
```
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--port` | `4096` | Port for HTTP server |
|
||||
| `--hostname` | `127.0.0.1` | Hostname to listen on |
|
||||
|
||||
For more details, see the [OpenCode Server documentation](https://opencode.ai/docs/server/).
|
||||
|
||||
## Git Master
|
||||
|
||||
@@ -271,6 +686,7 @@ Configure concurrency limits for background agent tasks. This controls how many
|
||||
{
|
||||
"background_task": {
|
||||
"defaultConcurrency": 5,
|
||||
"staleTimeoutMs": 180000,
|
||||
"providerConcurrency": {
|
||||
"anthropic": 3,
|
||||
"openai": 5,
|
||||
@@ -287,6 +703,7 @@ Configure concurrency limits for background agent tasks. This controls how many
|
||||
| Option | Default | Description |
|
||||
| --------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `defaultConcurrency` | - | Default maximum concurrent background tasks for all providers/models |
|
||||
| `staleTimeoutMs` | `180000` | Stale timeout in milliseconds - interrupt tasks with no activity for this duration (minimum: 60000 = 1 minute) |
|
||||
| `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. |
|
||||
|
||||
@@ -301,27 +718,96 @@ Configure concurrency limits for background agent tasks. This controls how many
|
||||
|
||||
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:**
|
||||
### Built-in 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). |
|
||||
All 7 categories come with optimal model defaults, but **you must configure them to use those defaults**:
|
||||
|
||||
**Usage:**
|
||||
| Category | Built-in Default Model | Description |
|
||||
| -------------------- | ---------------------------------- | -------------------------------------------------------------------- |
|
||||
| `visual-engineering` | `google/gemini-3-pro-preview` | Frontend, UI/UX, design, styling, animation |
|
||||
| `ultrabrain` | `openai/gpt-5.2-codex` (xhigh) | Deep logical reasoning, complex architecture decisions |
|
||||
| `artistry` | `google/gemini-3-pro-preview` (max)| Highly creative/artistic tasks, novel ideas |
|
||||
| `quick` | `anthropic/claude-haiku-4-5` | Trivial tasks - single file changes, typo fixes, simple modifications|
|
||||
| `unspecified-low` | `anthropic/claude-sonnet-4-5` | Tasks that don't fit other categories, low effort required |
|
||||
| `unspecified-high` | `anthropic/claude-opus-4-5` (max) | Tasks that don't fit other categories, high effort required |
|
||||
| `writing` | `google/gemini-3-flash-preview` | Documentation, prose, technical writing |
|
||||
|
||||
### ⚠️ Critical: Model Resolution Priority
|
||||
|
||||
**Categories DO NOT use their built-in defaults unless configured.** Model resolution follows this priority:
|
||||
|
||||
```
|
||||
// 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")
|
||||
1. User-configured model (in oh-my-opencode.json)
|
||||
2. Category's built-in default (if you add category to config)
|
||||
3. System default model (from opencode.json)
|
||||
```
|
||||
|
||||
// Or target a specific agent directly
|
||||
**Example Problem:**
|
||||
|
||||
```json
|
||||
// opencode.json
|
||||
{ "model": "anthropic/claude-sonnet-4-5" }
|
||||
|
||||
// oh-my-opencode.json (empty categories section)
|
||||
{}
|
||||
|
||||
// Result: ALL categories use claude-sonnet-4-5 (wasteful!)
|
||||
// - quick tasks use Sonnet instead of Haiku (expensive)
|
||||
// - ultrabrain uses Sonnet instead of GPT-5.2 (inferior reasoning)
|
||||
// - visual tasks use Sonnet instead of Gemini (suboptimal for UI)
|
||||
```
|
||||
|
||||
### Recommended Configuration
|
||||
|
||||
**To use optimal models for each category, add them to your config:**
|
||||
|
||||
```json
|
||||
{
|
||||
"categories": {
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview"
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
"variant": "xhigh"
|
||||
},
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"variant": "max"
|
||||
},
|
||||
"quick": {
|
||||
"model": "anthropic/claude-haiku-4-5" // Fast + cheap for trivial tasks
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "anthropic/claude-sonnet-4-5"
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
"variant": "max"
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Only configure categories you have access to.** Unconfigured categories fall back to your system default model.
|
||||
|
||||
### Usage
|
||||
|
||||
```javascript
|
||||
// Via delegate_task tool
|
||||
delegate_task(category="visual-engineering", prompt="Create a responsive dashboard component")
|
||||
delegate_task(category="ultrabrain", prompt="Design the payment processing flow")
|
||||
|
||||
// Or target a specific agent directly (bypasses categories)
|
||||
delegate_task(agent="oracle", prompt="Review this architecture")
|
||||
```
|
||||
|
||||
**Custom Categories:**
|
||||
### Custom Categories
|
||||
|
||||
Add custom categories in `oh-my-opencode.json`:
|
||||
Add your own categories or override built-in ones:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -331,7 +817,7 @@ Add custom categories in `oh-my-opencode.json`:
|
||||
"temperature": 0.2,
|
||||
"prompt_append": "Focus on data analysis, ML pipelines, and statistical methods."
|
||||
},
|
||||
"visual": {
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"prompt_append": "Use shadcn/ui components and Tailwind CSS."
|
||||
}
|
||||
@@ -339,7 +825,14 @@ Add custom categories in `oh-my-opencode.json`:
|
||||
}
|
||||
```
|
||||
|
||||
Each category supports: `model`, `temperature`, `top_p`, `maxTokens`, `thinking`, `reasoningEffort`, `textVerbosity`, `tools`, `prompt_append`.
|
||||
Each category supports: `model`, `temperature`, `top_p`, `maxTokens`, `thinking`, `reasoningEffort`, `textVerbosity`, `tools`, `prompt_append`, `variant`, `description`, `is_unstable_agent`.
|
||||
|
||||
### Additional Category Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------------ | ------- | ------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `description` | string | - | Human-readable description of the category's purpose. Shown in delegate_task prompt. |
|
||||
| `is_unstable_agent`| boolean | `false` | Mark agent as unstable - forces background mode for monitoring. Auto-enabled for gemini models. |
|
||||
|
||||
## Model Resolution System
|
||||
|
||||
@@ -403,9 +896,9 @@ Each agent has a defined provider priority chain. The system tries providers in
|
||||
|-------|-------------------|-------------------------|
|
||||
| **Sisyphus** | `claude-opus-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
| **oracle** | `gpt-5.2` | openai → anthropic → google → github-copilot → opencode |
|
||||
| **librarian** | `glm-4.7-free` | opencode → github-copilot → anthropic |
|
||||
| **explore** | `grok-code` | opencode → anthropic → github-copilot |
|
||||
| **multimodal-looker** | `gemini-3-flash-preview` | google → anthropic → zai → openai → github-copilot → opencode |
|
||||
| **librarian** | `big-pickle` | opencode → github-copilot → anthropic |
|
||||
| **explore** | `gpt-5-nano` | anthropic → opencode |
|
||||
| **multimodal-looker** | `gemini-3-flash` | google → openai → zai-coding-plan → anthropic → opencode |
|
||||
| **Prometheus (Planner)** | `claude-opus-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
| **Metis (Plan Consultant)** | `claude-sonnet-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
| **Momus (Plan Reviewer)** | `claude-opus-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
@@ -417,13 +910,13 @@ Categories follow the same resolution logic:
|
||||
|
||||
| Category | Model (no prefix) | Provider Priority Chain |
|
||||
|----------|-------------------|-------------------------|
|
||||
| **visual-engineering** | `gemini-3-pro-preview` | google → openai → anthropic → github-copilot → opencode |
|
||||
| **visual-engineering** | `gemini-3-pro` | google → openai → anthropic → github-copilot → opencode |
|
||||
| **ultrabrain** | `gpt-5.2-codex` | openai → anthropic → google → github-copilot → opencode |
|
||||
| **artistry** | `gemini-3-pro-preview` | google → openai → anthropic → github-copilot → opencode |
|
||||
| **artistry** | `gemini-3-pro` | google → openai → anthropic → github-copilot → opencode |
|
||||
| **quick** | `claude-haiku-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
| **unspecified-low** | `claude-sonnet-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
| **unspecified-high** | `claude-opus-4-5` | anthropic → github-copilot → opencode → antigravity → google |
|
||||
| **writing** | `gemini-3-flash-preview` | google → openai → anthropic → github-copilot → opencode |
|
||||
| **writing** | `gemini-3-flash` | google → openai → anthropic → github-copilot → opencode |
|
||||
|
||||
### Checking Your Configuration
|
||||
|
||||
@@ -473,10 +966,93 @@ 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`, `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`, `auto-slash-command`, `sisyphus-junior-notepad`, `start-work`
|
||||
|
||||
**Note on `directory-agents-injector`**: This hook is **automatically disabled** when running on OpenCode 1.1.37+ because OpenCode now has native support for dynamically resolving AGENTS.md files from subdirectories (PR #10678). This prevents duplicate AGENTS.md injection. For older OpenCode versions, the hook remains active to provide the same functionality.
|
||||
|
||||
**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`.
|
||||
|
||||
## Disabled Commands
|
||||
|
||||
Disable specific built-in commands via `disabled_commands` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_commands": ["init-deep", "start-work"]
|
||||
}
|
||||
```
|
||||
|
||||
Available commands: `init-deep`, `start-work`
|
||||
|
||||
## Comment Checker
|
||||
|
||||
Configure comment-checker hook behavior. The comment checker warns when excessive comments are added to code.
|
||||
|
||||
```json
|
||||
{
|
||||
"comment_checker": {
|
||||
"custom_prompt": "Your custom warning message. Use {{comments}} placeholder for detected comments XML."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ------------- | ------- | -------------------------------------------------------------------------- |
|
||||
| `custom_prompt` | - | Custom warning message to replace the default. Use `{{comments}}` placeholder. |
|
||||
|
||||
## Notification
|
||||
|
||||
Configure notification behavior for background task completion.
|
||||
|
||||
```json
|
||||
{
|
||||
"notification": {
|
||||
"force_enable": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| -------------- | ------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `force_enable` | `false` | Force enable session-notification even if external notification plugins are detected. Default: `false`. |
|
||||
|
||||
## Sisyphus Tasks & Swarm
|
||||
|
||||
Configure Sisyphus Tasks and Swarm systems for advanced task management and multi-agent orchestration.
|
||||
|
||||
```json
|
||||
{
|
||||
"sisyphus": {
|
||||
"tasks": {
|
||||
"enabled": false,
|
||||
"storage_path": ".sisyphus/tasks",
|
||||
"claude_code_compat": false
|
||||
},
|
||||
"swarm": {
|
||||
"enabled": false,
|
||||
"storage_path": ".sisyphus/teams",
|
||||
"ui_mode": "toast"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tasks Configuration
|
||||
|
||||
| Option | Default | Description |
|
||||
| -------------------- | ------------------ | ------------------------------------------------------------------------- |
|
||||
| `enabled` | `false` | Enable Sisyphus Tasks system |
|
||||
| `storage_path` | `.sisyphus/tasks` | Storage path for tasks (relative to project root) |
|
||||
| `claude_code_compat` | `false` | Enable Claude Code path compatibility mode |
|
||||
|
||||
### Swarm Configuration
|
||||
|
||||
| Option | Default | Description |
|
||||
| -------------- | ------------------ | -------------------------------------------------------------- |
|
||||
| `enabled` | `false` | Enable Sisyphus Swarm system for multi-agent orchestration |
|
||||
| `storage_path` | `.sisyphus/teams` | Storage path for teams (relative to project root) |
|
||||
| `ui_mode` | `toast` | UI mode: `toast` (notifications), `tmux` (panes), or `both` |
|
||||
|
||||
## MCPs
|
||||
|
||||
Exa, Context7 and grep.app MCP enabled by default.
|
||||
@@ -518,6 +1094,38 @@ Add LSP servers via the `lsp` option in `~/.config/opencode/oh-my-opencode.json`
|
||||
|
||||
Each server supports: `command`, `extensions`, `priority`, `env`, `initialization`, `disabled`.
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| -------------- | -------- | ------- | ---------------------------------------------------------------------- |
|
||||
| `command` | array | - | Command to start the LSP server (executable + args) |
|
||||
| `extensions` | array | - | File extensions this server handles (e.g., `[".ts", ".tsx"]`) |
|
||||
| `priority` | number | - | Server priority when multiple servers match a file |
|
||||
| `env` | object | - | Environment variables for the LSP server (key-value pairs) |
|
||||
| `initialization`| object | - | Custom initialization options passed to the LSP server |
|
||||
| `disabled` | boolean | `false` | Whether to disable this LSP server |
|
||||
|
||||
**Example with advanced options:**
|
||||
|
||||
```json
|
||||
{
|
||||
"lsp": {
|
||||
"typescript-language-server": {
|
||||
"command": ["typescript-language-server", "--stdio"],
|
||||
"extensions": [".ts", ".tsx"],
|
||||
"priority": 10,
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--max-old-space-size=4096"
|
||||
},
|
||||
"initialization": {
|
||||
"preferences": {
|
||||
"includeInlayParameterNameHints": "all",
|
||||
"includeInlayFunctionParameterTypeHints": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Experimental
|
||||
|
||||
Opt-in experimental features that may change or be removed in future versions. Use with caution.
|
||||
@@ -527,7 +1135,29 @@ Opt-in experimental features that may change or be removed in future versions. U
|
||||
"experimental": {
|
||||
"truncate_all_tool_outputs": true,
|
||||
"aggressive_truncation": true,
|
||||
"auto_resume": true
|
||||
"auto_resume": true,
|
||||
"dynamic_context_pruning": {
|
||||
"enabled": false,
|
||||
"notification": "detailed",
|
||||
"turn_protection": {
|
||||
"enabled": true,
|
||||
"turns": 3
|
||||
},
|
||||
"protected_tools": ["task", "todowrite", "lsp_rename"],
|
||||
"strategies": {
|
||||
"deduplication": {
|
||||
"enabled": true
|
||||
},
|
||||
"supersede_writes": {
|
||||
"enabled": true,
|
||||
"aggressive": false
|
||||
},
|
||||
"purge_errors": {
|
||||
"enabled": true,
|
||||
"turns": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -536,7 +1166,72 @@ Opt-in experimental features that may change or be removed in future versions. U
|
||||
| --------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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. |
|
||||
| `auto_resume` | `false` | Automatically resumes session after successful recovery from thinking block errors or thinking disabled violations. Extracts last user message and continues. |
|
||||
| `dynamic_context_pruning` | See below | Dynamic context pruning configuration for managing context window usage automatically. See [Dynamic Context Pruning](#dynamic-context-pruning) below. |
|
||||
|
||||
### Dynamic Context Pruning
|
||||
|
||||
Dynamic context pruning automatically manages context window by intelligently pruning old tool outputs. This feature helps maintain performance in long sessions.
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"dynamic_context_pruning": {
|
||||
"enabled": false,
|
||||
"notification": "detailed",
|
||||
"turn_protection": {
|
||||
"enabled": true,
|
||||
"turns": 3
|
||||
},
|
||||
"protected_tools": ["task", "todowrite", "todoread", "lsp_rename", "session_read", "session_write", "session_search"],
|
||||
"strategies": {
|
||||
"deduplication": {
|
||||
"enabled": true
|
||||
},
|
||||
"supersede_writes": {
|
||||
"enabled": true,
|
||||
"aggressive": false
|
||||
},
|
||||
"purge_errors": {
|
||||
"enabled": true,
|
||||
"turns": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ----------------- | ------- | ----------------------------------------------------------------------------------------- |
|
||||
| `enabled` | `false` | Enable dynamic context pruning |
|
||||
| `notification` | `detailed` | Notification level: `off`, `minimal`, or `detailed` |
|
||||
| `turn_protection` | See below | Turn protection settings - prevent pruning recent tool outputs |
|
||||
|
||||
#### Turn Protection
|
||||
|
||||
| Option | Default | Description |
|
||||
| --------- | ------- | ------------------------------------------------------------ |
|
||||
| `enabled` | `true` | Enable turn protection |
|
||||
| `turns` | `3` | Number of recent turns to protect from pruning (1-10) |
|
||||
|
||||
#### Protected Tools
|
||||
|
||||
Tools that should never be pruned (default):
|
||||
|
||||
```json
|
||||
["task", "todowrite", "todoread", "lsp_rename", "session_read", "session_write", "session_search"]
|
||||
```
|
||||
|
||||
#### Pruning Strategies
|
||||
|
||||
| Strategy | Option | Default | Description |
|
||||
| ------------------- | ------------ | ------- | ---------------------------------------------------------------------------- |
|
||||
| **deduplication** | `enabled` | `true` | Remove duplicate tool calls (same tool + same args) |
|
||||
| **supersede_writes**| `enabled` | `true` | Prune write inputs when file subsequently read |
|
||||
| | `aggressive` | `false` | Aggressive mode: prune any write if ANY subsequent read |
|
||||
| **purge_errors** | `enabled` | `true` | Prune errored tool inputs after N turns |
|
||||
| | `turns` | `5` | Number of turns before pruning errors (1-20) |
|
||||
|
||||
**Warning**: These features are experimental and may cause unexpected behavior. Enable only if you understand the implications.
|
||||
|
||||
|
||||
126
docs/features.md
126
docs/features.md
@@ -12,9 +12,9 @@ Oh-My-OpenCode provides 10 specialized AI agents. Each has distinct expertise, o
|
||||
|-------|-------|---------|
|
||||
| **Sisyphus** | `anthropic/claude-opus-4-5` | **The default orchestrator.** Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Todo-driven workflow with extended thinking (32k budget). |
|
||||
| **oracle** | `openai/gpt-5.2` | Architecture decisions, code review, debugging. Read-only consultation - stellar logical reasoning and deep analysis. Inspired by AmpCode. |
|
||||
| **librarian** | `opencode/glm-4.7-free` | Multi-repo analysis, documentation lookup, OSS implementation examples. Deep codebase understanding with evidence-based answers. Inspired by AmpCode. |
|
||||
| **explore** | `opencode/grok-code` | Fast codebase exploration and contextual grep. Uses Gemini 3 Flash when Antigravity auth is configured, Haiku when Claude max20 is available, otherwise Grok. Inspired by Claude Code. |
|
||||
| **multimodal-looker** | `google/gemini-3-flash-preview` | Visual content specialist. Analyzes PDFs, images, diagrams to extract information. Saves tokens by having another agent process media. |
|
||||
| **librarian** | `opencode/big-pickle` | Multi-repo analysis, documentation lookup, OSS implementation examples. Deep codebase understanding with evidence-based answers. Inspired by AmpCode. |
|
||||
| **explore** | `opencode/gpt-5-nano` | Fast codebase exploration and contextual grep. Uses Gemini 3 Flash when Antigravity auth is configured, Haiku when Claude max20 is available, otherwise Grok. Inspired by Claude Code. |
|
||||
| **multimodal-looker** | `google/gemini-3-flash` | Visual content specialist. Analyzes PDFs, images, diagrams to extract information. Saves tokens by having another agent process media. |
|
||||
|
||||
### Planning Agents
|
||||
|
||||
@@ -62,6 +62,27 @@ delegate_task(agent="explore", background=true, prompt="Find auth implementation
|
||||
background_output(task_id="bg_abc123")
|
||||
```
|
||||
|
||||
#### Visual Multi-Agent with Tmux
|
||||
|
||||
Enable `tmux.enabled` to see background agents in separate tmux panes:
|
||||
|
||||
```json
|
||||
{
|
||||
"tmux": {
|
||||
"enabled": true,
|
||||
"layout": "main-vertical"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When running inside tmux:
|
||||
- Background agents spawn in new panes
|
||||
- Watch multiple agents work in real-time
|
||||
- Each pane shows agent output live
|
||||
- Auto-cleanup when agents complete
|
||||
|
||||
See [Tmux Integration](configurations.md#tmux-integration) for full configuration options.
|
||||
|
||||
Customize agent models, prompts, and permissions in `oh-my-opencode.json`. See [Configuration](configurations.md#agents).
|
||||
|
||||
---
|
||||
@@ -78,11 +99,15 @@ Skills provide specialized workflows with embedded MCP servers and detailed inst
|
||||
| **frontend-ui-ux** | UI/UX tasks, styling | Designer-turned-developer persona. Crafts stunning UI/UX even without design mockups. Emphasizes bold aesthetic direction, distinctive typography, cohesive color palettes. |
|
||||
| **git-master** | commit, rebase, squash, blame | MUST USE for ANY git operations. Atomic commits with automatic splitting, rebase/squash workflows, history search (blame, bisect, log -S). |
|
||||
|
||||
### Skill: playwright
|
||||
### Skill: Browser Automation (playwright / agent-browser)
|
||||
|
||||
**Trigger**: Any browser-related request
|
||||
|
||||
Provides browser automation via Playwright MCP server:
|
||||
Oh-My-OpenCode provides two browser automation providers, configurable via `browser_automation_engine.provider`:
|
||||
|
||||
#### Option 1: Playwright MCP (Default)
|
||||
|
||||
The default provider uses Playwright MCP server:
|
||||
|
||||
```yaml
|
||||
mcp:
|
||||
@@ -91,18 +116,41 @@ mcp:
|
||||
args: ["@playwright/mcp@latest"]
|
||||
```
|
||||
|
||||
**Capabilities**:
|
||||
**Usage**:
|
||||
```
|
||||
/playwright Navigate to example.com and take a screenshot
|
||||
```
|
||||
|
||||
#### Option 2: Agent Browser CLI (Vercel)
|
||||
|
||||
Alternative provider using [Vercel's agent-browser CLI](https://github.com/vercel-labs/agent-browser):
|
||||
|
||||
```json
|
||||
{
|
||||
"browser_automation_engine": {
|
||||
"provider": "agent-browser"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Requires installation**:
|
||||
```bash
|
||||
bun add -g agent-browser
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```
|
||||
Use agent-browser to navigate to example.com and extract the main heading
|
||||
```
|
||||
|
||||
#### Capabilities (Both Providers)
|
||||
|
||||
- Navigate and interact with web pages
|
||||
- Take screenshots and PDFs
|
||||
- Fill forms and click elements
|
||||
- Wait for network requests
|
||||
- Scrape content
|
||||
|
||||
**Usage**:
|
||||
```
|
||||
/playwright Navigate to example.com and take a screenshot
|
||||
```
|
||||
|
||||
### Skill: frontend-ui-ux
|
||||
|
||||
**Trigger**: UI design tasks, visual changes
|
||||
@@ -272,7 +320,7 @@ Hooks intercept and modify behavior at key points in the agent lifecycle.
|
||||
|
||||
| Hook | Event | Description |
|
||||
|------|-------|-------------|
|
||||
| **directory-agents-injector** | PostToolUse | Auto-injects AGENTS.md when reading files. Walks from file to project root, collecting all AGENTS.md files. |
|
||||
| **directory-agents-injector** | PostToolUse | Auto-injects AGENTS.md when reading files. Walks from file to project root, collecting all AGENTS.md files. **Deprecated for OpenCode 1.1.37+** - Auto-disabled when native AGENTS.md injection is available. |
|
||||
| **directory-readme-injector** | PostToolUse | Auto-injects README.md for directory context. |
|
||||
| **rules-injector** | PostToolUse | Injects rules from `.claude/rules/` when conditions match. Supports globs and alwaysApply. |
|
||||
| **compaction-context-injector** | Stop | Preserves critical context during session compaction. |
|
||||
@@ -418,6 +466,29 @@ Disable specific hooks in config:
|
||||
| **session_search** | Full-text search across session messages |
|
||||
| **session_info** | Get session metadata and statistics |
|
||||
|
||||
### Interactive Terminal Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| **interactive_bash** | Tmux-based terminal for TUI apps (vim, htop, pudb). Pass tmux subcommands directly without prefix. |
|
||||
|
||||
**Usage Examples**:
|
||||
```bash
|
||||
# Create a new session
|
||||
interactive_bash(tmux_command="new-session -d -s dev-app")
|
||||
|
||||
# Send keystrokes to a session
|
||||
interactive_bash(tmux_command="send-keys -t dev-app 'vim main.py' Enter")
|
||||
|
||||
# Capture pane output
|
||||
interactive_bash(tmux_command="capture-pane -p -t dev-app")
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- Commands are tmux subcommands (no `tmux` prefix)
|
||||
- Use for interactive apps that need persistent sessions
|
||||
- One-shot commands should use regular `Bash` tool with `&`
|
||||
|
||||
---
|
||||
|
||||
## MCPs: Built-in Servers
|
||||
@@ -450,6 +521,37 @@ mcp:
|
||||
|
||||
The `skill_mcp` tool invokes these operations with full schema discovery.
|
||||
|
||||
#### OAuth-Enabled MCPs
|
||||
|
||||
Skills can define OAuth-protected remote MCP servers. OAuth 2.1 with full RFC compliance (RFC 9728, 8414, 8707, 7591) is supported:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: My API skill
|
||||
mcp:
|
||||
my-api:
|
||||
url: https://api.example.com/mcp
|
||||
oauth:
|
||||
clientId: ${CLIENT_ID}
|
||||
scopes: ["read", "write"]
|
||||
---
|
||||
```
|
||||
|
||||
When a skill MCP has `oauth` configured:
|
||||
- **Auto-discovery**: Fetches `/.well-known/oauth-protected-resource` (RFC 9728), falls back to `/.well-known/oauth-authorization-server` (RFC 8414)
|
||||
- **Dynamic Client Registration**: Auto-registers with servers supporting RFC 7591 (clientId becomes optional)
|
||||
- **PKCE**: Mandatory for all flows
|
||||
- **Resource Indicators**: Auto-generated from MCP URL per RFC 8707
|
||||
- **Token Storage**: Persisted in `~/.config/opencode/mcp-oauth.json` (chmod 0600)
|
||||
- **Auto-refresh**: Tokens refresh on 401; step-up authorization on 403 with `WWW-Authenticate`
|
||||
- **Dynamic Port**: OAuth callback server uses an auto-discovered available port
|
||||
|
||||
Pre-authenticate via CLI:
|
||||
|
||||
```bash
|
||||
bunx oh-my-opencode mcp oauth login <server-name> --server-url https://api.example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context Injection
|
||||
|
||||
@@ -132,7 +132,7 @@ First, add the opencode-antigravity-auth plugin:
|
||||
{
|
||||
"plugin": [
|
||||
"oh-my-opencode",
|
||||
"opencode-antigravity-auth@1.2.8"
|
||||
"opencode-antigravity-auth@latest"
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -140,7 +140,7 @@ First, add the opencode-antigravity-auth plugin:
|
||||
##### Model Configuration
|
||||
|
||||
You'll also need full model settings in `opencode.json`.
|
||||
Read the [opencode-antigravity-auth documentation](https://github.com/NoeFabris/opencode-antigravity-auth), copy provider/models config from the README, and merge carefully to avoid breaking the user's existing setup.
|
||||
Read the [opencode-antigravity-auth documentation](https://github.com/NoeFabris/opencode-antigravity-auth), copy the full model configuration from the README, and merge carefully to avoid breaking the user's existing setup. The plugin now uses a **variant system** — models like `antigravity-gemini-3-pro` support `low`/`high` variants instead of separate `-low`/`-high` model entries.
|
||||
|
||||
##### oh-my-opencode Agent Model Override
|
||||
|
||||
@@ -154,7 +154,17 @@ The `opencode-antigravity-auth` plugin uses different model names than the built
|
||||
}
|
||||
```
|
||||
|
||||
**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`
|
||||
**Available models (Antigravity quota)**:
|
||||
- `google/antigravity-gemini-3-pro` — variants: `low`, `high`
|
||||
- `google/antigravity-gemini-3-flash` — variants: `minimal`, `low`, `medium`, `high`
|
||||
- `google/antigravity-claude-sonnet-4-5` — no variants
|
||||
- `google/antigravity-claude-sonnet-4-5-thinking` — variants: `low`, `max`
|
||||
- `google/antigravity-claude-opus-4-5-thinking` — variants: `low`, `max`
|
||||
|
||||
**Available models (Gemini CLI quota)**:
|
||||
- `google/gemini-2.5-flash`, `google/gemini-2.5-pro`, `google/gemini-3-flash-preview`, `google/gemini-3-pro-preview`
|
||||
|
||||
> **Note**: Legacy tier-suffixed names like `google/antigravity-gemini-3-pro-high` still work but variants are recommended. Use `--variant=high` with the base model name instead.
|
||||
|
||||
Then authenticate:
|
||||
|
||||
@@ -183,7 +193,7 @@ When GitHub Copilot is the best available provider, oh-my-opencode uses these mo
|
||||
| ------------- | -------------------------------- |
|
||||
| **Sisyphus** | `github-copilot/claude-opus-4.5` |
|
||||
| **Oracle** | `github-copilot/gpt-5.2` |
|
||||
| **Explore** | `github-copilot/grok-code-fast-1`|
|
||||
| **Explore** | `opencode/gpt-5-nano` |
|
||||
| **Librarian** | `zai-coding-plan/glm-4.7` (if Z.ai available) or fallback |
|
||||
|
||||
GitHub Copilot acts as a proxy provider, routing requests to underlying models based on your subscription.
|
||||
@@ -203,7 +213,7 @@ If Z.ai is the only provider available, all agents will use GLM models:
|
||||
|
||||
#### OpenCode Zen
|
||||
|
||||
OpenCode Zen provides access to `opencode/` prefixed models including `opencode/claude-opus-4-5`, `opencode/gpt-5.2`, `opencode/grok-code`, and `opencode/glm-4.7-free`.
|
||||
OpenCode Zen provides access to `opencode/` prefixed models including `opencode/claude-opus-4-5`, `opencode/gpt-5.2`, `opencode/gpt-5-nano`, and `opencode/big-pickle`.
|
||||
|
||||
When OpenCode Zen is the best available provider (no native or Copilot), these models are used:
|
||||
|
||||
@@ -211,8 +221,8 @@ When OpenCode Zen is the best available provider (no native or Copilot), these m
|
||||
| ------------- | -------------------------------- |
|
||||
| **Sisyphus** | `opencode/claude-opus-4-5` |
|
||||
| **Oracle** | `opencode/gpt-5.2` |
|
||||
| **Explore** | `opencode/grok-code` |
|
||||
| **Librarian** | `opencode/glm-4.7-free` |
|
||||
| **Explore** | `opencode/gpt-5-nano` |
|
||||
| **Librarian** | `opencode/big-pickle` |
|
||||
|
||||
##### Setup
|
||||
|
||||
|
||||
@@ -112,12 +112,12 @@ Each agent has a **provider priority chain**. The system tries providers in orde
|
||||
|
||||
```
|
||||
Example: multimodal-looker
|
||||
google → anthropic → zai → openai → github-copilot → opencode
|
||||
↓ ↓ ↓ ↓ ↓ ↓
|
||||
gemini haiku glm-4.6v gpt-5.2 fallback fallback
|
||||
google → openai → zai-coding-plan → anthropic → opencode
|
||||
↓ ↓ ↓ ↓ ↓
|
||||
gemini gpt-5.2 glm-4.6v haiku gpt-5-nano
|
||||
```
|
||||
|
||||
If you have Gemini, it uses `google/gemini-3-flash-preview`. No Gemini but have Claude? Uses `anthropic/claude-haiku-4-5`. And so on.
|
||||
If you have Gemini, it uses `google/gemini-3-flash`. No Gemini but have Claude? Uses `anthropic/claude-haiku-4-5`. And so on.
|
||||
|
||||
### Example Configuration
|
||||
|
||||
@@ -128,14 +128,14 @@ Here's a real-world config for a user with **Claude, OpenAI, Gemini, and Z.ai**
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
// Override specific agents only - rest use fallback chain
|
||||
"Atlas": { "model": "anthropic/claude-sonnet-4-5", "variant": "max" },
|
||||
"atlas": { "model": "anthropic/claude-sonnet-4-5", "variant": "max" },
|
||||
"librarian": { "model": "zai-coding-plan/glm-4.7" },
|
||||
"explore": { "model": "opencode/grok-code" },
|
||||
"explore": { "model": "opencode/gpt-5-nano" },
|
||||
"multimodal-looker": { "model": "zai-coding-plan/glm-4.6v" }
|
||||
},
|
||||
"categories": {
|
||||
// Override categories for cost optimization
|
||||
"quick": { "model": "opencode/grok-code" },
|
||||
"quick": { "model": "opencode/gpt-5-nano" },
|
||||
"unspecified-low": { "model": "zai-coding-plan/glm-4.7" }
|
||||
},
|
||||
"experimental": {
|
||||
|
||||
@@ -326,13 +326,13 @@ Skills prepend specialized instructions to subagent prompts:
|
||||
// Category + Skill combination
|
||||
delegate_task(
|
||||
category="visual-engineering",
|
||||
skills=["frontend-ui-ux"], // Adds UI/UX expertise
|
||||
load_skills=["frontend-ui-ux"], // Adds UI/UX expertise
|
||||
prompt="..."
|
||||
)
|
||||
|
||||
delegate_task(
|
||||
category="general",
|
||||
skills=["playwright"], // Adds browser automation expertise
|
||||
load_skills=["playwright"], // Adds browser automation expertise
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
@@ -341,8 +341,8 @@ delegate_task(
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| Hardcoded: `frontend-ui-ux-engineer` (Gemini 3 Pro) | `category="visual-engineering" + skills=["frontend-ui-ux"]` |
|
||||
| One-size-fits-all | `category="visual-engineering" + skills=["unity-master"]` |
|
||||
| Hardcoded: `frontend-ui-ux-engineer` (Gemini 3 Pro) | `category="visual-engineering" + load_skills=["frontend-ui-ux"]` |
|
||||
| One-size-fits-all | `category="visual-engineering" + load_skills=["unity-master"]` |
|
||||
| Model bias | Category-based: model abstraction eliminates bias |
|
||||
|
||||
---
|
||||
@@ -365,7 +365,7 @@ sequenceDiagram
|
||||
|
||||
Note over Orchestrator: Prompt Structure:<br/>1. TASK (exact checkbox)<br/>2. EXPECTED OUTCOME<br/>3. REQUIRED SKILLS<br/>4. REQUIRED TOOLS<br/>5. MUST DO<br/>6. MUST NOT DO<br/>7. CONTEXT + Wisdom
|
||||
|
||||
Orchestrator->>Junior: delegate_task(category, skills, prompt)
|
||||
Orchestrator->>Junior: delegate_task(category, load_skills, prompt)
|
||||
|
||||
Junior->>Junior: Create todos, execute
|
||||
Junior->>Junior: Verify (lsp_diagnostics, tests)
|
||||
|
||||
126
docs/troubleshooting/ollama-streaming-issue.md
Normal file
126
docs/troubleshooting/ollama-streaming-issue.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Ollama Streaming Issue - JSON Parse Error
|
||||
|
||||
## Problem
|
||||
|
||||
When using Ollama as a provider with oh-my-opencode agents, you may encounter:
|
||||
|
||||
```
|
||||
JSON Parse error: Unexpected EOF
|
||||
```
|
||||
|
||||
This occurs when agents attempt tool calls (e.g., `explore` agent using `mcp_grep_search`).
|
||||
|
||||
## Root Cause
|
||||
|
||||
Ollama returns **NDJSON** (newline-delimited JSON) when `stream: true` is used in API requests:
|
||||
|
||||
```json
|
||||
{"message":{"tool_calls":[{"function":{"name":"read","arguments":{"filePath":"README.md"}}}]}, "done":false}
|
||||
{"message":{"content":""}, "done":true}
|
||||
```
|
||||
|
||||
Claude Code SDK expects a single JSON object, not multiple NDJSON lines, causing the parse error.
|
||||
|
||||
### Why This Happens
|
||||
|
||||
- **Ollama API**: Returns streaming responses as NDJSON by design
|
||||
- **Claude Code SDK**: Doesn't properly handle NDJSON responses for tool calls
|
||||
- **oh-my-opencode**: Passes through the SDK's behavior (can't fix at this layer)
|
||||
|
||||
## Solutions
|
||||
|
||||
### Option 1: Disable Streaming (Recommended - Immediate Fix)
|
||||
|
||||
Configure your Ollama provider to use `stream: false`:
|
||||
|
||||
```json
|
||||
{
|
||||
"provider": "ollama",
|
||||
"model": "qwen3-coder",
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Works immediately
|
||||
- No code changes needed
|
||||
- Simple configuration
|
||||
|
||||
**Cons:**
|
||||
- Slightly slower response time (no streaming)
|
||||
- Less interactive feedback
|
||||
|
||||
### Option 2: Use Non-Tool Agents Only
|
||||
|
||||
If you need streaming, avoid agents that use tools:
|
||||
|
||||
- ✅ **Safe**: Simple text generation, non-tool tasks
|
||||
- ❌ **Problematic**: Any agent with tool calls (explore, librarian, etc.)
|
||||
|
||||
### Option 3: Wait for SDK Fix (Long-term)
|
||||
|
||||
The proper fix requires Claude Code SDK to:
|
||||
|
||||
1. Detect NDJSON responses
|
||||
2. Parse each line separately
|
||||
3. Merge `tool_calls` from multiple lines
|
||||
4. Return a single merged response
|
||||
|
||||
**Tracking**: https://github.com/code-yeongyu/oh-my-opencode/issues/1124
|
||||
|
||||
## Workaround Implementation
|
||||
|
||||
Until the SDK is fixed, here's how to implement NDJSON parsing (for SDK maintainers):
|
||||
|
||||
```typescript
|
||||
async function parseOllamaStreamResponse(response: string): Promise<object> {
|
||||
const lines = response.split('\n').filter(line => line.trim());
|
||||
const mergedMessage = { tool_calls: [] };
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const json = JSON.parse(line);
|
||||
if (json.message?.tool_calls) {
|
||||
mergedMessage.tool_calls.push(...json.message.tool_calls);
|
||||
}
|
||||
if (json.message?.content) {
|
||||
mergedMessage.content = json.message.content;
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip malformed lines
|
||||
console.warn('Skipping malformed NDJSON line:', line);
|
||||
}
|
||||
}
|
||||
|
||||
return mergedMessage;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
To verify the fix works:
|
||||
|
||||
```bash
|
||||
# Test with curl (should work with stream: false)
|
||||
curl -s http://localhost:11434/api/chat \
|
||||
-d '{
|
||||
"model": "qwen3-coder",
|
||||
"messages": [{"role": "user", "content": "Read file README.md"}],
|
||||
"stream": false,
|
||||
"tools": [{"type": "function", "function": {"name": "read", "description": "Read a file", "parameters": {"type": "object", "properties": {"filePath": {"type": "string"}}, "required": ["filePath"]}}}]
|
||||
}'
|
||||
```
|
||||
|
||||
## Related Issues
|
||||
|
||||
- **oh-my-opencode**: https://github.com/code-yeongyu/oh-my-opencode/issues/1124
|
||||
- **Ollama API Docs**: https://github.com/ollama/ollama/blob/main/docs/api.md
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you encounter this issue:
|
||||
|
||||
1. Check your Ollama provider configuration
|
||||
2. Set `stream: false` as a workaround
|
||||
3. Report any additional errors to the issue tracker
|
||||
4. Provide your configuration (without secrets) for debugging
|
||||
17
package.json
17
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.1.7",
|
||||
"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",
|
||||
@@ -64,6 +64,7 @@
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"picocolors": "^1.1.1",
|
||||
"picomatch": "^4.0.2",
|
||||
"vscode-jsonrpc": "^8.2.0",
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -73,13 +74,13 @@
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"oh-my-opencode-darwin-arm64": "3.0.0-beta.14",
|
||||
"oh-my-opencode-darwin-x64": "3.0.0-beta.14",
|
||||
"oh-my-opencode-linux-arm64": "3.0.0-beta.14",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.0.0-beta.14",
|
||||
"oh-my-opencode-linux-x64": "3.0.0-beta.14",
|
||||
"oh-my-opencode-linux-x64-musl": "3.0.0-beta.14",
|
||||
"oh-my-opencode-windows-x64": "3.0.0-beta.14"
|
||||
"oh-my-opencode-darwin-arm64": "3.1.7",
|
||||
"oh-my-opencode-darwin-x64": "3.1.7",
|
||||
"oh-my-opencode-linux-arm64": "3.1.7",
|
||||
"oh-my-opencode-linux-arm64-musl": "3.1.7",
|
||||
"oh-my-opencode-linux-x64": "3.1.7",
|
||||
"oh-my-opencode-linux-x64-musl": "3.1.7",
|
||||
"oh-my-opencode-windows-x64": "3.1.7"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@ast-grep/cli",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-arm64",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.1.7",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-arm64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
22
packages/darwin-x64-baseline/package.json
Normal file
22
packages/darwin-x64-baseline/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-x64-baseline",
|
||||
"version": "3.1.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-x64-baseline, no AVX2)",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-darwin-x64",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.1.7",
|
||||
"description": "Platform-specific binary for oh-my-opencode (darwin-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-arm64-musl",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.1.7",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64-musl)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-arm64",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.1.7",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-arm64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
25
packages/linux-x64-baseline/package.json
Normal file
25
packages/linux-x64-baseline/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64-baseline",
|
||||
"version": "3.1.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64-baseline, no AVX2)",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
25
packages/linux-x64-musl-baseline/package.json
Normal file
25
packages/linux-x64-musl-baseline/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64-musl-baseline",
|
||||
"version": "3.1.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl-baseline, no AVX2)",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64-musl",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.1.7",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-linux-x64",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.1.7",
|
||||
"description": "Platform-specific binary for oh-my-opencode (linux-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
22
packages/windows-x64-baseline/package.json
Normal file
22
packages/windows-x64-baseline/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "oh-my-opencode-windows-x64-baseline",
|
||||
"version": "3.1.1",
|
||||
"description": "Platform-specific binary for oh-my-opencode (windows-x64-baseline, no AVX2)",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode-windows-x64",
|
||||
"version": "3.0.0-beta.14",
|
||||
"version": "3.1.7",
|
||||
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
79
script/build-binaries.test.ts
Normal file
79
script/build-binaries.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
// script/build-binaries.test.ts
|
||||
// Tests for platform binary build configuration
|
||||
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
// Import PLATFORMS from build-binaries.ts
|
||||
// We need to export it first, but for now we'll test the expected structure
|
||||
const EXPECTED_BASELINE_TARGETS = [
|
||||
"bun-linux-x64-baseline",
|
||||
"bun-linux-x64-musl-baseline",
|
||||
"bun-darwin-x64-baseline",
|
||||
"bun-windows-x64-baseline",
|
||||
];
|
||||
|
||||
describe("build-binaries", () => {
|
||||
describe("PLATFORMS array", () => {
|
||||
it("includes baseline variants for non-AVX2 CPU support", async () => {
|
||||
// given
|
||||
const module = await import("./build-binaries.ts");
|
||||
const platforms = (module as { PLATFORMS: { target: string }[] }).PLATFORMS;
|
||||
const targets = platforms.map((p) => p.target);
|
||||
|
||||
// when
|
||||
const hasAllBaselineTargets = EXPECTED_BASELINE_TARGETS.every((baseline) =>
|
||||
targets.includes(baseline)
|
||||
);
|
||||
|
||||
// then
|
||||
expect(hasAllBaselineTargets).toBe(true);
|
||||
for (const baseline of EXPECTED_BASELINE_TARGETS) {
|
||||
expect(targets).toContain(baseline);
|
||||
}
|
||||
});
|
||||
|
||||
it("has correct directory names for baseline platforms", async () => {
|
||||
// given
|
||||
const module = await import("./build-binaries.ts");
|
||||
const platforms = (module as { PLATFORMS: { dir: string; target: string }[] }).PLATFORMS;
|
||||
|
||||
// when
|
||||
const baselinePlatforms = platforms.filter((p) => p.target.includes("baseline"));
|
||||
|
||||
// then
|
||||
expect(baselinePlatforms.length).toBe(4);
|
||||
expect(baselinePlatforms.map((p) => p.dir)).toContain("linux-x64-baseline");
|
||||
expect(baselinePlatforms.map((p) => p.dir)).toContain("linux-x64-musl-baseline");
|
||||
expect(baselinePlatforms.map((p) => p.dir)).toContain("darwin-x64-baseline");
|
||||
expect(baselinePlatforms.map((p) => p.dir)).toContain("windows-x64-baseline");
|
||||
});
|
||||
|
||||
it("has correct binary names for baseline platforms", async () => {
|
||||
// given
|
||||
const module = await import("./build-binaries.ts");
|
||||
const platforms = (module as { PLATFORMS: { dir: string; target: string; binary: string }[] }).PLATFORMS;
|
||||
|
||||
// when
|
||||
const windowsBaseline = platforms.find((p) => p.target === "bun-windows-x64-baseline");
|
||||
const linuxBaseline = platforms.find((p) => p.target === "bun-linux-x64-baseline");
|
||||
|
||||
// then
|
||||
expect(windowsBaseline?.binary).toBe("oh-my-opencode.exe");
|
||||
expect(linuxBaseline?.binary).toBe("oh-my-opencode");
|
||||
});
|
||||
|
||||
it("has descriptions mentioning no AVX2 for baseline platforms", async () => {
|
||||
// given
|
||||
const module = await import("./build-binaries.ts");
|
||||
const platforms = (module as { PLATFORMS: { target: string; description: string }[] }).PLATFORMS;
|
||||
|
||||
// when
|
||||
const baselinePlatforms = platforms.filter((p) => p.target.includes("baseline"));
|
||||
|
||||
// then
|
||||
for (const platform of baselinePlatforms) {
|
||||
expect(platform.description).toContain("no AVX2");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,14 +13,18 @@ interface PlatformTarget {
|
||||
description: string;
|
||||
}
|
||||
|
||||
const PLATFORMS: PlatformTarget[] = [
|
||||
export 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: "darwin-x64-baseline", target: "bun-darwin-x64-baseline", binary: "oh-my-opencode", description: "macOS x64 (no AVX2)" },
|
||||
{ dir: "linux-x64", target: "bun-linux-x64", binary: "oh-my-opencode", description: "Linux x64 (glibc)" },
|
||||
{ dir: "linux-x64-baseline", target: "bun-linux-x64-baseline", binary: "oh-my-opencode", description: "Linux x64 (glibc, no AVX2)" },
|
||||
{ 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-x64-musl-baseline", target: "bun-linux-x64-musl-baseline", binary: "oh-my-opencode", description: "Linux x64 (musl, no AVX2)" },
|
||||
{ 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" },
|
||||
{ dir: "windows-x64-baseline", target: "bun-windows-x64-baseline", binary: "oh-my-opencode.exe", description: "Windows x64 (no AVX2)" },
|
||||
];
|
||||
|
||||
const ENTRY_POINT = "src/cli/index.ts";
|
||||
|
||||
@@ -735,6 +735,238 @@
|
||||
"created_at": "2026-01-23T08:27:02Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 985
|
||||
},
|
||||
{
|
||||
"name": "RouHim",
|
||||
"id": 3582050,
|
||||
"comment_id": 3791988227,
|
||||
"created_at": "2026-01-23T19:32:01Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1031
|
||||
},
|
||||
{
|
||||
"name": "gongxh0901",
|
||||
"id": 15622561,
|
||||
"comment_id": 3793478620,
|
||||
"created_at": "2026-01-24T02:15:02Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1037
|
||||
},
|
||||
{
|
||||
"name": "gongxh0901",
|
||||
"id": 15622561,
|
||||
"comment_id": 3793521632,
|
||||
"created_at": "2026-01-24T02:23:34Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1037
|
||||
},
|
||||
{
|
||||
"name": "AndersHsueh",
|
||||
"id": 121805544,
|
||||
"comment_id": 3793787614,
|
||||
"created_at": "2026-01-24T04:41:46Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1042
|
||||
},
|
||||
{
|
||||
"name": "AamiRobin",
|
||||
"id": 22963668,
|
||||
"comment_id": 3794632200,
|
||||
"created_at": "2026-01-24T13:28:22Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1067
|
||||
},
|
||||
{
|
||||
"name": "ThanhNguyxn",
|
||||
"id": 74597207,
|
||||
"comment_id": 3795232176,
|
||||
"created_at": "2026-01-24T17:41:53Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1075
|
||||
},
|
||||
{
|
||||
"name": "sadnow",
|
||||
"id": 87896100,
|
||||
"comment_id": 3795495342,
|
||||
"created_at": "2026-01-24T20:49:29Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1080
|
||||
},
|
||||
{
|
||||
"name": "jsl9208",
|
||||
"id": 4048787,
|
||||
"comment_id": 3795582626,
|
||||
"created_at": "2026-01-24T21:41:24Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1082
|
||||
},
|
||||
{
|
||||
"name": "potb",
|
||||
"id": 10779093,
|
||||
"comment_id": 3795856573,
|
||||
"created_at": "2026-01-25T02:38:16Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1083
|
||||
},
|
||||
{
|
||||
"name": "kvokka",
|
||||
"id": 15954013,
|
||||
"comment_id": 3795884358,
|
||||
"created_at": "2026-01-25T03:13:52Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1084
|
||||
},
|
||||
{
|
||||
"name": "misyuari",
|
||||
"id": 12197761,
|
||||
"comment_id": 3798225767,
|
||||
"created_at": "2026-01-26T07:31:02Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1132
|
||||
},
|
||||
{
|
||||
"name": "boguan",
|
||||
"id": 3226538,
|
||||
"comment_id": 3798448537,
|
||||
"created_at": "2026-01-26T08:40:37Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1137
|
||||
},
|
||||
{
|
||||
"name": "boguan",
|
||||
"id": 3226538,
|
||||
"comment_id": 3798471978,
|
||||
"created_at": "2026-01-26T08:46:03Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1137
|
||||
},
|
||||
{
|
||||
"name": "Jeremy-Kr",
|
||||
"id": 110771206,
|
||||
"comment_id": 3799211732,
|
||||
"created_at": "2026-01-26T11:59:13Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1141
|
||||
},
|
||||
{
|
||||
"name": "orientpine",
|
||||
"id": 32758428,
|
||||
"comment_id": 3799897021,
|
||||
"created_at": "2026-01-26T14:30:33Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1145
|
||||
},
|
||||
{
|
||||
"name": "craftaholic",
|
||||
"id": 63741110,
|
||||
"comment_id": 3797014417,
|
||||
"created_at": "2026-01-25T17:52:34Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1110
|
||||
},
|
||||
{
|
||||
"name": "acamq",
|
||||
"id": 179265037,
|
||||
"comment_id": 3801038978,
|
||||
"created_at": "2026-01-26T18:20:17Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1151
|
||||
},
|
||||
{
|
||||
"name": "itsmylife44",
|
||||
"id": 34112129,
|
||||
"comment_id": 3802225779,
|
||||
"created_at": "2026-01-26T23:20:30Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1157
|
||||
},
|
||||
{
|
||||
"name": "ghtndl",
|
||||
"id": 117787238,
|
||||
"comment_id": 3802593326,
|
||||
"created_at": "2026-01-27T01:27:17Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1158
|
||||
},
|
||||
{
|
||||
"name": "alvinunreal",
|
||||
"id": 204474669,
|
||||
"comment_id": 3796402213,
|
||||
"created_at": "2026-01-25T10:26:58Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1100
|
||||
},
|
||||
{
|
||||
"name": "MoerAI",
|
||||
"id": 26067127,
|
||||
"comment_id": 3803968993,
|
||||
"created_at": "2026-01-27T09:00:57Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1172
|
||||
},
|
||||
{
|
||||
"name": "moha-abdi",
|
||||
"id": 83307623,
|
||||
"comment_id": 3804988070,
|
||||
"created_at": "2026-01-27T12:36:21Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1179
|
||||
},
|
||||
{
|
||||
"name": "zycaskevin",
|
||||
"id": 223135116,
|
||||
"comment_id": 3806137669,
|
||||
"created_at": "2026-01-27T16:20:38Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1184
|
||||
},
|
||||
{
|
||||
"name": "agno01",
|
||||
"id": 4479380,
|
||||
"comment_id": 3808373433,
|
||||
"created_at": "2026-01-28T01:02:02Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1188
|
||||
},
|
||||
{
|
||||
"name": "rooftop-Owl",
|
||||
"id": 254422872,
|
||||
"comment_id": 3809867225,
|
||||
"created_at": "2026-01-28T08:46:58Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1197
|
||||
},
|
||||
{
|
||||
"name": "youming-ai",
|
||||
"id": 173424537,
|
||||
"comment_id": 3811195276,
|
||||
"created_at": "2026-01-28T13:04:16Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1203
|
||||
},
|
||||
{
|
||||
"name": "KennyDizi",
|
||||
"id": 16578966,
|
||||
"comment_id": 3811619818,
|
||||
"created_at": "2026-01-28T14:26:10Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1214
|
||||
},
|
||||
{
|
||||
"name": "mrdavidlaing",
|
||||
"id": 227505,
|
||||
"comment_id": 3813542625,
|
||||
"created_at": "2026-01-28T19:51:34Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1226
|
||||
},
|
||||
{
|
||||
"name": "Lynricsy",
|
||||
"id": 62173814,
|
||||
"comment_id": 3816370548,
|
||||
"created_at": "2026-01-29T09:00:28Z",
|
||||
"repoId": 1108837393,
|
||||
"pullRequestNo": 1241
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -239,7 +239,7 @@ Ask yourself:
|
||||
I will use delegate_task with:
|
||||
- **Category**: [selected-category-name]
|
||||
- **Why this category**: [how category description matches task domain]
|
||||
- **Skills**: [list of selected skills]
|
||||
- **load_skills**: [list of selected skills]
|
||||
- **Skill evaluation**:
|
||||
- [skill-1]: INCLUDED because [reason based on skill description]
|
||||
- [skill-2]: OMITTED because [reason why skill domain doesn't apply]
|
||||
@@ -256,7 +256,7 @@ I will use delegate_task with:
|
||||
I will use delegate_task with:
|
||||
- **Category**: [category-name]
|
||||
- **Why this category**: Category description says "[quote description]" which matches this task's requirements
|
||||
- **Skills**: ["skill-a", "skill-b"]
|
||||
- **load_skills**: ["skill-a", "skill-b"]
|
||||
- **Skill evaluation**:
|
||||
- skill-a: INCLUDED - description says "[quote]" which applies to this task
|
||||
- skill-b: INCLUDED - description says "[quote]" which is needed here
|
||||
@@ -265,7 +265,7 @@ I will use delegate_task with:
|
||||
|
||||
delegate_task(
|
||||
category="[category-name]",
|
||||
skills=["skill-a", "skill-b"],
|
||||
load_skills=["skill-a", "skill-b"],
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
@@ -276,12 +276,12 @@ delegate_task(
|
||||
I will use delegate_task with:
|
||||
- **Agent**: [agent-name]
|
||||
- **Reason**: This requires [agent's specialty] based on agent description
|
||||
- **Skills**: [] (agents have built-in expertise)
|
||||
- **load_skills**: [] (agents have built-in expertise)
|
||||
- **Expected Outcome**: [what agent should return]
|
||||
|
||||
delegate_task(
|
||||
subagent_type="[agent-name]",
|
||||
skills=[],
|
||||
load_skills=[],
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
@@ -292,13 +292,13 @@ delegate_task(
|
||||
I will use delegate_task with:
|
||||
- **Agent**: explore
|
||||
- **Reason**: Need to find all authentication implementations across the codebase - this is contextual grep
|
||||
- **Skills**: []
|
||||
- **load_skills**: []
|
||||
- **Expected Outcome**: List of files containing auth patterns
|
||||
|
||||
delegate_task(
|
||||
subagent_type="explore",
|
||||
run_in_background=true,
|
||||
skills=[],
|
||||
load_skills=[],
|
||||
prompt="Find all authentication implementations in the codebase"
|
||||
)
|
||||
```
|
||||
@@ -306,7 +306,7 @@ delegate_task(
|
||||
**WRONG: No Skill Evaluation**
|
||||
|
||||
```
|
||||
delegate_task(category="...", skills=[], prompt="...") // Where's the justification?
|
||||
delegate_task(category="...", load_skills=[], prompt="...") // Where's the justification?
|
||||
```
|
||||
|
||||
**WRONG: Vague Category Selection**
|
||||
@@ -329,11 +329,11 @@ I'll use this category because it seems right.
|
||||
```typescript
|
||||
// CORRECT: Always background, always parallel
|
||||
// Contextual Grep (internal)
|
||||
delegate_task(subagent_type="explore", run_in_background=true, skills=[], prompt="Find auth implementations in our codebase...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, skills=[], prompt="Find error handling patterns here...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find auth implementations in our codebase...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find error handling patterns here...")
|
||||
// Reference Grep (external)
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, skills=[], prompt="Find JWT best practices in official docs...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, skills=[], prompt="Find how production apps handle auth in Express...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find JWT best practices in official docs...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find how production apps handle auth in Express...")
|
||||
// Continue working immediately. Collect with background_output when needed.
|
||||
|
||||
// WRONG: Sequential or blocking
|
||||
@@ -416,7 +416,7 @@ Skills inject specialized instructions into the subagent. Read the description t
|
||||
For EVERY skill listed above, ask yourself:
|
||||
> "Does this skill's expertise domain overlap with my task?"
|
||||
|
||||
- If YES → INCLUDE in `skills=[...]`
|
||||
- If YES → INCLUDE in `load_skills=[...]`
|
||||
- If NO → You MUST justify why (see below)
|
||||
|
||||
**STEP 3: Justify Omissions**
|
||||
@@ -444,14 +444,14 @@ SKILL EVALUATION for "[skill-name]":
|
||||
```typescript
|
||||
delegate_task(
|
||||
category="[selected-category]",
|
||||
skills=["skill-1", "skill-2"], // Include ALL relevant skills
|
||||
load_skills=["skill-1", "skill-2"], // Include ALL relevant skills
|
||||
prompt="..."
|
||||
)
|
||||
```
|
||||
|
||||
**ANTI-PATTERN (will produce poor results):**
|
||||
```typescript
|
||||
delegate_task(category="...", skills=[], prompt="...") // Empty skills without justification
|
||||
delegate_task(category="...", load_skills=[], prompt="...") // Empty load_skills without justification
|
||||
```
|
||||
### Delegation Table:
|
||||
|
||||
@@ -724,7 +724,7 @@ If the user's approach seems problematic:
|
||||
| **Error Handling** | Empty catch blocks `catch(e) {}` |
|
||||
| **Testing** | Deleting failing tests to "pass" |
|
||||
| **Search** | Firing agents for single-line typos or obvious syntax errors |
|
||||
| **Delegation** | Using `skills=[]` without justifying why no skills apply |
|
||||
| **Delegation** | Using `load_skills=[]` without justifying why no skills apply |
|
||||
| **Debugging** | Shotgun debugging, random changes |
|
||||
## Soft Guidelines
|
||||
|
||||
|
||||
@@ -1,53 +1,48 @@
|
||||
# AGENTS KNOWLEDGE BASE
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
10 AI agents for multi-model orchestration. Sisyphus (primary), Atlas (orchestrator), oracle, librarian, explore, multimodal-looker, Prometheus, Metis, Momus, Sisyphus-Junior.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
agents/
|
||||
├── atlas.ts # Master Orchestrator (543 lines)
|
||||
├── sisyphus.ts # Main prompt (615 lines)
|
||||
├── sisyphus-junior.ts # Delegated task executor
|
||||
├── dynamic-agent-prompt-builder.ts # Dynamic prompt generation
|
||||
├── atlas.ts # Master Orchestrator (holds todo list)
|
||||
├── sisyphus.ts # Main prompt (SF Bay Area engineer identity)
|
||||
├── sisyphus-junior.ts # Delegated task executor (category-spawned)
|
||||
├── oracle.ts # Strategic advisor (GPT-5.2)
|
||||
├── librarian.ts # Multi-repo research (GLM-4.7-free)
|
||||
├── explore.ts # Fast grep (Grok Code)
|
||||
├── librarian.ts # Multi-repo research (GitHub CLI, Context7)
|
||||
├── explore.ts # Fast contextual grep (Grok Code)
|
||||
├── multimodal-looker.ts # Media analyzer (Gemini 3 Flash)
|
||||
├── prometheus-prompt.ts # Planning (1196 lines)
|
||||
├── metis.ts # Plan consultant
|
||||
├── momus.ts # Plan reviewer
|
||||
├── prometheus-prompt.ts # Planning (Interview/Consultant mode, 1196 lines)
|
||||
├── metis.ts # Pre-planning analysis (Gap detection)
|
||||
├── momus.ts # Plan reviewer (Ruthless fault-finding)
|
||||
├── dynamic-agent-prompt-builder.ts # Dynamic prompt generation
|
||||
├── types.ts # AgentModelConfig, AgentPromptMetadata
|
||||
├── utils.ts # createBuiltinAgents(), resolveModelWithFallback()
|
||||
└── index.ts # builtinAgents export
|
||||
```
|
||||
|
||||
## AGENT MODELS
|
||||
|
||||
| Agent | Model | Temp | Purpose |
|
||||
|-------|-------|------|---------|
|
||||
| Sisyphus | anthropic/claude-opus-4-5 | 0.1 | Primary orchestrator |
|
||||
| Atlas | anthropic/claude-opus-4-5 | 0.1 | Master orchestrator |
|
||||
| oracle | openai/gpt-5.2 | 0.1 | Consultation, debugging |
|
||||
| librarian | opencode/glm-4.7-free | 0.1 | Docs, GitHub search |
|
||||
| explore | opencode/grok-code | 0.1 | Fast contextual grep |
|
||||
| multimodal-looker | google/gemini-3-flash-preview | 0.1 | PDF/image analysis |
|
||||
| librarian | opencode/big-pickle | 0.1 | Docs, GitHub search |
|
||||
| explore | opencode/gpt-5-nano | 0.1 | Fast contextual grep |
|
||||
| multimodal-looker | google/gemini-3-flash | 0.1 | PDF/image analysis |
|
||||
| Prometheus | anthropic/claude-opus-4-5 | 0.1 | Strategic planning |
|
||||
| Metis | anthropic/claude-sonnet-4-5 | 0.3 | Pre-planning analysis |
|
||||
| Momus | anthropic/claude-sonnet-4-5 | 0.1 | Plan validation |
|
||||
| Sisyphus-Junior | anthropic/claude-sonnet-4-5 | 0.1 | Category-spawned executor |
|
||||
|
||||
## HOW TO ADD
|
||||
|
||||
1. Create `src/agents/my-agent.ts` exporting factory + metadata
|
||||
2. Add to `agentSources` in `src/agents/utils.ts`
|
||||
3. Update `AgentNameSchema` in `src/config/schema.ts`
|
||||
4. Register in `src/index.ts` initialization
|
||||
1. Create `src/agents/my-agent.ts` exporting factory + metadata.
|
||||
2. Add to `agentSources` in `src/agents/utils.ts`.
|
||||
3. Update `AgentNameSchema` in `src/config/schema.ts`.
|
||||
4. Register in `src/index.ts` initialization.
|
||||
|
||||
## TOOL RESTRICTIONS
|
||||
|
||||
| Agent | Denied Tools |
|
||||
|-------|-------------|
|
||||
| oracle | write, edit, task, delegate_task |
|
||||
@@ -57,14 +52,13 @@ agents/
|
||||
| Sisyphus-Junior | task, delegate_task |
|
||||
|
||||
## PATTERNS
|
||||
|
||||
- **Factory**: `createXXXAgent(model?: string): AgentConfig`
|
||||
- **Metadata**: `XXX_PROMPT_METADATA` with category, cost, triggers
|
||||
- **Tool restrictions**: `createAgentToolRestrictions(tools)` or `createAgentToolAllowlist(tools)`
|
||||
- **Thinking**: 32k budget tokens for Sisyphus, Oracle, Prometheus, Atlas
|
||||
- **Factory**: `createXXXAgent(model: string): AgentConfig`
|
||||
- **Metadata**: `XXX_PROMPT_METADATA` with category, cost, triggers.
|
||||
- **Tool restrictions**: `createAgentToolRestrictions(tools)` or `createAgentToolAllowlist(tools)`.
|
||||
- **Thinking**: 32k budget tokens for Sisyphus, Oracle, Prometheus, Atlas.
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
- **Trust reports**: NEVER trust "I'm done" - verify outputs
|
||||
- **High temp**: Don't use >0.3 for code agents
|
||||
- **Sequential calls**: Use `delegate_task` with `run_in_background`
|
||||
- **Trust reports**: NEVER trust "I'm done" - verify outputs.
|
||||
- **High temp**: Don't use >0.3 for code agents.
|
||||
- **Sequential calls**: Use `delegate_task` with `run_in_background` for exploration.
|
||||
- **Prometheus writing code**: Planner only - never implements.
|
||||
|
||||
@@ -58,7 +58,7 @@ Categories spawn \`Sisyphus-Junior-{category}\` with optimized settings:
|
||||
${categoryRows.join("\n")}
|
||||
|
||||
\`\`\`typescript
|
||||
delegate_task(category="[category-name]", skills=[...], prompt="...")
|
||||
delegate_task(category="[category-name]", load_skills=[...], prompt="...")
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
@@ -84,12 +84,12 @@ ${skillRows.join("\n")}
|
||||
**MANDATORY: Evaluate ALL skills for relevance to your task.**
|
||||
|
||||
Read each skill's description and ask: "Does this skill's domain overlap with my task?"
|
||||
- If YES: INCLUDE in skills=[...]
|
||||
- If YES: INCLUDE in load_skills=[...]
|
||||
- If NO: You MUST justify why in your pre-delegation declaration
|
||||
|
||||
**Usage:**
|
||||
\`\`\`typescript
|
||||
delegate_task(category="[category]", skills=["skill-1", "skill-2"], prompt="...")
|
||||
delegate_task(category="[category]", load_skills=["skill-1", "skill-2"], prompt="...")
|
||||
\`\`\`
|
||||
|
||||
**IMPORTANT:**
|
||||
@@ -102,7 +102,7 @@ function buildDecisionMatrix(agents: AvailableAgent[], userCategories?: Record<s
|
||||
const allCategories = { ...DEFAULT_CATEGORIES, ...userCategories }
|
||||
|
||||
const categoryRows = Object.entries(allCategories).map(([name]) =>
|
||||
`| ${getCategoryDescription(name, userCategories)} | \`category="${name}", skills=[...]\` |`
|
||||
`| ${getCategoryDescription(name, userCategories)} | \`category="${name}", load_skills=[...]\` |`
|
||||
)
|
||||
|
||||
const agentRows = agents.map((a) => {
|
||||
@@ -323,7 +323,7 @@ delegate_task(
|
||||
**If verification fails**: Resume the SAME session with the ACTUAL error output:
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
resume="ses_xyz789", // ALWAYS use the session from the failed task
|
||||
session_id="ses_xyz789", // ALWAYS use the session from the failed task
|
||||
load_skills=[...],
|
||||
prompt="Verification failed: {actual error}. Fix."
|
||||
)
|
||||
@@ -331,24 +331,24 @@ delegate_task(
|
||||
|
||||
### 3.5 Handle Failures (USE RESUME)
|
||||
|
||||
**CRITICAL: When re-delegating, ALWAYS use \`resume\` parameter.**
|
||||
**CRITICAL: When re-delegating, ALWAYS use \`session_id\` parameter.**
|
||||
|
||||
Every \`delegate_task()\` output includes a session_id. STORE IT.
|
||||
|
||||
If task fails:
|
||||
1. Identify what went wrong
|
||||
2. **Resume the SAME session** - subagent has full context already:
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
resume="ses_xyz789", // Session from failed task
|
||||
load_skills=[...],
|
||||
prompt="FAILED: {error}. Fix by: {specific instruction}"
|
||||
)
|
||||
\`\`\`
|
||||
\`\`\`typescript
|
||||
delegate_task(
|
||||
session_id="ses_xyz789", // Session from failed task
|
||||
load_skills=[...],
|
||||
prompt="FAILED: {error}. Fix by: {specific instruction}"
|
||||
)
|
||||
\`\`\`
|
||||
3. Maximum 3 retry attempts with the SAME session
|
||||
4. If blocked after 3 attempts: Document and continue to independent tasks
|
||||
|
||||
**Why resume is MANDATORY for failures:**
|
||||
**Why session_id is MANDATORY for failures:**
|
||||
- Subagent already read all files, knows the context
|
||||
- No repeated exploration = 70%+ token savings
|
||||
- Subagent knows what approaches already failed
|
||||
@@ -493,7 +493,7 @@ You are the QA gate. Subagents lie. Verify EVERYTHING.
|
||||
- Parallelize independent tasks
|
||||
- Verify with your own tools
|
||||
- **Store session_id from every delegation output**
|
||||
- **Use \`resume="{session_id}"\` for retries, fixes, and follow-ups**
|
||||
- **Use \`session_id="{session_id}"\` for retries, fixes, and follow-ups**
|
||||
</critical_overrides>
|
||||
`
|
||||
|
||||
@@ -523,18 +523,15 @@ function buildDynamicOrchestratorPrompt(ctx?: OrchestratorContext): string {
|
||||
}
|
||||
|
||||
export function createAtlasAgent(ctx: OrchestratorContext): AgentConfig {
|
||||
if (!ctx.model) {
|
||||
throw new Error("createAtlasAgent requires a model in context")
|
||||
}
|
||||
const restrictions = createAgentToolRestrictions([
|
||||
"task",
|
||||
"call_omo_agent",
|
||||
])
|
||||
return {
|
||||
description:
|
||||
"Orchestrates work via delegate_task() to complete ALL tasks in a todo list until fully done",
|
||||
"Orchestrates work via delegate_task() to complete ALL tasks in a todo list until fully done. (Atlas - OhMyOpenCode)",
|
||||
mode: "primary" as const,
|
||||
model: ctx.model,
|
||||
...(ctx.model ? { model: ctx.model } : {}),
|
||||
temperature: 0.1,
|
||||
prompt: buildDynamicOrchestratorPrompt(ctx),
|
||||
thinking: { type: "enabled", budgetTokens: 32000 },
|
||||
|
||||
@@ -33,7 +33,7 @@ export function createExploreAgent(model: string): AgentConfig {
|
||||
|
||||
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.',
|
||||
'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. (Explore - OhMyOpenCode)',
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
|
||||
@@ -30,7 +30,7 @@ export function createLibrarianAgent(model: string): AgentConfig {
|
||||
|
||||
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.",
|
||||
"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. (Librarian - OhMyOpenCode)",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
|
||||
@@ -230,6 +230,8 @@ call_omo_agent(subagent_type="librarian", prompt="Find OSS implementations of Z.
|
||||
- [Risk 2]: [Mitigation]
|
||||
|
||||
## Directives for Prometheus
|
||||
|
||||
### Core Directives
|
||||
- MUST: [Required action]
|
||||
- MUST: [Required action]
|
||||
- MUST NOT: [Forbidden action]
|
||||
@@ -237,6 +239,29 @@ call_omo_agent(subagent_type="librarian", prompt="Find OSS implementations of Z.
|
||||
- PATTERN: Follow \`[file:lines]\`
|
||||
- TOOL: Use \`[specific tool]\` for [purpose]
|
||||
|
||||
### QA/Acceptance Criteria Directives (MANDATORY)
|
||||
> **ZERO USER INTERVENTION PRINCIPLE**: All acceptance criteria MUST be executable by agents.
|
||||
|
||||
- MUST: Write acceptance criteria as executable commands (curl, bun test, playwright actions)
|
||||
- MUST: Include exact expected outputs, not vague descriptions
|
||||
- MUST: Specify verification tool for each deliverable type (playwright for UI, curl for API, etc.)
|
||||
- MUST NOT: Create criteria requiring "user manually tests..."
|
||||
- MUST NOT: Create criteria requiring "user visually confirms..."
|
||||
- MUST NOT: Create criteria requiring "user clicks/interacts..."
|
||||
- MUST NOT: Use placeholders without concrete examples (bad: "[endpoint]", good: "/api/users")
|
||||
|
||||
Example of GOOD acceptance criteria:
|
||||
\`\`\`
|
||||
curl -s http://localhost:3000/api/health | jq '.status'
|
||||
# Assert: Output is "ok"
|
||||
\`\`\`
|
||||
|
||||
Example of BAD acceptance criteria (FORBIDDEN):
|
||||
\`\`\`
|
||||
User opens browser and checks if the page loads correctly.
|
||||
User confirms the button works as expected.
|
||||
\`\`\`
|
||||
|
||||
## Recommended Approach
|
||||
[1-2 sentence summary of how to proceed]
|
||||
\`\`\`
|
||||
@@ -263,12 +288,16 @@ call_omo_agent(subagent_type="librarian", prompt="Find OSS implementations of Z.
|
||||
- Ask generic questions ("What's the scope?")
|
||||
- Proceed without addressing ambiguity
|
||||
- Make assumptions about user's codebase
|
||||
- Suggest acceptance criteria requiring user intervention ("user manually tests", "user confirms", "user clicks")
|
||||
- Leave QA/acceptance criteria vague or placeholder-heavy
|
||||
|
||||
**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
|
||||
- Include QA automation directives in every output
|
||||
- Ensure acceptance criteria are agent-executable (commands, not human actions)
|
||||
`
|
||||
|
||||
const metisRestrictions = createAgentToolRestrictions([
|
||||
@@ -281,7 +310,7 @@ const metisRestrictions = createAgentToolRestrictions([
|
||||
export function createMetisAgent(model: string): AgentConfig {
|
||||
return {
|
||||
description:
|
||||
"Pre-planning consultant that analyzes requests to identify hidden intentions, ambiguities, and AI failure points.",
|
||||
"Pre-planning consultant that analyzes requests to identify hidden intentions, ambiguities, and AI failure points. (Metis - OhMyOpenCode)",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.3,
|
||||
|
||||
@@ -399,7 +399,7 @@ export function createMomusAgent(model: string): AgentConfig {
|
||||
|
||||
const base = {
|
||||
description:
|
||||
"Expert reviewer for evaluating work plans against rigorous clarity, verifiability, and completeness standards.",
|
||||
"Expert reviewer for evaluating work plans against rigorous clarity, verifiability, and completeness standards. (Momus - OhMyOpenCode)",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
|
||||
@@ -14,7 +14,7 @@ export function createMultimodalLookerAgent(model: string): AgentConfig {
|
||||
|
||||
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.",
|
||||
"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. (Multimodal-Looker - OhMyOpenCode)",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
|
||||
@@ -105,7 +105,7 @@ export function createOracleAgent(model: string): AgentConfig {
|
||||
|
||||
const base = {
|
||||
description:
|
||||
"Read-only consultation agent. High-IQ reasoning specialist for debugging hard problems and high-difficulty architecture design.",
|
||||
"Read-only consultation agent. High-IQ reasoning specialist for debugging hard problems and high-difficulty architecture design. (Oracle - OhMyOpenCode)",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
|
||||
@@ -863,6 +863,20 @@ Generate plan to: \`.sisyphus/plans/{name}.md\`
|
||||
\`\`\`markdown
|
||||
# {Plan Title}
|
||||
|
||||
## TL;DR
|
||||
|
||||
> **Quick Summary**: [1-2 sentences capturing the core objective and approach]
|
||||
>
|
||||
> **Deliverables**: [Bullet list of concrete outputs]
|
||||
> - [Output 1]
|
||||
> - [Output 2]
|
||||
>
|
||||
> **Estimated Effort**: [Quick | Short | Medium | Large | XL]
|
||||
> **Parallel Execution**: [YES - N waves | NO - sequential]
|
||||
> **Critical Path**: [Task X → Task Y → Task Z]
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
### Original Request
|
||||
@@ -939,53 +953,89 @@ Each TODO follows RED-GREEN-REFACTOR:
|
||||
- Example: Create \`src/__tests__/example.test.ts\`
|
||||
- Verify: \`bun test\` → 1 test passes
|
||||
|
||||
### If Manual QA Only
|
||||
### If Automated Verification Only (NO User Intervention)
|
||||
|
||||
**CRITICAL**: Without automated tests, manual verification MUST be exhaustive.
|
||||
> **CRITICAL PRINCIPLE: ZERO USER INTERVENTION**
|
||||
>
|
||||
> **NEVER** create acceptance criteria that require:
|
||||
> - "User manually tests..." / "사용자가 직접 테스트..."
|
||||
> - "User visually confirms..." / "사용자가 눈으로 확인..."
|
||||
> - "User interacts with..." / "사용자가 직접 조작..."
|
||||
> - "Ask user to verify..." / "사용자에게 확인 요청..."
|
||||
> - ANY step that requires a human to perform an action
|
||||
>
|
||||
> **ALL verification MUST be automated and executable by the agent.**
|
||||
> If a verification cannot be automated, find an automated alternative or explicitly note it as a known limitation.
|
||||
|
||||
Each TODO includes detailed verification procedures:
|
||||
Each TODO includes EXECUTABLE verification procedures that agents can run directly:
|
||||
|
||||
**By Deliverable Type:**
|
||||
|
||||
| Type | Verification Tool | Procedure |
|
||||
|------|------------------|-----------|
|
||||
| **Frontend/UI** | Playwright browser | Navigate, interact, screenshot |
|
||||
| **TUI/CLI** | interactive_bash (tmux) | Run command, verify output |
|
||||
| **API/Backend** | curl / httpie | Send request, verify response |
|
||||
| **Library/Module** | Node/Python REPL | Import, call, verify |
|
||||
| **Config/Infra** | Shell commands | Apply, verify state |
|
||||
| Type | Verification Tool | Automated Procedure |
|
||||
|------|------------------|---------------------|
|
||||
| **Frontend/UI** | Playwright browser via playwright skill | Agent navigates, clicks, screenshots, asserts DOM state |
|
||||
| **TUI/CLI** | interactive_bash (tmux) | Agent runs command, captures output, validates expected strings |
|
||||
| **API/Backend** | curl / httpie via Bash | Agent sends request, parses response, validates JSON fields |
|
||||
| **Library/Module** | Node/Python REPL via Bash | Agent imports, calls function, compares output |
|
||||
| **Config/Infra** | Shell commands via Bash | Agent applies config, runs state check, validates output |
|
||||
|
||||
**Evidence Required:**
|
||||
- Commands run with actual output
|
||||
- Screenshots for visual changes
|
||||
- Response bodies for API changes
|
||||
- Terminal output for CLI changes
|
||||
**Evidence Requirements (Agent-Executable):**
|
||||
- Command output captured and compared against expected patterns
|
||||
- Screenshots saved to .sisyphus/evidence/ for visual verification
|
||||
- JSON response fields validated with specific assertions
|
||||
- Exit codes checked (0 = success)
|
||||
|
||||
---
|
||||
|
||||
## Task Flow
|
||||
## Execution Strategy
|
||||
|
||||
### Parallel Execution Waves
|
||||
|
||||
> Maximize throughput by grouping independent tasks into parallel waves.
|
||||
> Each wave completes before the next begins.
|
||||
|
||||
\`\`\`
|
||||
Task 1 → Task 2 → Task 3
|
||||
↘ Task 4 (parallel)
|
||||
Wave 1 (Start Immediately):
|
||||
├── Task 1: [no dependencies]
|
||||
└── Task 5: [no dependencies]
|
||||
|
||||
Wave 2 (After Wave 1):
|
||||
├── Task 2: [depends: 1]
|
||||
├── Task 3: [depends: 1]
|
||||
└── Task 6: [depends: 5]
|
||||
|
||||
Wave 3 (After Wave 2):
|
||||
└── Task 4: [depends: 2, 3]
|
||||
|
||||
Critical Path: Task 1 → Task 2 → Task 4
|
||||
Parallel Speedup: ~40% faster than sequential
|
||||
\`\`\`
|
||||
|
||||
## Parallelization
|
||||
### Dependency Matrix
|
||||
|
||||
| Group | Tasks | Reason |
|
||||
|-------|-------|--------|
|
||||
| A | 2, 3 | Independent files |
|
||||
| Task | Depends On | Blocks | Can Parallelize With |
|
||||
|------|------------|--------|---------------------|
|
||||
| 1 | None | 2, 3 | 5 |
|
||||
| 2 | 1 | 4 | 3, 6 |
|
||||
| 3 | 1 | 4 | 2, 6 |
|
||||
| 4 | 2, 3 | None | None (final) |
|
||||
| 5 | None | 6 | 1 |
|
||||
| 6 | 5 | None | 2, 3 |
|
||||
|
||||
| Task | Depends On | Reason |
|
||||
|------|------------|--------|
|
||||
| 4 | 1 | Requires output from 1 |
|
||||
### Agent Dispatch Summary
|
||||
|
||||
| Wave | Tasks | Recommended Agents |
|
||||
|------|-------|-------------------|
|
||||
| 1 | 1, 5 | delegate_task(category="...", load_skills=[...], run_in_background=true) |
|
||||
| 2 | 2, 3, 6 | dispatch parallel after Wave 1 completes |
|
||||
| 3 | 4 | final integration task |
|
||||
|
||||
---
|
||||
|
||||
## TODOs
|
||||
|
||||
> Implementation + Test = ONE Task. Never separate.
|
||||
> Specify parallelizability for EVERY task.
|
||||
> EVERY task MUST have: Recommended Agent Profile + Parallelization info.
|
||||
|
||||
- [ ] 1. [Task Title]
|
||||
|
||||
@@ -996,7 +1046,21 @@ Task 1 → Task 2 → Task 3
|
||||
**Must NOT do**:
|
||||
- [Specific exclusions from guardrails]
|
||||
|
||||
**Parallelizable**: YES (with 3, 4) | NO (depends on 0)
|
||||
**Recommended Agent Profile**:
|
||||
> Select category + skills based on task domain. Justify each choice.
|
||||
- **Category**: \`[visual-engineering | ultrabrain | artistry | quick | unspecified-low | unspecified-high | writing]\`
|
||||
- Reason: [Why this category fits the task domain]
|
||||
- **Skills**: [\`skill-1\`, \`skill-2\`]
|
||||
- \`skill-1\`: [Why needed - domain overlap explanation]
|
||||
- \`skill-2\`: [Why needed - domain overlap explanation]
|
||||
- **Skills Evaluated but Omitted**:
|
||||
- \`omitted-skill\`: [Why domain doesn't overlap]
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES | NO
|
||||
- **Parallel Group**: Wave N (with Tasks X, Y) | Sequential
|
||||
- **Blocks**: [Tasks that depend on this task completing]
|
||||
- **Blocked By**: [Tasks this depends on] | None (can start immediately)
|
||||
|
||||
**References** (CRITICAL - Be Exhaustive):
|
||||
|
||||
@@ -1029,53 +1093,76 @@ Task 1 → Task 2 → Task 3
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
> CRITICAL: Acceptance = EXECUTION, not just "it should work".
|
||||
> The executor MUST run these commands and verify output.
|
||||
> **CRITICAL: AGENT-EXECUTABLE VERIFICATION ONLY**
|
||||
>
|
||||
> - Acceptance = EXECUTION by the agent, not "user checks if it works"
|
||||
> - Every criterion MUST be verifiable by running a command or using a tool
|
||||
> - NO steps like "user opens browser", "user clicks", "user confirms"
|
||||
> - If you write "[placeholder]" - REPLACE IT with actual values based on task context
|
||||
|
||||
**If TDD (tests enabled):**
|
||||
- [ ] Test file created: \`[path].test.ts\`
|
||||
- [ ] Test covers: [specific scenario]
|
||||
- [ ] \`bun test [file]\` → PASS (N tests, 0 failures)
|
||||
- [ ] Test file created: src/auth/login.test.ts
|
||||
- [ ] Test covers: successful login returns JWT token
|
||||
- [ ] bun test src/auth/login.test.ts → PASS (3 tests, 0 failures)
|
||||
|
||||
**Manual Execution Verification (ALWAYS include, even with tests):**
|
||||
**Automated Verification (ALWAYS include, choose by deliverable type):**
|
||||
|
||||
*Choose based on deliverable type:*
|
||||
**For Frontend/UI changes** (using playwright skill):
|
||||
\\\`\\\`\\\`
|
||||
# Agent executes via playwright browser automation:
|
||||
1. Navigate to: http://localhost:3000/login
|
||||
2. Fill: input[name="email"] with "test@example.com"
|
||||
3. Fill: input[name="password"] with "password123"
|
||||
4. Click: button[type="submit"]
|
||||
5. Wait for: selector ".dashboard-welcome" to be visible
|
||||
6. Assert: text "Welcome back" appears on page
|
||||
7. Screenshot: .sisyphus/evidence/task-1-login-success.png
|
||||
\\\`\\\`\\\`
|
||||
|
||||
**For Frontend/UI changes:**
|
||||
- [ ] Using playwright browser automation:
|
||||
- Navigate to: \`http://localhost:[port]/[path]\`
|
||||
- Action: [click X, fill Y, scroll to Z]
|
||||
- Verify: [visual element appears, animation completes, state changes]
|
||||
- Screenshot: Save evidence to \`.sisyphus/evidence/[task-id]-[step].png\`
|
||||
**For TUI/CLI changes** (using interactive_bash):
|
||||
\\\`\\\`\\\`
|
||||
# Agent executes via tmux session:
|
||||
1. Command: ./my-cli --config test.yaml
|
||||
2. Wait for: "Configuration loaded" in output
|
||||
3. Send keys: "q" to quit
|
||||
4. Assert: Exit code 0
|
||||
5. Assert: Output contains "Goodbye"
|
||||
\\\`\\\`\\\`
|
||||
|
||||
**For TUI/CLI changes:**
|
||||
- [ ] Using interactive_bash (tmux session):
|
||||
- Command: \`[exact command to run]\`
|
||||
- Input sequence: [if interactive, list inputs]
|
||||
- Expected output contains: \`[expected string or pattern]\`
|
||||
- Exit code: [0 for success, specific code if relevant]
|
||||
**For API/Backend changes** (using Bash curl):
|
||||
\\\`\\\`\\\`bash
|
||||
# Agent runs:
|
||||
curl -s -X POST http://localhost:8080/api/users \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"email":"new@test.com","name":"Test User"}' \\
|
||||
| jq '.id'
|
||||
# Assert: Returns non-empty UUID
|
||||
# Assert: HTTP status 201
|
||||
\\\`\\\`\\\`
|
||||
|
||||
**For API/Backend changes:**
|
||||
- [ ] Request: \`curl -X [METHOD] http://localhost:[port]/[endpoint] -H "Content-Type: application/json" -d '[body]'\`
|
||||
- [ ] Response status: [200/201/etc]
|
||||
- [ ] Response body contains: \`{"key": "expected_value"}\`
|
||||
**For Library/Module changes** (using Bash node/bun):
|
||||
\\\`\\\`\\\`bash
|
||||
# Agent runs:
|
||||
bun -e "import { validateEmail } from './src/utils/validate'; console.log(validateEmail('test@example.com'))"
|
||||
# Assert: Output is "true"
|
||||
|
||||
bun -e "import { validateEmail } from './src/utils/validate'; console.log(validateEmail('invalid'))"
|
||||
# Assert: Output is "false"
|
||||
\\\`\\\`\\\`
|
||||
|
||||
**For Library/Module changes:**
|
||||
- [ ] REPL verification:
|
||||
\`\`\`
|
||||
> import { [function] } from '[module]'
|
||||
> [function]([args])
|
||||
Expected: [output]
|
||||
\`\`\`
|
||||
**For Config/Infra changes** (using Bash):
|
||||
\\\`\\\`\\\`bash
|
||||
# Agent runs:
|
||||
docker compose up -d
|
||||
# Wait 5s for containers
|
||||
docker compose ps --format json | jq '.[].State'
|
||||
# Assert: All states are "running"
|
||||
\\\`\\\`\\\`
|
||||
|
||||
**For Config/Infra changes:**
|
||||
- [ ] Apply: \`[command to apply config]\`
|
||||
- [ ] Verify state: \`[command to check state]\` → \`[expected output]\`
|
||||
|
||||
**Evidence Required:**
|
||||
- [ ] Command output captured (copy-paste actual terminal output)
|
||||
- [ ] Screenshot saved (for visual changes)
|
||||
- [ ] Response body logged (for API changes)
|
||||
**Evidence to Capture:**
|
||||
- [ ] Terminal output from verification commands (actual output, not expected)
|
||||
- [ ] Screenshot files in .sisyphus/evidence/ for UI changes
|
||||
- [ ] JSON response bodies for API changes
|
||||
|
||||
**Commit**: YES | NO (groups with N)
|
||||
- Message: \`type(scope): desc\`
|
||||
|
||||
@@ -20,32 +20,6 @@ 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.
|
||||
IMPORTANT: Always APPEND to notepad files - never overwrite or use Edit tool.
|
||||
|
||||
## 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
|
||||
@@ -110,7 +84,7 @@ export function createSisyphusJuniorAgentWithOverrides(
|
||||
|
||||
const base: AgentConfig = {
|
||||
description: override?.description ??
|
||||
"Sisyphus-Junior - Focused task executor. Same discipline, no delegation.",
|
||||
"Focused task executor. Same discipline, no delegation. (Sisyphus-Junior - OhMyOpenCode)",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature,
|
||||
|
||||
@@ -144,11 +144,11 @@ ${librarianSection}
|
||||
\`\`\`typescript
|
||||
// CORRECT: Always background, always parallel
|
||||
// Contextual Grep (internal)
|
||||
delegate_task(subagent_type="explore", run_in_background=true, skills=[], prompt="Find auth implementations in our codebase...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, skills=[], prompt="Find error handling patterns here...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find auth implementations in our codebase...")
|
||||
delegate_task(subagent_type="explore", run_in_background=true, load_skills=[], prompt="Find error handling patterns here...")
|
||||
// Reference Grep (external)
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, skills=[], prompt="Find JWT best practices in official docs...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, skills=[], prompt="Find how production apps handle auth in Express...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find JWT best practices in official docs...")
|
||||
delegate_task(subagent_type="librarian", run_in_background=true, load_skills=[], prompt="Find how production apps handle auth in Express...")
|
||||
// Continue working immediately. Collect with background_output when needed.
|
||||
|
||||
// WRONG: Sequential or blocking
|
||||
@@ -209,15 +209,15 @@ AFTER THE WORK YOU DELEGATED SEEMS DONE, ALWAYS VERIFY THE RESULTS AS FOLLOWING:
|
||||
|
||||
Every \`delegate_task()\` output includes a session_id. **USE IT.**
|
||||
|
||||
**ALWAYS resume when:**
|
||||
**ALWAYS continue when:**
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| Task failed/incomplete | \`resume="{session_id}", prompt="Fix: {specific error}"\` |
|
||||
| Follow-up question on result | \`resume="{session_id}", prompt="Also: {question}"\` |
|
||||
| Multi-turn with same agent | \`resume="{session_id}"\` - NEVER start fresh |
|
||||
| Verification failed | \`resume="{session_id}", prompt="Failed verification: {error}. Fix."\` |
|
||||
| Task failed/incomplete | \`session_id="{session_id}", prompt="Fix: {specific error}"\` |
|
||||
| Follow-up question on result | \`session_id="{session_id}", prompt="Also: {question}"\` |
|
||||
| Multi-turn with same agent | \`session_id="{session_id}"\` - NEVER start fresh |
|
||||
| Verification failed | \`session_id="{session_id}", prompt="Failed verification: {error}. Fix."\` |
|
||||
|
||||
**Why resume is CRITICAL:**
|
||||
**Why session_id is CRITICAL:**
|
||||
- Subagent has FULL conversation context preserved
|
||||
- No repeated file reads, exploration, or setup
|
||||
- Saves 70%+ tokens on follow-ups
|
||||
@@ -228,10 +228,10 @@ Every \`delegate_task()\` output includes a session_id. **USE IT.**
|
||||
delegate_task(category="quick", prompt="Fix the type error in auth.ts...")
|
||||
|
||||
// CORRECT: Resume preserves everything
|
||||
delegate_task(resume="ses_abc123", prompt="Fix: Type error on line 42")
|
||||
delegate_task(session_id="ses_abc123", prompt="Fix: Type error on line 42")
|
||||
\`\`\`
|
||||
|
||||
**After EVERY delegation, STORE the session_id for potential resume.**
|
||||
**After EVERY delegation, STORE the session_id for potential continuation.**
|
||||
|
||||
### Code Changes:
|
||||
- Match existing patterns (if codebase is disciplined)
|
||||
@@ -433,7 +433,7 @@ export function createSisyphusAgent(
|
||||
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 via category+skills combinations. Uses explore for internal code (parallel-friendly), librarian for external docs.",
|
||||
"Powerful AI orchestrator. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically via category+skills combinations. Uses explore for internal code (parallel-friendly), librarian for external docs. (Sisyphus - OhMyOpenCode)",
|
||||
mode: "primary" as const,
|
||||
model,
|
||||
maxTokens: 64000,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { describe, test, expect, beforeEach, spyOn, afterEach } from "bun:test"
|
||||
import { createBuiltinAgents } from "./utils"
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { clearSkillCache } from "../features/opencode-skill-loader/skill-content"
|
||||
import * as connectedProvidersCache from "../shared/connected-providers-cache"
|
||||
|
||||
const TEST_DEFAULT_MODEL = "anthropic/claude-opus-4-5"
|
||||
|
||||
@@ -45,17 +47,31 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
expect(agents.sisyphus.reasoningEffort).toBeUndefined()
|
||||
})
|
||||
|
||||
test("Oracle uses first fallback entry when no availableModels provided (no cache scenario)", async () => {
|
||||
// #given - no available models simulates CI without model cache
|
||||
test("Oracle uses connected provider fallback when availableModels is empty and cache exists", async () => {
|
||||
// #given - connected providers cache has "openai", which matches oracle's first fallback entry
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - uses first fallback entry (openai/gpt-5.2) instead of system default
|
||||
// #then - oracle resolves via connected cache fallback to openai/gpt-5.2 (not system default)
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
expect(agents.oracle.reasoningEffort).toBe("medium")
|
||||
expect(agents.oracle.textVerbosity).toBe("high")
|
||||
expect(agents.oracle.thinking).toBeUndefined()
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("Oracle created without model field when no cache exists (first run scenario)", async () => {
|
||||
// #given - no cache at all (first run)
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - oracle should be created with system default model (fallback to systemDefaultModel)
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.model).toBe(TEST_DEFAULT_MODEL)
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("Oracle with GPT model override has reasoningEffort, no thinking", async () => {
|
||||
@@ -105,10 +121,54 @@ describe("createBuiltinAgents with model overrides", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("createBuiltinAgents without systemDefaultModel", () => {
|
||||
test("agents created via connected cache fallback even without systemDefaultModel", async () => {
|
||||
// #given - connected cache has "openai", which matches oracle's fallback chain
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["openai"])
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, undefined)
|
||||
|
||||
// #then - connected cache enables model resolution despite no systemDefaultModel
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2")
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("agents NOT created when no cache and no systemDefaultModel (first run without defaults)", async () => {
|
||||
// #given
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(null)
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, undefined)
|
||||
|
||||
// #then
|
||||
expect(agents.oracle).toBeUndefined()
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
|
||||
test("sisyphus created via connected cache fallback even without systemDefaultModel", async () => {
|
||||
// #given - connected cache has "anthropic", which matches sisyphus's first fallback entry
|
||||
const cacheSpy = spyOn(connectedProvidersCache, "readConnectedProvidersCache").mockReturnValue(["anthropic"])
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], {}, undefined, undefined)
|
||||
|
||||
// #then - connected cache enables model resolution despite no systemDefaultModel
|
||||
expect(agents.sisyphus).toBeDefined()
|
||||
expect(agents.sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
||||
cacheSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildAgent with category and skills", () => {
|
||||
const { buildAgent } = require("./utils")
|
||||
const TEST_MODEL = "anthropic/claude-opus-4-5"
|
||||
|
||||
beforeEach(() => {
|
||||
clearSkillCache()
|
||||
})
|
||||
|
||||
test("agent with category inherits category settings", () => {
|
||||
// #given - agent factory that sets category but no model
|
||||
const source = {
|
||||
@@ -123,7 +183,7 @@ describe("buildAgent with category and skills", () => {
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then - category's built-in model is applied
|
||||
expect(agent.model).toBe("google/gemini-3-pro-preview")
|
||||
expect(agent.model).toBe("google/gemini-3-pro")
|
||||
})
|
||||
|
||||
test("agent with category and existing model keeps existing model", () => {
|
||||
@@ -308,4 +368,158 @@ describe("buildAgent with category and skills", () => {
|
||||
// #then
|
||||
expect(agent.prompt).toBe("Base prompt")
|
||||
})
|
||||
|
||||
test("agent with agent-browser skill resolves when browserProvider is set", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
skills: ["agent-browser"],
|
||||
prompt: "Base prompt",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when - browserProvider is "agent-browser"
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL, undefined, undefined, "agent-browser")
|
||||
|
||||
// #then - agent-browser skill content should be in prompt
|
||||
expect(agent.prompt).toContain("agent-browser")
|
||||
expect(agent.prompt).toContain("Base prompt")
|
||||
})
|
||||
|
||||
test("agent with agent-browser skill NOT resolved when browserProvider not set", () => {
|
||||
// #given
|
||||
const source = {
|
||||
"test-agent": () =>
|
||||
({
|
||||
description: "Test agent",
|
||||
skills: ["agent-browser"],
|
||||
prompt: "Base prompt",
|
||||
}) as AgentConfig,
|
||||
}
|
||||
|
||||
// #when - no browserProvider (defaults to playwright)
|
||||
const agent = buildAgent(source["test-agent"], TEST_MODEL)
|
||||
|
||||
// #then - agent-browser skill not found, only base prompt remains
|
||||
expect(agent.prompt).toBe("Base prompt")
|
||||
expect(agent.prompt).not.toContain("agent-browser open")
|
||||
})
|
||||
})
|
||||
|
||||
describe("override.category expansion in createBuiltinAgents", () => {
|
||||
test("standard agent override with category expands category properties", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
oracle: { category: "ultrabrain" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - ultrabrain category: model=openai/gpt-5.2-codex, variant=xhigh
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agents.oracle.variant).toBe("xhigh")
|
||||
})
|
||||
|
||||
test("standard agent override with category AND direct variant - direct wins", async () => {
|
||||
// #given - ultrabrain has variant=xhigh, but direct override says "max"
|
||||
const overrides = {
|
||||
oracle: { category: "ultrabrain", variant: "max" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - direct variant overrides category variant
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.variant).toBe("max")
|
||||
})
|
||||
|
||||
test("standard agent override with category AND direct reasoningEffort - direct wins", async () => {
|
||||
// #given - custom category has reasoningEffort=xhigh, direct override says "low"
|
||||
const categories = {
|
||||
"test-cat": {
|
||||
model: "openai/gpt-5.2",
|
||||
reasoningEffort: "xhigh" as const,
|
||||
},
|
||||
}
|
||||
const overrides = {
|
||||
oracle: { category: "test-cat", reasoningEffort: "low" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, categories)
|
||||
|
||||
// #then - direct reasoningEffort wins over category
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.reasoningEffort).toBe("low")
|
||||
})
|
||||
|
||||
test("standard agent override with category applies reasoningEffort from category when no direct override", async () => {
|
||||
// #given - custom category has reasoningEffort, no direct reasoningEffort in override
|
||||
const categories = {
|
||||
"reasoning-cat": {
|
||||
model: "openai/gpt-5.2",
|
||||
reasoningEffort: "high" as const,
|
||||
},
|
||||
}
|
||||
const overrides = {
|
||||
oracle: { category: "reasoning-cat" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL, categories)
|
||||
|
||||
// #then - category reasoningEffort is applied
|
||||
expect(agents.oracle).toBeDefined()
|
||||
expect(agents.oracle.reasoningEffort).toBe("high")
|
||||
})
|
||||
|
||||
test("sisyphus override with category expands category properties", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
sisyphus: { category: "ultrabrain" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - ultrabrain category: model=openai/gpt-5.2-codex, variant=xhigh
|
||||
expect(agents.sisyphus).toBeDefined()
|
||||
expect(agents.sisyphus.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agents.sisyphus.variant).toBe("xhigh")
|
||||
})
|
||||
|
||||
test("atlas override with category expands category properties", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
atlas: { category: "ultrabrain" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - ultrabrain category: model=openai/gpt-5.2-codex, variant=xhigh
|
||||
expect(agents.atlas).toBeDefined()
|
||||
expect(agents.atlas.model).toBe("openai/gpt-5.2-codex")
|
||||
expect(agents.atlas.variant).toBe("xhigh")
|
||||
})
|
||||
|
||||
test("override with non-existent category has no effect on config", async () => {
|
||||
// #given
|
||||
const overrides = {
|
||||
oracle: { category: "non-existent-category" } as any,
|
||||
}
|
||||
|
||||
// #when
|
||||
const agents = await createBuiltinAgents([], overrides, undefined, TEST_DEFAULT_MODEL)
|
||||
|
||||
// #then - no category-specific variant/reasoningEffort applied from non-existent category
|
||||
expect(agents.oracle).toBeDefined()
|
||||
const agentsWithoutOverride = await createBuiltinAgents([], {}, undefined, TEST_DEFAULT_MODEL)
|
||||
expect(agents.oracle.model).toBe(agentsWithoutOverride.oracle.model)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,11 +10,12 @@ import { createMetisAgent } from "./metis"
|
||||
import { createAtlasAgent } from "./atlas"
|
||||
import { createMomusAgent } from "./momus"
|
||||
import type { AvailableAgent, AvailableCategory, AvailableSkill } from "./dynamic-agent-prompt-builder"
|
||||
import { deepMerge, fetchAvailableModels, resolveModelWithFallback, AGENT_MODEL_REQUIREMENTS, findCaseInsensitive, includesCaseInsensitive } from "../shared"
|
||||
import { deepMerge, fetchAvailableModels, resolveModelWithFallback, AGENT_MODEL_REQUIREMENTS, findCaseInsensitive, includesCaseInsensitive, readConnectedProvidersCache } from "../shared"
|
||||
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../tools/delegate-task/constants"
|
||||
import { resolveMultipleSkills } from "../features/opencode-skill-loader/skill-content"
|
||||
import { createBuiltinSkills } from "../features/builtin-skills"
|
||||
import type { LoadedSkill, SkillScope } from "../features/opencode-skill-loader/types"
|
||||
import type { BrowserAutomationProvider } from "../config/schema"
|
||||
|
||||
type AgentSource = AgentFactory | AgentConfig
|
||||
|
||||
@@ -50,7 +51,8 @@ export function buildAgent(
|
||||
source: AgentSource,
|
||||
model: string,
|
||||
categories?: CategoriesConfig,
|
||||
gitMasterConfig?: GitMasterConfig
|
||||
gitMasterConfig?: GitMasterConfig,
|
||||
browserProvider?: BrowserAutomationProvider
|
||||
): AgentConfig {
|
||||
const base = isFactory(source) ? source(model) : source
|
||||
const categoryConfigs: Record<string, CategoryConfig> = categories
|
||||
@@ -74,7 +76,7 @@ export function buildAgent(
|
||||
}
|
||||
|
||||
if (agentWithCategory.skills?.length) {
|
||||
const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig })
|
||||
const { resolved } = resolveMultipleSkills(agentWithCategory.skills, { gitMasterConfig, browserProvider })
|
||||
if (resolved.size > 0) {
|
||||
const skillContent = Array.from(resolved.values()).join("\n\n")
|
||||
base.prompt = skillContent + (base.prompt ? "\n\n" + base.prompt : "")
|
||||
@@ -118,6 +120,33 @@ export function createEnvContext(): string {
|
||||
</omo-env>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a category reference from an agent override into concrete config properties.
|
||||
* Category properties are applied unconditionally (overwriting factory defaults),
|
||||
* because the user's chosen category should take priority over factory base values.
|
||||
* Direct override properties applied later via mergeAgentConfig() will supersede these.
|
||||
*/
|
||||
function applyCategoryOverride(
|
||||
config: AgentConfig,
|
||||
categoryName: string,
|
||||
mergedCategories: Record<string, CategoryConfig>
|
||||
): AgentConfig {
|
||||
const categoryConfig = mergedCategories[categoryName]
|
||||
if (!categoryConfig) return config
|
||||
|
||||
const result = { ...config } as AgentConfig & Record<string, unknown>
|
||||
if (categoryConfig.model) result.model = categoryConfig.model
|
||||
if (categoryConfig.variant !== undefined) result.variant = categoryConfig.variant
|
||||
if (categoryConfig.temperature !== undefined) result.temperature = categoryConfig.temperature
|
||||
if (categoryConfig.reasoningEffort !== undefined) result.reasoningEffort = categoryConfig.reasoningEffort
|
||||
if (categoryConfig.textVerbosity !== undefined) result.textVerbosity = categoryConfig.textVerbosity
|
||||
if (categoryConfig.thinking !== undefined) result.thinking = categoryConfig.thinking
|
||||
if (categoryConfig.top_p !== undefined) result.top_p = categoryConfig.top_p
|
||||
if (categoryConfig.maxTokens !== undefined) result.maxTokens = categoryConfig.maxTokens
|
||||
|
||||
return result as AgentConfig
|
||||
}
|
||||
|
||||
function mergeAgentConfig(
|
||||
base: AgentConfig,
|
||||
override: AgentOverrideConfig
|
||||
@@ -146,14 +175,14 @@ export async function createBuiltinAgents(
|
||||
categories?: CategoriesConfig,
|
||||
gitMasterConfig?: GitMasterConfig,
|
||||
discoveredSkills: LoadedSkill[] = [],
|
||||
client?: any
|
||||
client?: any,
|
||||
browserProvider?: BrowserAutomationProvider,
|
||||
uiSelectedModel?: string
|
||||
): Promise<Record<string, AgentConfig>> {
|
||||
if (!systemDefaultModel) {
|
||||
throw new Error("createBuiltinAgents requires systemDefaultModel")
|
||||
}
|
||||
|
||||
// Fetch available models at plugin init
|
||||
const availableModels = client ? await fetchAvailableModels(client) : new Set<string>()
|
||||
const connectedProviders = readConnectedProvidersCache()
|
||||
const availableModels = client
|
||||
? await fetchAvailableModels(client, { connectedProviders: connectedProviders ?? undefined })
|
||||
: new Set<string>()
|
||||
|
||||
const result: Record<string, AgentConfig> = {}
|
||||
const availableAgents: AvailableAgent[] = []
|
||||
@@ -167,7 +196,7 @@ export async function createBuiltinAgents(
|
||||
description: categories?.[name]?.description ?? CATEGORY_DESCRIPTIONS[name] ?? "General tasks",
|
||||
}))
|
||||
|
||||
const builtinSkills = createBuiltinSkills()
|
||||
const builtinSkills = createBuiltinSkills({ browserProvider })
|
||||
const builtinSkillNames = new Set(builtinSkills.map(s => s.name))
|
||||
|
||||
const builtinAvailable: AvailableSkill[] = builtinSkills.map((skill) => ({
|
||||
@@ -196,28 +225,35 @@ export async function createBuiltinAgents(
|
||||
const override = findCaseInsensitive(agentOverrides, agentName)
|
||||
const requirement = AGENT_MODEL_REQUIREMENTS[agentName]
|
||||
|
||||
// Use resolver to determine model
|
||||
const { model, variant: resolvedVariant } = resolveModelWithFallback({
|
||||
const resolution = resolveModelWithFallback({
|
||||
uiSelectedModel,
|
||||
userModel: override?.model,
|
||||
fallbackChain: requirement?.fallbackChain,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
if (!resolution) continue
|
||||
const { model, variant: resolvedVariant } = resolution
|
||||
|
||||
let config = buildAgent(source, model, mergedCategories, gitMasterConfig)
|
||||
let config = buildAgent(source, model, mergedCategories, gitMasterConfig, browserProvider)
|
||||
|
||||
// Apply variant from override or resolved fallback chain
|
||||
if (override?.variant) {
|
||||
config = { ...config, variant: override.variant }
|
||||
} else if (resolvedVariant) {
|
||||
// Apply resolved variant from model fallback chain
|
||||
if (resolvedVariant) {
|
||||
config = { ...config, variant: resolvedVariant }
|
||||
}
|
||||
|
||||
// Expand override.category into concrete properties (higher priority than factory/resolved)
|
||||
const overrideCategory = (override as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (overrideCategory) {
|
||||
config = applyCategoryOverride(config, overrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (agentName === "librarian" && directory && config.prompt) {
|
||||
const envContext = createEnvContext()
|
||||
config = { ...config, prompt: config.prompt + envContext }
|
||||
}
|
||||
|
||||
// Direct override properties take highest priority
|
||||
if (override) {
|
||||
config = mergeAgentConfig(config, override)
|
||||
}
|
||||
@@ -238,72 +274,84 @@ export async function createBuiltinAgents(
|
||||
const sisyphusOverride = agentOverrides["sisyphus"]
|
||||
const sisyphusRequirement = AGENT_MODEL_REQUIREMENTS["sisyphus"]
|
||||
|
||||
// Use resolver to determine model
|
||||
const { model: sisyphusModel, variant: sisyphusResolvedVariant } = resolveModelWithFallback({
|
||||
const sisyphusResolution = resolveModelWithFallback({
|
||||
uiSelectedModel,
|
||||
userModel: sisyphusOverride?.model,
|
||||
fallbackChain: sisyphusRequirement?.fallbackChain,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
|
||||
let sisyphusConfig = createSisyphusAgent(
|
||||
sisyphusModel,
|
||||
availableAgents,
|
||||
undefined,
|
||||
availableSkills,
|
||||
availableCategories
|
||||
)
|
||||
|
||||
// Apply variant from override or resolved fallback chain
|
||||
if (sisyphusOverride?.variant) {
|
||||
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusOverride.variant }
|
||||
} else if (sisyphusResolvedVariant) {
|
||||
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusResolvedVariant }
|
||||
}
|
||||
if (sisyphusResolution) {
|
||||
const { model: sisyphusModel, variant: sisyphusResolvedVariant } = sisyphusResolution
|
||||
|
||||
if (directory && sisyphusConfig.prompt) {
|
||||
const envContext = createEnvContext()
|
||||
sisyphusConfig = { ...sisyphusConfig, prompt: sisyphusConfig.prompt + envContext }
|
||||
}
|
||||
let sisyphusConfig = createSisyphusAgent(
|
||||
sisyphusModel,
|
||||
availableAgents,
|
||||
undefined,
|
||||
availableSkills,
|
||||
availableCategories
|
||||
)
|
||||
|
||||
if (sisyphusResolvedVariant) {
|
||||
sisyphusConfig = { ...sisyphusConfig, variant: sisyphusResolvedVariant }
|
||||
}
|
||||
|
||||
if (sisyphusOverride) {
|
||||
sisyphusConfig = mergeAgentConfig(sisyphusConfig, sisyphusOverride)
|
||||
}
|
||||
const sisOverrideCategory = (sisyphusOverride as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (sisOverrideCategory) {
|
||||
sisyphusConfig = applyCategoryOverride(sisyphusConfig, sisOverrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
result["sisyphus"] = sisyphusConfig
|
||||
if (directory && sisyphusConfig.prompt) {
|
||||
const envContext = createEnvContext()
|
||||
sisyphusConfig = { ...sisyphusConfig, prompt: sisyphusConfig.prompt + envContext }
|
||||
}
|
||||
|
||||
if (sisyphusOverride) {
|
||||
sisyphusConfig = mergeAgentConfig(sisyphusConfig, sisyphusOverride)
|
||||
}
|
||||
|
||||
result["sisyphus"] = sisyphusConfig
|
||||
}
|
||||
}
|
||||
|
||||
if (!disabledAgents.includes("atlas")) {
|
||||
const orchestratorOverride = agentOverrides["atlas"]
|
||||
const atlasRequirement = AGENT_MODEL_REQUIREMENTS["atlas"]
|
||||
|
||||
// Use resolver to determine model
|
||||
const { model: atlasModel, variant: atlasResolvedVariant } = resolveModelWithFallback({
|
||||
const atlasResolution = resolveModelWithFallback({
|
||||
uiSelectedModel,
|
||||
userModel: orchestratorOverride?.model,
|
||||
fallbackChain: atlasRequirement?.fallbackChain,
|
||||
availableModels,
|
||||
systemDefaultModel,
|
||||
})
|
||||
|
||||
let orchestratorConfig = createAtlasAgent({
|
||||
model: atlasModel,
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
userCategories: categories,
|
||||
})
|
||||
|
||||
// Apply variant from override or resolved fallback chain
|
||||
if (orchestratorOverride?.variant) {
|
||||
orchestratorConfig = { ...orchestratorConfig, variant: orchestratorOverride.variant }
|
||||
} else if (atlasResolvedVariant) {
|
||||
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
|
||||
}
|
||||
if (atlasResolution) {
|
||||
const { model: atlasModel, variant: atlasResolvedVariant } = atlasResolution
|
||||
|
||||
if (orchestratorOverride) {
|
||||
orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride)
|
||||
}
|
||||
let orchestratorConfig = createAtlasAgent({
|
||||
model: atlasModel,
|
||||
availableAgents,
|
||||
availableSkills,
|
||||
userCategories: categories,
|
||||
})
|
||||
|
||||
if (atlasResolvedVariant) {
|
||||
orchestratorConfig = { ...orchestratorConfig, variant: atlasResolvedVariant }
|
||||
}
|
||||
|
||||
result["atlas"] = orchestratorConfig
|
||||
const atlasOverrideCategory = (orchestratorOverride as Record<string, unknown> | undefined)?.category as string | undefined
|
||||
if (atlasOverrideCategory) {
|
||||
orchestratorConfig = applyCategoryOverride(orchestratorConfig, atlasOverrideCategory, mergedCategories)
|
||||
}
|
||||
|
||||
if (orchestratorOverride) {
|
||||
orchestratorConfig = mergeAgentConfig(orchestratorConfig, orchestratorOverride)
|
||||
}
|
||||
|
||||
result["atlas"] = orchestratorConfig
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@@ -8,16 +8,17 @@ CLI entry: `bunx oh-my-opencode`. Interactive installer, doctor diagnostics. Com
|
||||
|
||||
```
|
||||
cli/
|
||||
├── index.ts # Commander.js entry
|
||||
├── index.ts # Commander.js entry (4 commands)
|
||||
├── install.ts # Interactive TUI (520 lines)
|
||||
├── config-manager.ts # JSONC parsing (641 lines)
|
||||
├── config-manager.ts # JSONC parsing (664 lines)
|
||||
├── types.ts # InstallArgs, InstallConfig
|
||||
├── model-fallback.ts # Model fallback configuration
|
||||
├── doctor/
|
||||
│ ├── index.ts # Doctor entry
|
||||
│ ├── runner.ts # Check orchestration
|
||||
│ ├── formatter.ts # Colored output
|
||||
│ ├── constants.ts # Check IDs, symbols
|
||||
│ ├── types.ts # CheckResult, CheckDefinition
|
||||
│ ├── types.ts # CheckResult, CheckDefinition (114 lines)
|
||||
│ └── checks/ # 14 checks, 21 files
|
||||
│ ├── version.ts # OpenCode + plugin version
|
||||
│ ├── config.ts # JSONC validity, Zod
|
||||
@@ -25,6 +26,7 @@ cli/
|
||||
│ ├── dependencies.ts # AST-Grep, Comment Checker
|
||||
│ ├── lsp.ts # LSP connectivity
|
||||
│ ├── mcp.ts # MCP validation
|
||||
│ ├── model-resolution.ts # Model resolution check
|
||||
│ └── gh.ts # GitHub CLI
|
||||
├── run/
|
||||
│ └── index.ts # Session launcher
|
||||
@@ -36,36 +38,37 @@ cli/
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `install` | Interactive setup |
|
||||
| `doctor` | 14 health checks |
|
||||
| `run` | Launch session |
|
||||
| `get-local-version` | Version check |
|
||||
| `install` | Interactive setup with provider selection |
|
||||
| `doctor` | 14 health checks for diagnostics |
|
||||
| `run` | Launch session with todo enforcement |
|
||||
| `get-local-version` | Version detection and update check |
|
||||
|
||||
## DOCTOR CATEGORIES
|
||||
## DOCTOR CATEGORIES (14 Checks)
|
||||
|
||||
| Category | Checks |
|
||||
|----------|--------|
|
||||
| installation | opencode, plugin |
|
||||
| configuration | config validity, Zod |
|
||||
| configuration | config validity, Zod, model-resolution |
|
||||
| authentication | anthropic, openai, google |
|
||||
| dependencies | ast-grep, comment-checker |
|
||||
| dependencies | ast-grep, comment-checker, gh-cli |
|
||||
| tools | LSP, MCP |
|
||||
| updates | version comparison |
|
||||
|
||||
## HOW TO ADD CHECK
|
||||
|
||||
1. Create `src/cli/doctor/checks/my-check.ts`
|
||||
2. Export from `checks/index.ts`
|
||||
3. Add to `getAllCheckDefinitions()`
|
||||
2. Export `getXXXCheckDefinition()` factory returning `CheckDefinition`
|
||||
3. Add to `getAllCheckDefinitions()` in `checks/index.ts`
|
||||
|
||||
## TUI FRAMEWORK
|
||||
|
||||
- **@clack/prompts**: `select()`, `spinner()`, `intro()`
|
||||
- **picocolors**: Terminal colors
|
||||
- **Symbols**: ✓ (pass), ✗ (fail), ⚠ (warn)
|
||||
- **@clack/prompts**: `select()`, `spinner()`, `intro()`, `outro()`
|
||||
- **picocolors**: Terminal colors for status and headers
|
||||
- **Symbols**: ✓ (pass), ✗ (fail), ⚠ (warn), ℹ (info)
|
||||
|
||||
## ANTI-PATTERNS
|
||||
|
||||
- **Blocking in non-TTY**: Check `process.stdout.isTTY`
|
||||
- **Direct JSON.parse**: Use `parseJsonc()`
|
||||
- **Silent failures**: Return warn/fail in doctor
|
||||
- **Blocking in non-TTY**: Always check `process.stdout.isTTY`
|
||||
- **Direct JSON.parse**: Use `parseJsonc()` from shared utils
|
||||
- **Silent failures**: Return `warn` or `fail` in doctor instead of throwing
|
||||
- **Hardcoded paths**: Use `getOpenCodeConfigPaths()` from `config-manager.ts`
|
||||
|
||||
@@ -5,54 +5,54 @@ exports[`generateModelConfig no providers available returns ULTIMATE_FALLBACK fo
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"momus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"writing": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -196,10 +196,10 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
"model": "openai/gpt-5.2",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -230,7 +230,7 @@ exports[`generateModelConfig single native provider uses OpenAI models when only
|
||||
"model": "openai/gpt-5.2",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
@@ -263,10 +263,10 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
"model": "openai/gpt-5.2",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -297,7 +297,7 @@ exports[`generateModelConfig single native provider uses OpenAI models with isMa
|
||||
"model": "openai/gpt-5.2",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-5.2-codex",
|
||||
@@ -327,57 +327,57 @@ exports[`generateModelConfig single native provider uses Gemini models when only
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -388,57 +388,57 @@ exports[`generateModelConfig single native provider uses Gemini models with isMa
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"momus": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -466,7 +466,7 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -482,7 +482,7 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -499,10 +499,10 @@ exports[`generateModelConfig all native providers uses preferred models from fal
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -530,7 +530,7 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -547,7 +547,7 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -565,10 +565,10 @@ exports[`generateModelConfig all native providers uses preferred models with isM
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -585,7 +585,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "opencode/claude-opus-4-5",
|
||||
@@ -596,7 +596,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "opencode/gemini-3-flash-preview",
|
||||
"model": "opencode/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/gpt-5.2",
|
||||
@@ -612,7 +612,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/gemini-3-pro-preview",
|
||||
"model": "opencode/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -629,10 +629,10 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models when on
|
||||
"model": "opencode/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/gemini-3-pro-preview",
|
||||
"model": "opencode/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "opencode/gemini-3-flash-preview",
|
||||
"model": "opencode/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -649,7 +649,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"model": "opencode/claude-haiku-4-5",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "opencode/claude-opus-4-5",
|
||||
@@ -660,7 +660,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "opencode/gemini-3-flash-preview",
|
||||
"model": "opencode/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/gpt-5.2",
|
||||
@@ -677,7 +677,7 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/gemini-3-pro-preview",
|
||||
"model": "opencode/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -695,10 +695,10 @@ exports[`generateModelConfig fallback providers uses OpenCode Zen models with is
|
||||
"model": "opencode/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/gemini-3-pro-preview",
|
||||
"model": "opencode/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "opencode/gemini-3-flash-preview",
|
||||
"model": "opencode/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -712,7 +712,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "github-copilot/gpt-5-mini",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
@@ -726,7 +726,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
@@ -742,7 +742,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -759,10 +759,10 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models when
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -776,7 +776,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "github-copilot/gpt-5-mini",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
@@ -790,7 +790,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
@@ -807,7 +807,7 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -825,10 +825,10 @@ exports[`generateModelConfig fallback providers uses GitHub Copilot models with
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -839,51 +839,51 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian whe
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"metis": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"momus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "zai-coding-plan/glm-4.6v",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"writing": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
@@ -897,28 +897,28 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian wit
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
|
||||
"agents": {
|
||||
"atlas": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "opencode/gpt-5-nano",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
},
|
||||
"metis": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"momus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "zai-coding-plan/glm-4.6v",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"sisyphus": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
@@ -926,22 +926,22 @@ exports[`generateModelConfig fallback providers uses ZAI model for librarian wit
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"quick": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"writing": {
|
||||
"model": "zai-coding-plan/glm-4.7",
|
||||
@@ -961,7 +961,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "opencode/glm-4.7-free",
|
||||
"model": "opencode/big-pickle",
|
||||
},
|
||||
"metis": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
@@ -972,7 +972,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "opencode/gemini-3-flash-preview",
|
||||
"model": "opencode/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "opencode/gpt-5.2",
|
||||
@@ -988,7 +988,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "opencode/gemini-3-pro-preview",
|
||||
"model": "opencode/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -1005,10 +1005,10 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "opencode/gemini-3-pro-preview",
|
||||
"model": "opencode/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "opencode/gemini-3-flash-preview",
|
||||
"model": "opencode/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1022,7 +1022,7 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"explore": {
|
||||
"model": "opencode/grok-code",
|
||||
"model": "github-copilot/gpt-5-mini",
|
||||
},
|
||||
"librarian": {
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
@@ -1036,7 +1036,7 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -1052,7 +1052,7 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -1069,10 +1069,10 @@ exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot comb
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1099,7 +1099,7 @@ exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combinat
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "anthropic/claude-haiku-4-5",
|
||||
"model": "zai-coding-plan/glm-4.6v",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
@@ -1163,7 +1163,7 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "anthropic/claude-opus-4-5",
|
||||
@@ -1179,7 +1179,7 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -1196,10 +1196,10 @@ exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combi
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1227,7 +1227,7 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "github-copilot/gpt-5.2",
|
||||
@@ -1243,7 +1243,7 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -1260,10 +1260,10 @@ exports[`generateModelConfig mixed provider scenarios uses all fallback provider
|
||||
"model": "github-copilot/claude-sonnet-4.5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "github-copilot/gemini-3-pro-preview",
|
||||
"model": "github-copilot/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "github-copilot/gemini-3-flash-preview",
|
||||
"model": "github-copilot/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1291,7 +1291,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -1307,7 +1307,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -1324,10 +1324,10 @@ exports[`generateModelConfig mixed provider scenarios uses all providers togethe
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1355,7 +1355,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
"variant": "medium",
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
"oracle": {
|
||||
"model": "openai/gpt-5.2",
|
||||
@@ -1372,7 +1372,7 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
},
|
||||
"categories": {
|
||||
"artistry": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
"variant": "max",
|
||||
},
|
||||
"quick": {
|
||||
@@ -1390,10 +1390,10 @@ exports[`generateModelConfig mixed provider scenarios uses all providers with is
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
"visual-engineering": {
|
||||
"model": "google/gemini-3-pro-preview",
|
||||
"model": "google/gemini-3-pro",
|
||||
},
|
||||
"writing": {
|
||||
"model": "google/gemini-3-flash-preview",
|
||||
"model": "google/gemini-3-flash",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ describe("fetchNpmDistTags", () => {
|
||||
})
|
||||
|
||||
describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
test("Gemini models include full spec (limit + modalities)", () => {
|
||||
test("all models include full spec (limit + modalities + Antigravity label)", () => {
|
||||
const google = (ANTIGRAVITY_PROVIDER_CONFIG as any).google
|
||||
expect(google).toBeTruthy()
|
||||
|
||||
@@ -178,9 +178,11 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
expect(models).toBeTruthy()
|
||||
|
||||
const required = [
|
||||
"antigravity-gemini-3-pro-high",
|
||||
"antigravity-gemini-3-pro-low",
|
||||
"antigravity-gemini-3-pro",
|
||||
"antigravity-gemini-3-flash",
|
||||
"antigravity-claude-sonnet-4-5",
|
||||
"antigravity-claude-sonnet-4-5-thinking",
|
||||
"antigravity-claude-opus-4-5-thinking",
|
||||
]
|
||||
|
||||
for (const key of required) {
|
||||
@@ -198,6 +200,43 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||
expect(Array.isArray(model.modalities.output)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("Gemini models have variant definitions", () => {
|
||||
// #given the antigravity provider config
|
||||
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
|
||||
|
||||
// #when checking Gemini Pro variants
|
||||
const pro = models["antigravity-gemini-3-pro"]
|
||||
// #then should have low and high variants
|
||||
expect(pro.variants).toBeTruthy()
|
||||
expect(pro.variants.low).toBeTruthy()
|
||||
expect(pro.variants.high).toBeTruthy()
|
||||
|
||||
// #when checking Gemini Flash variants
|
||||
const flash = models["antigravity-gemini-3-flash"]
|
||||
// #then should have minimal, low, medium, high variants
|
||||
expect(flash.variants).toBeTruthy()
|
||||
expect(flash.variants.minimal).toBeTruthy()
|
||||
expect(flash.variants.low).toBeTruthy()
|
||||
expect(flash.variants.medium).toBeTruthy()
|
||||
expect(flash.variants.high).toBeTruthy()
|
||||
})
|
||||
|
||||
test("Claude thinking models have variant definitions", () => {
|
||||
// #given the antigravity provider config
|
||||
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
|
||||
|
||||
// #when checking Claude thinking variants
|
||||
const sonnetThinking = models["antigravity-claude-sonnet-4-5-thinking"]
|
||||
const opusThinking = models["antigravity-claude-opus-4-5-thinking"]
|
||||
|
||||
// #then both should have low and max variants
|
||||
for (const model of [sonnetThinking, opusThinking]) {
|
||||
expect(model.variants).toBeTruthy()
|
||||
expect(model.variants.low).toBeTruthy()
|
||||
expect(model.variants.max).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("generateOmoConfig - model fallback system", () => {
|
||||
@@ -277,7 +316,7 @@ describe("generateOmoConfig - model fallback system", () => {
|
||||
|
||||
// #then should use ultimate fallback for all agents
|
||||
expect(result.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json")
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("opencode/glm-4.7-free")
|
||||
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("opencode/big-pickle")
|
||||
})
|
||||
|
||||
test("uses zai-coding-plan/glm-4.7 for librarian when Z.ai available", () => {
|
||||
|
||||
@@ -497,38 +497,61 @@ export async function runBunInstallWithDetails(): Promise<BunInstallResult> {
|
||||
*
|
||||
* 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)
|
||||
* Since opencode-antigravity-auth v1.3.0, models use a variant system:
|
||||
* - `antigravity-gemini-3-pro` with variants: low, high
|
||||
* - `antigravity-gemini-3-flash` with variants: minimal, low, medium, high
|
||||
*
|
||||
* Legacy names rely on Gemini CLI using `-preview` suffix for disambiguation.
|
||||
* If Google removes `-preview`, legacy names may route to wrong quota.
|
||||
* Legacy tier-suffixed names (e.g., `antigravity-gemini-3-pro-high`) still work
|
||||
* but variants are the recommended approach.
|
||||
*
|
||||
* @see https://github.com/NoeFabris/opencode-antigravity-auth#migration-guide-v127
|
||||
* @see https://github.com/NoeFabris/opencode-antigravity-auth#models
|
||||
*/
|
||||
export const ANTIGRAVITY_PROVIDER_CONFIG = {
|
||||
google: {
|
||||
name: "Google",
|
||||
models: {
|
||||
"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"] },
|
||||
},
|
||||
"antigravity-gemini-3-pro-low": {
|
||||
name: "Gemini 3 Pro Low (Antigravity)",
|
||||
thinking: true,
|
||||
attachment: true,
|
||||
"antigravity-gemini-3-pro": {
|
||||
name: "Gemini 3 Pro (Antigravity)",
|
||||
limit: { context: 1048576, output: 65535 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
low: { thinkingLevel: "low" },
|
||||
high: { thinkingLevel: "high" },
|
||||
},
|
||||
},
|
||||
"antigravity-gemini-3-flash": {
|
||||
name: "Gemini 3 Flash (Antigravity)",
|
||||
attachment: true,
|
||||
limit: { context: 1048576, output: 65536 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
minimal: { thinkingLevel: "minimal" },
|
||||
low: { thinkingLevel: "low" },
|
||||
medium: { thinkingLevel: "medium" },
|
||||
high: { thinkingLevel: "high" },
|
||||
},
|
||||
},
|
||||
"antigravity-claude-sonnet-4-5": {
|
||||
name: "Claude Sonnet 4.5 (Antigravity)",
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
"antigravity-claude-sonnet-4-5-thinking": {
|
||||
name: "Claude Sonnet 4.5 Thinking (Antigravity)",
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
low: { thinkingConfig: { thinkingBudget: 8192 } },
|
||||
max: { thinkingConfig: { thinkingBudget: 32768 } },
|
||||
},
|
||||
},
|
||||
"antigravity-claude-opus-4-5-thinking": {
|
||||
name: "Claude Opus 4.5 Thinking (Antigravity)",
|
||||
limit: { context: 200000, output: 64000 },
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
variants: {
|
||||
low: { thinkingConfig: { thinkingBudget: 8192 } },
|
||||
max: { thinkingConfig: { thinkingBudget: 32768 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getDependencyCheckDefinitions } from "./dependencies"
|
||||
import { getGhCliCheckDefinition } from "./gh"
|
||||
import { getLspCheckDefinition } from "./lsp"
|
||||
import { getMcpCheckDefinitions } from "./mcp"
|
||||
import { getMcpOAuthCheckDefinition } from "./mcp-oauth"
|
||||
import { getVersionCheckDefinition } from "./version"
|
||||
|
||||
export * from "./opencode"
|
||||
@@ -19,6 +20,7 @@ export * from "./dependencies"
|
||||
export * from "./gh"
|
||||
export * from "./lsp"
|
||||
export * from "./mcp"
|
||||
export * from "./mcp-oauth"
|
||||
export * from "./version"
|
||||
|
||||
export function getAllCheckDefinitions(): CheckDefinition[] {
|
||||
@@ -32,6 +34,7 @@ export function getAllCheckDefinitions(): CheckDefinition[] {
|
||||
getGhCliCheckDefinition(),
|
||||
getLspCheckDefinition(),
|
||||
...getMcpCheckDefinitions(),
|
||||
getMcpOAuthCheckDefinition(),
|
||||
getVersionCheckDefinition(),
|
||||
]
|
||||
}
|
||||
|
||||
133
src/cli/doctor/checks/mcp-oauth.test.ts
Normal file
133
src/cli/doctor/checks/mcp-oauth.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { describe, it, expect, spyOn, afterEach } from "bun:test"
|
||||
import * as mcpOauth from "./mcp-oauth"
|
||||
|
||||
describe("mcp-oauth check", () => {
|
||||
describe("getMcpOAuthCheckDefinition", () => {
|
||||
it("returns check definition with correct properties", () => {
|
||||
// #given
|
||||
// #when getting definition
|
||||
const def = mcpOauth.getMcpOAuthCheckDefinition()
|
||||
|
||||
// #then should have correct structure
|
||||
expect(def.id).toBe("mcp-oauth-tokens")
|
||||
expect(def.name).toBe("MCP OAuth Tokens")
|
||||
expect(def.category).toBe("tools")
|
||||
expect(def.critical).toBe(false)
|
||||
expect(typeof def.check).toBe("function")
|
||||
})
|
||||
})
|
||||
|
||||
describe("checkMcpOAuthTokens", () => {
|
||||
let readStoreSpy: ReturnType<typeof spyOn>
|
||||
|
||||
afterEach(() => {
|
||||
readStoreSpy?.mockRestore()
|
||||
})
|
||||
|
||||
it("returns skip when no tokens stored", async () => {
|
||||
// #given no OAuth tokens configured
|
||||
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue(null)
|
||||
|
||||
// #when checking OAuth tokens
|
||||
const result = await mcpOauth.checkMcpOAuthTokens()
|
||||
|
||||
// #then should skip
|
||||
expect(result.status).toBe("skip")
|
||||
expect(result.message).toContain("No OAuth")
|
||||
})
|
||||
|
||||
it("returns pass when all tokens valid", async () => {
|
||||
// #given valid tokens with future expiry (expiresAt is in epoch seconds)
|
||||
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
||||
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
|
||||
"example.com/resource1": {
|
||||
accessToken: "token1",
|
||||
expiresAt: futureTime,
|
||||
},
|
||||
"example.com/resource2": {
|
||||
accessToken: "token2",
|
||||
expiresAt: futureTime,
|
||||
},
|
||||
})
|
||||
|
||||
// #when checking OAuth tokens
|
||||
const result = await mcpOauth.checkMcpOAuthTokens()
|
||||
|
||||
// #then should pass
|
||||
expect(result.status).toBe("pass")
|
||||
expect(result.message).toContain("2")
|
||||
expect(result.message).toContain("valid")
|
||||
})
|
||||
|
||||
it("returns warn when some tokens expired", async () => {
|
||||
// #given mix of valid and expired tokens (expiresAt is in epoch seconds)
|
||||
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
||||
const pastTime = Math.floor(Date.now() / 1000) - 3600
|
||||
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
|
||||
"example.com/resource1": {
|
||||
accessToken: "token1",
|
||||
expiresAt: futureTime,
|
||||
},
|
||||
"example.com/resource2": {
|
||||
accessToken: "token2",
|
||||
expiresAt: pastTime,
|
||||
},
|
||||
})
|
||||
|
||||
// #when checking OAuth tokens
|
||||
const result = await mcpOauth.checkMcpOAuthTokens()
|
||||
|
||||
// #then should warn
|
||||
expect(result.status).toBe("warn")
|
||||
expect(result.message).toContain("1")
|
||||
expect(result.message).toContain("expired")
|
||||
expect(result.details?.some((d: string) => d.includes("Expired"))).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("returns pass when tokens have no expiry", async () => {
|
||||
// #given tokens without expiry info
|
||||
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
|
||||
"example.com/resource1": {
|
||||
accessToken: "token1",
|
||||
},
|
||||
})
|
||||
|
||||
// #when checking OAuth tokens
|
||||
const result = await mcpOauth.checkMcpOAuthTokens()
|
||||
|
||||
// #then should pass (no expiry = assume valid)
|
||||
expect(result.status).toBe("pass")
|
||||
expect(result.message).toContain("1")
|
||||
})
|
||||
|
||||
it("includes token details in output", async () => {
|
||||
// #given multiple tokens
|
||||
const futureTime = Math.floor(Date.now() / 1000) + 3600
|
||||
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
|
||||
"api.example.com/v1": {
|
||||
accessToken: "token1",
|
||||
expiresAt: futureTime,
|
||||
},
|
||||
"auth.example.com/oauth": {
|
||||
accessToken: "token2",
|
||||
expiresAt: futureTime,
|
||||
},
|
||||
})
|
||||
|
||||
// #when checking OAuth tokens
|
||||
const result = await mcpOauth.checkMcpOAuthTokens()
|
||||
|
||||
// #then should list tokens in details
|
||||
expect(result.details).toBeDefined()
|
||||
expect(result.details?.length).toBeGreaterThan(0)
|
||||
expect(
|
||||
result.details?.some((d: string) => d.includes("api.example.com"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
result.details?.some((d: string) => d.includes("auth.example.com"))
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
80
src/cli/doctor/checks/mcp-oauth.ts
Normal file
80
src/cli/doctor/checks/mcp-oauth.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { CheckResult, CheckDefinition } from "../types"
|
||||
import { CHECK_IDS, CHECK_NAMES } from "../constants"
|
||||
import { getMcpOauthStoragePath } from "../../../features/mcp-oauth/storage"
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
|
||||
interface OAuthTokenData {
|
||||
accessToken: string
|
||||
refreshToken?: string
|
||||
expiresAt?: number
|
||||
clientInfo?: {
|
||||
clientId: string
|
||||
clientSecret?: string
|
||||
}
|
||||
}
|
||||
|
||||
type TokenStore = Record<string, OAuthTokenData>
|
||||
|
||||
export function readTokenStore(): TokenStore | null {
|
||||
const filePath = getMcpOauthStoragePath()
|
||||
if (!existsSync(filePath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8")
|
||||
return JSON.parse(content) as TokenStore
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkMcpOAuthTokens(): Promise<CheckResult> {
|
||||
const store = readTokenStore()
|
||||
|
||||
if (!store || Object.keys(store).length === 0) {
|
||||
return {
|
||||
name: CHECK_NAMES[CHECK_IDS.MCP_OAUTH_TOKENS],
|
||||
status: "skip",
|
||||
message: "No OAuth tokens configured",
|
||||
details: ["Optional: Configure OAuth tokens for MCP servers"],
|
||||
}
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const tokens = Object.entries(store)
|
||||
const expiredTokens = tokens.filter(
|
||||
([, token]) => token.expiresAt && token.expiresAt < now
|
||||
)
|
||||
|
||||
if (expiredTokens.length > 0) {
|
||||
return {
|
||||
name: CHECK_NAMES[CHECK_IDS.MCP_OAUTH_TOKENS],
|
||||
status: "warn",
|
||||
message: `${expiredTokens.length} of ${tokens.length} token(s) expired`,
|
||||
details: [
|
||||
...tokens
|
||||
.filter(([, token]) => !token.expiresAt || token.expiresAt >= now)
|
||||
.map(([key]) => `Valid: ${key}`),
|
||||
...expiredTokens.map(([key]) => `Expired: ${key}`),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: CHECK_NAMES[CHECK_IDS.MCP_OAUTH_TOKENS],
|
||||
status: "pass",
|
||||
message: `${tokens.length} OAuth token(s) valid`,
|
||||
details: tokens.map(([key]) => `Configured: ${key}`),
|
||||
}
|
||||
}
|
||||
|
||||
export function getMcpOAuthCheckDefinition(): CheckDefinition {
|
||||
return {
|
||||
id: CHECK_IDS.MCP_OAUTH_TOKENS,
|
||||
name: CHECK_NAMES[CHECK_IDS.MCP_OAUTH_TOKENS],
|
||||
category: "tools",
|
||||
check: checkMcpOAuthTokens,
|
||||
critical: false,
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ describe("model-resolution check", () => {
|
||||
// #then: Should have category entries
|
||||
const visual = info.categories.find((c) => c.name === "visual-engineering")
|
||||
expect(visual).toBeDefined()
|
||||
expect(visual!.requirement.fallbackChain[0]?.model).toBe("gemini-3-pro-preview")
|
||||
expect(visual!.requirement.fallbackChain[0]?.model).toBe("gemini-3-pro")
|
||||
expect(visual!.requirement.fallbackChain[0]?.providers).toContain("google")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -199,9 +199,11 @@ function buildDetailsArray(info: ModelResolutionInfo, available: AvailableModels
|
||||
details.push("═══ Available Models (from cache) ═══")
|
||||
details.push("")
|
||||
if (available.cacheExists) {
|
||||
details.push(` Providers: ${available.providers.length} (${available.providers.slice(0, 8).join(", ")}${available.providers.length > 8 ? "..." : ""})`)
|
||||
details.push(` Providers in cache: ${available.providers.length}`)
|
||||
details.push(` Sample: ${available.providers.slice(0, 6).join(", ")}${available.providers.length > 6 ? "..." : ""}`)
|
||||
details.push(` Total models: ${available.modelCount}`)
|
||||
details.push(` Cache: ~/.cache/opencode/models.json`)
|
||||
details.push(` ℹ Runtime: only connected providers used`)
|
||||
details.push(` Refresh: opencode models --refresh`)
|
||||
} else {
|
||||
details.push(" ⚠ Cache not found. Run 'opencode' to populate.")
|
||||
|
||||
@@ -32,6 +32,7 @@ export const CHECK_IDS = {
|
||||
LSP_SERVERS: "lsp-servers",
|
||||
MCP_BUILTIN: "mcp-builtin",
|
||||
MCP_USER: "mcp-user",
|
||||
MCP_OAUTH_TOKENS: "mcp-oauth-tokens",
|
||||
VERSION_STATUS: "version-status",
|
||||
} as const
|
||||
|
||||
@@ -50,6 +51,7 @@ export const CHECK_NAMES: Record<string, string> = {
|
||||
[CHECK_IDS.LSP_SERVERS]: "LSP Servers",
|
||||
[CHECK_IDS.MCP_BUILTIN]: "Built-in MCP Servers",
|
||||
[CHECK_IDS.MCP_USER]: "User MCP Configuration",
|
||||
[CHECK_IDS.MCP_OAUTH_TOKENS]: "MCP OAuth Tokens",
|
||||
[CHECK_IDS.VERSION_STATUS]: "Version Status",
|
||||
} as const
|
||||
|
||||
|
||||
17
src/cli/index.test.ts
Normal file
17
src/cli/index.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import packageJson from "../../package.json" with { type: "json" }
|
||||
|
||||
describe("CLI version", () => {
|
||||
it("reads version from package.json as valid semver", () => {
|
||||
//#given
|
||||
const semverRegex = /^\d+\.\d+\.\d+(-[\w.]+)?$/
|
||||
|
||||
//#when
|
||||
const version = packageJson.version
|
||||
|
||||
//#then
|
||||
expect(version).toMatch(semverRegex)
|
||||
expect(typeof version).toBe("string")
|
||||
expect(version.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import { install } from "./install"
|
||||
import { run } from "./run"
|
||||
import { getLocalVersion } from "./get-local-version"
|
||||
import { doctor } from "./doctor"
|
||||
import { createMcpOAuthCommand } from "./mcp-oauth"
|
||||
import type { InstallArgs } from "./types"
|
||||
import type { RunOptions } from "./run"
|
||||
import type { GetLocalVersionOptions } from "./get-local-version/types"
|
||||
@@ -150,4 +151,6 @@ program
|
||||
console.log(`oh-my-opencode v${VERSION}`)
|
||||
})
|
||||
|
||||
program.addCommand(createMcpOAuthCommand())
|
||||
|
||||
program.parse()
|
||||
|
||||
@@ -178,7 +178,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/glm-4.7-free as fallback" },
|
||||
{ value: "no" as const, label: "No", hint: "Will use opencode/big-pickle 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" },
|
||||
],
|
||||
@@ -363,7 +363,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
|
||||
}
|
||||
|
||||
if (!config.hasClaude && !config.hasOpenAI && !config.hasGemini && !config.hasCopilot && !config.hasOpencodeZen) {
|
||||
printWarning("No model providers configured. Using opencode/glm-4.7-free as fallback.")
|
||||
printWarning("No model providers configured. Using opencode/big-pickle as fallback.")
|
||||
}
|
||||
|
||||
console.log(`${SYMBOLS.star} ${color.bold(color.green(isUpdate ? "Configuration updated!" : "Installation complete!"))}`)
|
||||
@@ -480,7 +480,7 @@ export async function install(args: InstallArgs): Promise<number> {
|
||||
}
|
||||
|
||||
if (!config.hasClaude && !config.hasOpenAI && !config.hasGemini && !config.hasCopilot && !config.hasOpencodeZen) {
|
||||
p.log.warn("No model providers configured. Using opencode/glm-4.7-free as fallback.")
|
||||
p.log.warn("No model providers configured. Using opencode/big-pickle as fallback.")
|
||||
}
|
||||
|
||||
p.note(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
|
||||
|
||||
123
src/cli/mcp-oauth/index.test.ts
Normal file
123
src/cli/mcp-oauth/index.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { Command } from "commander"
|
||||
import { createMcpOAuthCommand } from "./index"
|
||||
|
||||
describe("mcp oauth command", () => {
|
||||
|
||||
describe("command structure", () => {
|
||||
it("creates mcp command group with oauth subcommand", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
|
||||
// when
|
||||
const subcommands = mcpCommand.commands.map((cmd: Command) => cmd.name())
|
||||
|
||||
// then
|
||||
expect(subcommands).toContain("oauth")
|
||||
})
|
||||
|
||||
it("oauth subcommand has login, logout, and status subcommands", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
|
||||
// when
|
||||
const subcommands = oauthCommand?.commands.map((cmd: Command) => cmd.name()) ?? []
|
||||
|
||||
// then
|
||||
expect(subcommands).toContain("login")
|
||||
expect(subcommands).toContain("logout")
|
||||
expect(subcommands).toContain("status")
|
||||
})
|
||||
})
|
||||
|
||||
describe("login subcommand", () => {
|
||||
it("exists and has description", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
const loginCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "login")
|
||||
|
||||
// when
|
||||
const description = loginCommand?.description() ?? ""
|
||||
|
||||
// then
|
||||
expect(loginCommand).toBeDefined()
|
||||
expect(description).toContain("OAuth")
|
||||
})
|
||||
|
||||
it("accepts --server-url option", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
const loginCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "login")
|
||||
|
||||
// when
|
||||
const options = loginCommand?.options ?? []
|
||||
const serverUrlOption = options.find((opt: { long?: string }) => opt.long === "--server-url")
|
||||
|
||||
// then
|
||||
expect(serverUrlOption).toBeDefined()
|
||||
})
|
||||
|
||||
it("accepts --client-id option", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
const loginCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "login")
|
||||
|
||||
// when
|
||||
const options = loginCommand?.options ?? []
|
||||
const clientIdOption = options.find((opt: { long?: string }) => opt.long === "--client-id")
|
||||
|
||||
// then
|
||||
expect(clientIdOption).toBeDefined()
|
||||
})
|
||||
|
||||
it("accepts --scopes option", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
const loginCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "login")
|
||||
|
||||
// when
|
||||
const options = loginCommand?.options ?? []
|
||||
const scopesOption = options.find((opt: { long?: string }) => opt.long === "--scopes")
|
||||
|
||||
// then
|
||||
expect(scopesOption).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("logout subcommand", () => {
|
||||
it("exists and has description", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
const logoutCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "logout")
|
||||
|
||||
// when
|
||||
const description = logoutCommand?.description() ?? ""
|
||||
|
||||
// then
|
||||
expect(logoutCommand).toBeDefined()
|
||||
expect(description).toContain("tokens")
|
||||
})
|
||||
})
|
||||
|
||||
describe("status subcommand", () => {
|
||||
it("exists and has description", () => {
|
||||
// given
|
||||
const mcpCommand = createMcpOAuthCommand()
|
||||
const oauthCommand = mcpCommand.commands.find((cmd: Command) => cmd.name() === "oauth")
|
||||
const statusCommand = oauthCommand?.commands.find((cmd: Command) => cmd.name() === "status")
|
||||
|
||||
// when
|
||||
const description = statusCommand?.description() ?? ""
|
||||
|
||||
// then
|
||||
expect(statusCommand).toBeDefined()
|
||||
expect(description).toContain("status")
|
||||
})
|
||||
})
|
||||
})
|
||||
43
src/cli/mcp-oauth/index.ts
Normal file
43
src/cli/mcp-oauth/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Command } from "commander"
|
||||
import { login } from "./login"
|
||||
import { logout } from "./logout"
|
||||
import { status } from "./status"
|
||||
|
||||
export function createMcpOAuthCommand(): Command {
|
||||
const mcp = new Command("mcp").description("MCP server management")
|
||||
|
||||
const oauth = new Command("oauth").description("OAuth token management for MCP servers")
|
||||
|
||||
oauth
|
||||
.command("login <server-name>")
|
||||
.description("Authenticate with an MCP server using OAuth")
|
||||
.option("--server-url <url>", "OAuth server URL (required if not in config)")
|
||||
.option("--client-id <id>", "OAuth client ID (optional, uses DCR if not provided)")
|
||||
.option("--scopes <scopes...>", "OAuth scopes to request")
|
||||
.action(async (serverName: string, options) => {
|
||||
const exitCode = await login(serverName, options)
|
||||
process.exit(exitCode)
|
||||
})
|
||||
|
||||
oauth
|
||||
.command("logout <server-name>")
|
||||
.description("Remove stored OAuth tokens for an MCP server")
|
||||
.option("--server-url <url>", "OAuth server URL (use if server name differs from URL)")
|
||||
.action(async (serverName: string, options) => {
|
||||
const exitCode = await logout(serverName, options)
|
||||
process.exit(exitCode)
|
||||
})
|
||||
|
||||
oauth
|
||||
.command("status [server-name]")
|
||||
.description("Show OAuth token status for MCP servers")
|
||||
.action(async (serverName: string | undefined) => {
|
||||
const exitCode = await status(serverName)
|
||||
process.exit(exitCode)
|
||||
})
|
||||
|
||||
mcp.addCommand(oauth)
|
||||
return mcp
|
||||
}
|
||||
|
||||
export { login, logout, status }
|
||||
80
src/cli/mcp-oauth/login.test.ts
Normal file
80
src/cli/mcp-oauth/login.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"
|
||||
|
||||
const mockLogin = mock(() => Promise.resolve({ accessToken: "test-token", expiresAt: 1710000000 }))
|
||||
|
||||
mock.module("../../features/mcp-oauth/provider", () => ({
|
||||
McpOAuthProvider: class MockMcpOAuthProvider {
|
||||
constructor(public options: { serverUrl: string; clientId?: string; scopes?: string[] }) {}
|
||||
async login() {
|
||||
return mockLogin()
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
const { login } = await import("./login")
|
||||
|
||||
describe("login command", () => {
|
||||
beforeEach(() => {
|
||||
mockLogin.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// cleanup
|
||||
})
|
||||
|
||||
it("returns error code when server-url is not provided", async () => {
|
||||
// given
|
||||
const serverName = "test-server"
|
||||
const options = {}
|
||||
|
||||
// when
|
||||
const exitCode = await login(serverName, options)
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(1)
|
||||
})
|
||||
|
||||
it("returns success code when login succeeds", async () => {
|
||||
// given
|
||||
const serverName = "test-server"
|
||||
const options = {
|
||||
serverUrl: "https://oauth.example.com",
|
||||
}
|
||||
|
||||
// when
|
||||
const exitCode = await login(serverName, options)
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(0)
|
||||
expect(mockLogin).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("returns error code when login throws", async () => {
|
||||
// given
|
||||
const serverName = "test-server"
|
||||
const options = {
|
||||
serverUrl: "https://oauth.example.com",
|
||||
}
|
||||
mockLogin.mockRejectedValueOnce(new Error("Network error"))
|
||||
|
||||
// when
|
||||
const exitCode = await login(serverName, options)
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(1)
|
||||
})
|
||||
|
||||
it("returns error code when server-url is missing", async () => {
|
||||
// given
|
||||
const serverName = "test-server"
|
||||
const options = {
|
||||
clientId: "test-client-id",
|
||||
}
|
||||
|
||||
// when
|
||||
const exitCode = await login(serverName, options)
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(1)
|
||||
})
|
||||
})
|
||||
38
src/cli/mcp-oauth/login.ts
Normal file
38
src/cli/mcp-oauth/login.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { McpOAuthProvider } from "../../features/mcp-oauth/provider"
|
||||
|
||||
export interface LoginOptions {
|
||||
serverUrl?: string
|
||||
clientId?: string
|
||||
scopes?: string[]
|
||||
}
|
||||
|
||||
export async function login(serverName: string, options: LoginOptions): Promise<number> {
|
||||
try {
|
||||
const serverUrl = options.serverUrl
|
||||
if (!serverUrl) {
|
||||
console.error(`Error: --server-url is required for server "${serverName}"`)
|
||||
return 1
|
||||
}
|
||||
|
||||
const provider = new McpOAuthProvider({
|
||||
serverUrl,
|
||||
clientId: options.clientId,
|
||||
scopes: options.scopes,
|
||||
})
|
||||
|
||||
console.log(`Authenticating with ${serverName}...`)
|
||||
const tokenData = await provider.login()
|
||||
|
||||
console.log(`✓ Successfully authenticated with ${serverName}`)
|
||||
if (tokenData.expiresAt) {
|
||||
const expiryDate = new Date(tokenData.expiresAt * 1000)
|
||||
console.log(` Token expires at: ${expiryDate.toISOString()}`)
|
||||
}
|
||||
|
||||
return 0
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error: Failed to authenticate with ${serverName}: ${message}`)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
65
src/cli/mcp-oauth/logout.test.ts
Normal file
65
src/cli/mcp-oauth/logout.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"
|
||||
import { existsSync, mkdirSync, rmSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
import { saveToken } from "../../features/mcp-oauth/storage"
|
||||
|
||||
const { logout } = await import("./logout")
|
||||
|
||||
describe("logout command", () => {
|
||||
const TEST_CONFIG_DIR = join(tmpdir(), "mcp-oauth-logout-test-" + Date.now())
|
||||
let originalConfigDir: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
originalConfigDir = process.env.OPENCODE_CONFIG_DIR
|
||||
process.env.OPENCODE_CONFIG_DIR = TEST_CONFIG_DIR
|
||||
if (!existsSync(TEST_CONFIG_DIR)) {
|
||||
mkdirSync(TEST_CONFIG_DIR, { recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (originalConfigDir === undefined) {
|
||||
delete process.env.OPENCODE_CONFIG_DIR
|
||||
} else {
|
||||
process.env.OPENCODE_CONFIG_DIR = originalConfigDir
|
||||
}
|
||||
if (existsSync(TEST_CONFIG_DIR)) {
|
||||
rmSync(TEST_CONFIG_DIR, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("returns success code when logout succeeds", async () => {
|
||||
// given
|
||||
const serverUrl = "https://test-server.example.com"
|
||||
saveToken(serverUrl, serverUrl, { accessToken: "test-token" })
|
||||
|
||||
// when
|
||||
const exitCode = await logout("test-server", { serverUrl })
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(0)
|
||||
})
|
||||
|
||||
it("handles non-existent server gracefully", async () => {
|
||||
// given
|
||||
const serverName = "non-existent-server"
|
||||
|
||||
// when
|
||||
const exitCode = await logout(serverName, { serverUrl: "https://nonexistent.example.com" })
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(0)
|
||||
})
|
||||
|
||||
it("returns error when --server-url is not provided", async () => {
|
||||
// given
|
||||
const serverName = "test-server"
|
||||
|
||||
// when
|
||||
const exitCode = await logout(serverName)
|
||||
|
||||
// then
|
||||
expect(exitCode).toBe(1)
|
||||
})
|
||||
})
|
||||
30
src/cli/mcp-oauth/logout.ts
Normal file
30
src/cli/mcp-oauth/logout.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { deleteToken } from "../../features/mcp-oauth/storage"
|
||||
|
||||
export interface LogoutOptions {
|
||||
serverUrl?: string
|
||||
}
|
||||
|
||||
export async function logout(serverName: string, options?: LogoutOptions): Promise<number> {
|
||||
try {
|
||||
const serverUrl = options?.serverUrl
|
||||
if (!serverUrl) {
|
||||
console.error(`Error: --server-url is required for logout. Token storage uses server URLs, not names.`)
|
||||
console.error(` Usage: mcp oauth logout ${serverName} --server-url https://your-server.example.com`)
|
||||
return 1
|
||||
}
|
||||
|
||||
const success = deleteToken(serverUrl, serverUrl)
|
||||
|
||||
if (success) {
|
||||
console.log(`✓ Successfully removed tokens for ${serverName}`)
|
||||
return 0
|
||||
}
|
||||
|
||||
console.error(`Error: Failed to remove tokens for ${serverName}`)
|
||||
return 1
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error: Failed to remove tokens for ${serverName}: ${message}`)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
48
src/cli/mcp-oauth/status.test.ts
Normal file
48
src/cli/mcp-oauth/status.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
||||
import { status } from "./status"
|
||||
|
||||
describe("status command", () => {
|
||||
beforeEach(() => {
|
||||
// setup
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// cleanup
|
||||
})
|
||||
|
||||
it("returns success code when checking status for specific server", async () => {
|
||||
// given
|
||||
const serverName = "test-server"
|
||||
|
||||
// when
|
||||
const exitCode = await status(serverName)
|
||||
|
||||
// then
|
||||
expect(typeof exitCode).toBe("number")
|
||||
expect(exitCode).toBe(0)
|
||||
})
|
||||
|
||||
it("returns success code when checking status for all servers", async () => {
|
||||
// given
|
||||
const serverName = undefined
|
||||
|
||||
// when
|
||||
const exitCode = await status(serverName)
|
||||
|
||||
// then
|
||||
expect(typeof exitCode).toBe("number")
|
||||
expect(exitCode).toBe(0)
|
||||
})
|
||||
|
||||
it("handles non-existent server gracefully", async () => {
|
||||
// given
|
||||
const serverName = "non-existent-server"
|
||||
|
||||
// when
|
||||
const exitCode = await status(serverName)
|
||||
|
||||
// then
|
||||
expect(typeof exitCode).toBe("number")
|
||||
expect(exitCode).toBe(0)
|
||||
})
|
||||
})
|
||||
50
src/cli/mcp-oauth/status.ts
Normal file
50
src/cli/mcp-oauth/status.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { listAllTokens, listTokensByHost } from "../../features/mcp-oauth/storage"
|
||||
|
||||
export async function status(serverName: string | undefined): Promise<number> {
|
||||
try {
|
||||
if (serverName) {
|
||||
const tokens = listTokensByHost(serverName)
|
||||
|
||||
if (Object.keys(tokens).length === 0) {
|
||||
console.log(`No tokens found for ${serverName}`)
|
||||
return 0
|
||||
}
|
||||
|
||||
console.log(`OAuth Status for ${serverName}:`)
|
||||
for (const [key, token] of Object.entries(tokens)) {
|
||||
console.log(` ${key}:`)
|
||||
console.log(` Access Token: [REDACTED]`)
|
||||
if (token.refreshToken) {
|
||||
console.log(` Refresh Token: [REDACTED]`)
|
||||
}
|
||||
if (token.expiresAt) {
|
||||
const expiryDate = new Date(token.expiresAt * 1000)
|
||||
const now = Date.now() / 1000
|
||||
const isExpired = token.expiresAt < now
|
||||
const tokenStatus = isExpired ? "EXPIRED" : "VALID"
|
||||
console.log(` Expiry: ${expiryDate.toISOString()} (${tokenStatus})`)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const tokens = listAllTokens()
|
||||
if (Object.keys(tokens).length === 0) {
|
||||
console.log("No OAuth tokens stored")
|
||||
return 0
|
||||
}
|
||||
|
||||
console.log("Stored OAuth Tokens:")
|
||||
for (const [key, token] of Object.entries(tokens)) {
|
||||
const isExpired = token.expiresAt && token.expiresAt < Date.now() / 1000
|
||||
const tokenStatus = isExpired ? "EXPIRED" : "VALID"
|
||||
console.log(` ${key}: ${tokenStatus}`)
|
||||
}
|
||||
|
||||
return 0
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error: Failed to get token status: ${message}`)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -310,15 +310,15 @@ describe("generateModelConfig", () => {
|
||||
})
|
||||
|
||||
describe("explore agent special cases", () => {
|
||||
test("explore uses grok-code when only Gemini available (no Claude)", () => {
|
||||
test("explore uses gpt-5-nano when only Gemini available (no Claude)", () => {
|
||||
// #given only Gemini is available (no Claude)
|
||||
const config = createConfig({ hasGemini: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should use grok-code (Claude haiku not available)
|
||||
expect(result.agents?.explore?.model).toBe("opencode/grok-code")
|
||||
// #then explore should use gpt-5-nano (Claude haiku not available)
|
||||
expect(result.agents?.explore?.model).toBe("opencode/gpt-5-nano")
|
||||
})
|
||||
|
||||
test("explore uses Claude haiku when Claude available", () => {
|
||||
@@ -343,15 +343,26 @@ describe("generateModelConfig", () => {
|
||||
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
|
||||
})
|
||||
|
||||
test("explore uses grok-code when only OpenAI available", () => {
|
||||
test("explore uses gpt-5-nano when only OpenAI available", () => {
|
||||
// #given only OpenAI is available
|
||||
const config = createConfig({ hasOpenAI: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should use grok-code (fallback)
|
||||
expect(result.agents?.explore?.model).toBe("opencode/grok-code")
|
||||
// #then explore should use gpt-5-nano (fallback)
|
||||
expect(result.agents?.explore?.model).toBe("opencode/gpt-5-nano")
|
||||
})
|
||||
|
||||
test("explore uses gpt-5-mini when only Copilot available", () => {
|
||||
// #given only Copilot is available
|
||||
const config = createConfig({ hasCopilot: true })
|
||||
|
||||
// #when generateModelConfig is called
|
||||
const result = generateModelConfig(config)
|
||||
|
||||
// #then explore should use gpt-5-mini (Copilot fallback)
|
||||
expect(result.agents?.explore?.model).toBe("github-copilot/gpt-5-mini")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export interface GeneratedOmoConfig {
|
||||
|
||||
const ZAI_MODEL = "zai-coding-plan/glm-4.7"
|
||||
|
||||
const ULTIMATE_FALLBACK = "opencode/glm-4.7-free"
|
||||
const ULTIMATE_FALLBACK = "opencode/big-pickle"
|
||||
const SCHEMA_URL = "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json"
|
||||
|
||||
function toProviderAvailability(config: InstallConfig): ProviderAvailability {
|
||||
@@ -103,7 +103,7 @@ function getSisyphusFallbackChain(isMaxPlan: boolean): FallbackEntry[] {
|
||||
return [
|
||||
{ providers: ["anthropic", "github-copilot", "opencode"], model: "claude-sonnet-4-5" },
|
||||
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2", variant: "high" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro-preview" },
|
||||
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -139,14 +139,16 @@ export function generateModelConfig(config: InstallConfig): GeneratedOmoConfig {
|
||||
continue
|
||||
}
|
||||
|
||||
// Special case: explore uses Claude haiku → OpenCode grok-code
|
||||
// Special case: explore uses Claude haiku → GitHub Copilot gpt-5-mini → OpenCode gpt-5-nano
|
||||
if (role === "explore") {
|
||||
if (avail.native.claude) {
|
||||
agents[role] = { model: "anthropic/claude-haiku-4-5" }
|
||||
} else if (avail.opencodeZen) {
|
||||
agents[role] = { model: "opencode/claude-haiku-4-5" }
|
||||
} else if (avail.copilot) {
|
||||
agents[role] = { model: "github-copilot/gpt-5-mini" }
|
||||
} else {
|
||||
agents[role] = { model: "opencode/grok-code" }
|
||||
agents[role] = { model: "opencode/gpt-5-nano" }
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -31,8 +31,18 @@ export async function run(options: RunOptions): Promise<number> {
|
||||
}
|
||||
|
||||
try {
|
||||
// Support custom OpenCode server port via environment variable
|
||||
// This allows Open Agent and other orchestrators to run multiple
|
||||
// concurrent missions without port conflicts
|
||||
const serverPort = process.env.OPENCODE_SERVER_PORT
|
||||
? parseInt(process.env.OPENCODE_SERVER_PORT, 10)
|
||||
: undefined
|
||||
const serverHostname = process.env.OPENCODE_SERVER_HOSTNAME || undefined
|
||||
|
||||
const { client, server } = await createOpencode({
|
||||
signal: abortController.signal,
|
||||
...(serverPort && !isNaN(serverPort) ? { port: serverPort } : {}),
|
||||
...(serverHostname ? { hostname: serverHostname } : {}),
|
||||
})
|
||||
|
||||
const cleanup = () => {
|
||||
|
||||
@@ -9,6 +9,8 @@ export {
|
||||
SisyphusAgentConfigSchema,
|
||||
ExperimentalConfigSchema,
|
||||
RalphLoopConfigSchema,
|
||||
TmuxConfigSchema,
|
||||
TmuxLayoutSchema,
|
||||
} from "./schema"
|
||||
|
||||
export type {
|
||||
@@ -23,4 +25,6 @@ export type {
|
||||
ExperimentalConfig,
|
||||
DynamicContextPruningConfig,
|
||||
RalphLoopConfig,
|
||||
TmuxConfig,
|
||||
TmuxLayout,
|
||||
} from "./schema"
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { AgentOverrideConfigSchema, BuiltinCategoryNameSchema, CategoryConfigSchema, OhMyOpenCodeConfigSchema } from "./schema"
|
||||
import {
|
||||
AgentOverrideConfigSchema,
|
||||
BrowserAutomationConfigSchema,
|
||||
BrowserAutomationProviderSchema,
|
||||
BuiltinCategoryNameSchema,
|
||||
CategoryConfigSchema,
|
||||
OhMyOpenCodeConfigSchema,
|
||||
} from "./schema"
|
||||
|
||||
describe("disabled_mcps schema", () => {
|
||||
test("should accept built-in MCP names", () => {
|
||||
@@ -345,6 +352,20 @@ describe("CategoryConfigSchema", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("accepts reasoningEffort as optional string with xhigh", () => {
|
||||
// #given
|
||||
const config = { reasoningEffort: "xhigh" }
|
||||
|
||||
// #when
|
||||
const result = CategoryConfigSchema.safeParse(config)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.reasoningEffort).toBe("xhigh")
|
||||
}
|
||||
})
|
||||
|
||||
test("rejects non-string variant", () => {
|
||||
// #given
|
||||
const config = { model: "openai/gpt-5.2", variant: 123 }
|
||||
@@ -494,3 +515,94 @@ describe("Sisyphus-Junior agent override", () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("BrowserAutomationProviderSchema", () => {
|
||||
test("accepts 'playwright' as valid provider", () => {
|
||||
// #given
|
||||
const input = "playwright"
|
||||
|
||||
// #when
|
||||
const result = BrowserAutomationProviderSchema.safeParse(input)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).toBe("playwright")
|
||||
})
|
||||
|
||||
test("accepts 'agent-browser' as valid provider", () => {
|
||||
// #given
|
||||
const input = "agent-browser"
|
||||
|
||||
// #when
|
||||
const result = BrowserAutomationProviderSchema.safeParse(input)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).toBe("agent-browser")
|
||||
})
|
||||
|
||||
test("rejects invalid provider", () => {
|
||||
// #given
|
||||
const input = "invalid-provider"
|
||||
|
||||
// #when
|
||||
const result = BrowserAutomationProviderSchema.safeParse(input)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("BrowserAutomationConfigSchema", () => {
|
||||
test("defaults provider to 'playwright' when not specified", () => {
|
||||
// #given
|
||||
const input = {}
|
||||
|
||||
// #when
|
||||
const result = BrowserAutomationConfigSchema.parse(input)
|
||||
|
||||
// #then
|
||||
expect(result.provider).toBe("playwright")
|
||||
})
|
||||
|
||||
test("accepts agent-browser provider", () => {
|
||||
// #given
|
||||
const input = { provider: "agent-browser" }
|
||||
|
||||
// #when
|
||||
const result = BrowserAutomationConfigSchema.parse(input)
|
||||
|
||||
// #then
|
||||
expect(result.provider).toBe("agent-browser")
|
||||
})
|
||||
})
|
||||
|
||||
describe("OhMyOpenCodeConfigSchema - browser_automation_engine", () => {
|
||||
test("accepts browser_automation_engine config", () => {
|
||||
// #given
|
||||
const input = {
|
||||
browser_automation_engine: {
|
||||
provider: "agent-browser",
|
||||
},
|
||||
}
|
||||
|
||||
// #when
|
||||
const result = OhMyOpenCodeConfigSchema.safeParse(input)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data?.browser_automation_engine?.provider).toBe("agent-browser")
|
||||
})
|
||||
|
||||
test("accepts config without browser_automation_engine", () => {
|
||||
// #given
|
||||
const input = {}
|
||||
|
||||
// #when
|
||||
const result = OhMyOpenCodeConfigSchema.safeParse(input)
|
||||
|
||||
// #then
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data?.browser_automation_engine).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,6 +30,7 @@ export const BuiltinAgentNameSchema = z.enum([
|
||||
|
||||
export const BuiltinSkillNameSchema = z.enum([
|
||||
"playwright",
|
||||
"agent-browser",
|
||||
"frontend-ui-ux",
|
||||
"git-master",
|
||||
])
|
||||
@@ -76,6 +77,7 @@ export const HookNameSchema = z.enum([
|
||||
|
||||
"thinking-block-validator",
|
||||
"ralph-loop",
|
||||
"category-skill-reminder",
|
||||
|
||||
"compaction-context-injector",
|
||||
"claude-code-hooks",
|
||||
@@ -83,6 +85,7 @@ export const HookNameSchema = z.enum([
|
||||
"edit-error-recovery",
|
||||
"delegate-task-retry",
|
||||
"prometheus-md-only",
|
||||
"sisyphus-junior-notepad",
|
||||
"start-work",
|
||||
"atlas",
|
||||
])
|
||||
@@ -113,6 +116,19 @@ export const AgentOverrideConfigSchema = z.object({
|
||||
.regex(/^#[0-9A-Fa-f]{6}$/)
|
||||
.optional(),
|
||||
permission: AgentPermissionSchema.optional(),
|
||||
/** Maximum tokens for response. Passed directly to OpenCode SDK. */
|
||||
maxTokens: z.number().optional(),
|
||||
/** Extended thinking configuration (Anthropic). Overrides category and default settings. */
|
||||
thinking: z.object({
|
||||
type: z.enum(["enabled", "disabled"]),
|
||||
budgetTokens: z.number().optional(),
|
||||
}).optional(),
|
||||
/** Reasoning effort level (OpenAI). Overrides category and default settings. */
|
||||
reasoningEffort: z.enum(["low", "medium", "high", "xhigh"]).optional(),
|
||||
/** Text verbosity level. */
|
||||
textVerbosity: z.enum(["low", "medium", "high"]).optional(),
|
||||
/** Provider-specific options. Passed directly to OpenCode SDK. */
|
||||
providerOptions: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export const AgentOverridesSchema = z.object({
|
||||
@@ -160,7 +176,7 @@ export const CategoryConfigSchema = z.object({
|
||||
type: z.enum(["enabled", "disabled"]),
|
||||
budgetTokens: z.number().optional(),
|
||||
}).optional(),
|
||||
reasoningEffort: z.enum(["low", "medium", "high"]).optional(),
|
||||
reasoningEffort: z.enum(["low", "medium", "high", "xhigh"]).optional(),
|
||||
textVerbosity: z.enum(["low", "medium", "high"]).optional(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
prompt_append: z.string().optional(),
|
||||
@@ -297,6 +313,56 @@ export const GitMasterConfigSchema = z.object({
|
||||
include_co_authored_by: z.boolean().default(true),
|
||||
})
|
||||
|
||||
export const BrowserAutomationProviderSchema = z.enum(["playwright", "agent-browser", "dev-browser"])
|
||||
|
||||
export const BrowserAutomationConfigSchema = z.object({
|
||||
/**
|
||||
* Browser automation provider to use for the "playwright" skill.
|
||||
* - "playwright": Uses Playwright MCP server (@playwright/mcp) - default
|
||||
* - "agent-browser": Uses Vercel's agent-browser CLI (requires: bun add -g agent-browser)
|
||||
* - "dev-browser": Uses dev-browser skill with persistent browser state
|
||||
*/
|
||||
provider: BrowserAutomationProviderSchema.default("playwright"),
|
||||
})
|
||||
|
||||
export const TmuxLayoutSchema = z.enum([
|
||||
'main-horizontal', // main pane top, agent panes bottom stack
|
||||
'main-vertical', // main pane left, agent panes right stack (default)
|
||||
'tiled', // all panes same size grid
|
||||
'even-horizontal', // all panes horizontal row
|
||||
'even-vertical', // all panes vertical stack
|
||||
])
|
||||
|
||||
export const TmuxConfigSchema = z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
layout: TmuxLayoutSchema.default('main-vertical'),
|
||||
main_pane_size: z.number().min(20).max(80).default(60),
|
||||
main_pane_min_width: z.number().min(40).default(120),
|
||||
agent_pane_min_width: z.number().min(20).default(40),
|
||||
})
|
||||
|
||||
export const SisyphusTasksConfigSchema = z.object({
|
||||
/** Enable Sisyphus Tasks system (default: false) */
|
||||
enabled: z.boolean().default(false),
|
||||
/** Storage path for tasks (default: .sisyphus/tasks) */
|
||||
storage_path: z.string().default(".sisyphus/tasks"),
|
||||
/** Enable Claude Code path compatibility mode */
|
||||
claude_code_compat: z.boolean().default(false),
|
||||
})
|
||||
|
||||
export const SisyphusSwarmConfigSchema = z.object({
|
||||
/** Enable Sisyphus Swarm system (default: false) */
|
||||
enabled: z.boolean().default(false),
|
||||
/** Storage path for teams (default: .sisyphus/teams) */
|
||||
storage_path: z.string().default(".sisyphus/teams"),
|
||||
/** UI mode: toast notifications, tmux panes, or both */
|
||||
ui_mode: z.enum(["toast", "tmux", "both"]).default("toast"),
|
||||
})
|
||||
|
||||
export const SisyphusConfigSchema = z.object({
|
||||
tasks: SisyphusTasksConfigSchema.optional(),
|
||||
swarm: SisyphusSwarmConfigSchema.optional(),
|
||||
})
|
||||
export const OhMyOpenCodeConfigSchema = z.object({
|
||||
$schema: z.string().optional(),
|
||||
disabled_mcps: z.array(AnyMcpNameSchema).optional(),
|
||||
@@ -316,6 +382,9 @@ export const OhMyOpenCodeConfigSchema = z.object({
|
||||
background_task: BackgroundTaskConfigSchema.optional(),
|
||||
notification: NotificationConfigSchema.optional(),
|
||||
git_master: GitMasterConfigSchema.optional(),
|
||||
browser_automation_engine: BrowserAutomationConfigSchema.optional(),
|
||||
tmux: TmuxConfigSchema.optional(),
|
||||
sisyphus: SisyphusConfigSchema.optional(),
|
||||
})
|
||||
|
||||
export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>
|
||||
@@ -338,5 +407,12 @@ export type CategoryConfig = z.infer<typeof CategoryConfigSchema>
|
||||
export type CategoriesConfig = z.infer<typeof CategoriesConfigSchema>
|
||||
export type BuiltinCategoryName = z.infer<typeof BuiltinCategoryNameSchema>
|
||||
export type GitMasterConfig = z.infer<typeof GitMasterConfigSchema>
|
||||
export type BrowserAutomationProvider = z.infer<typeof BrowserAutomationProviderSchema>
|
||||
export type BrowserAutomationConfig = z.infer<typeof BrowserAutomationConfigSchema>
|
||||
export type TmuxConfig = z.infer<typeof TmuxConfigSchema>
|
||||
export type TmuxLayout = z.infer<typeof TmuxLayoutSchema>
|
||||
export type SisyphusTasksConfig = z.infer<typeof SisyphusTasksConfigSchema>
|
||||
export type SisyphusSwarmConfig = z.infer<typeof SisyphusSwarmConfigSchema>
|
||||
export type SisyphusConfig = z.infer<typeof SisyphusConfigSchema>
|
||||
|
||||
export { AnyMcpNameSchema, type AnyMcpName, McpNameSchema, type McpName } from "../mcp/types"
|
||||
|
||||
@@ -2,31 +2,31 @@
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
Core feature modules + Claude Code compatibility layer. Background agents, skill MCP, builtin skills/commands, 5 loaders.
|
||||
Core feature modules + Claude Code compatibility layer. Orchestrates background agents, skill MCPs, builtin skills/commands, and 16 feature modules.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
features/
|
||||
├── background-agent/ # Task lifecycle (1335 lines)
|
||||
├── background-agent/ # Task lifecycle (1377 lines)
|
||||
│ ├── manager.ts # Launch → poll → complete
|
||||
│ ├── concurrency.ts # Per-provider limits
|
||||
│ └── types.ts # BackgroundTask, LaunchInput
|
||||
├── skill-mcp-manager/ # MCP client lifecycle
|
||||
│ ├── manager.ts # Lazy loading, cleanup
|
||||
│ └── types.ts # SkillMcpConfig
|
||||
├── builtin-skills/ # Playwright, git-master, frontend-ui-ux
|
||||
│ └── skills.ts # 1203 lines
|
||||
├── builtin-commands/ # ralph-loop, refactor, init-deep
|
||||
│ └── concurrency.ts # Per-provider limits
|
||||
├── builtin-skills/ # Core skills (1729 lines)
|
||||
│ └── skills.ts # agent-browser, dev-browser, frontend-ui-ux, git-master, typescript-programmer
|
||||
├── builtin-commands/ # ralph-loop, refactor, ulw-loop, init-deep, start-work, cancel-ralph
|
||||
├── claude-code-agent-loader/ # ~/.claude/agents/*.md
|
||||
├── claude-code-command-loader/ # ~/.claude/commands/*.md
|
||||
├── claude-code-mcp-loader/ # .mcp.json
|
||||
├── claude-code-mcp-loader/ # .mcp.json with ${VAR} expansion
|
||||
├── claude-code-plugin-loader/ # installed_plugins.json
|
||||
├── claude-code-session-state/ # Session persistence
|
||||
├── opencode-skill-loader/ # Skills from 6 directories
|
||||
├── context-injector/ # AGENTS.md/README.md injection
|
||||
├── boulder-state/ # Todo state persistence
|
||||
└── hook-message-injector/ # Message injection
|
||||
├── hook-message-injector/ # Message injection
|
||||
├── task-toast-manager/ # Background task notifications
|
||||
├── skill-mcp-manager/ # MCP client lifecycle (520 lines)
|
||||
├── tmux-subagent/ # Tmux session management
|
||||
└── ... (16 modules total)
|
||||
```
|
||||
|
||||
## LOADER PRIORITY
|
||||
@@ -41,8 +41,9 @@ features/
|
||||
|
||||
- **Lifecycle**: `launch` → `poll` (2s) → `complete`
|
||||
- **Stability**: 3 consecutive polls = idle
|
||||
- **Concurrency**: Per-provider/model limits
|
||||
- **Concurrency**: Per-provider/model limits via `ConcurrencyManager`
|
||||
- **Cleanup**: 30m TTL, 3m stale timeout
|
||||
- **State**: Per-session Maps, cleaned on `session.deleted`
|
||||
|
||||
## SKILL MCP
|
||||
|
||||
@@ -55,3 +56,4 @@ features/
|
||||
- **Sequential delegation**: Use `delegate_task` parallel
|
||||
- **Trust self-reports**: ALWAYS verify
|
||||
- **Main thread blocks**: No heavy I/O in loader init
|
||||
- **Direct state mutation**: Use managers for boulder/session state
|
||||
|
||||
@@ -170,6 +170,7 @@ function createBackgroundManager(): BackgroundManager {
|
||||
const client = {
|
||||
session: {
|
||||
prompt: async () => ({}),
|
||||
abort: async () => ({}),
|
||||
},
|
||||
}
|
||||
return new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||
@@ -776,7 +777,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
parentModel: { providerID: "old", modelID: "old-model" },
|
||||
}
|
||||
const currentMessage: CurrentMessage = {
|
||||
agent: "Sisyphus",
|
||||
agent: "sisyphus",
|
||||
model: { providerID: "anthropic", modelID: "claude-opus-4-5" },
|
||||
}
|
||||
|
||||
@@ -784,7 +785,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
const promptBody = buildNotificationPromptBody(task, currentMessage)
|
||||
|
||||
// #then - uses currentMessage values, not task.parentModel/parentAgent
|
||||
expect(promptBody.agent).toBe("Sisyphus")
|
||||
expect(promptBody.agent).toBe("sisyphus")
|
||||
expect(promptBody.model).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-5" })
|
||||
})
|
||||
|
||||
@@ -827,11 +828,11 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
status: "completed",
|
||||
startedAt: new Date(),
|
||||
completedAt: new Date(),
|
||||
parentAgent: "Sisyphus",
|
||||
parentAgent: "sisyphus",
|
||||
parentModel: { providerID: "anthropic", modelID: "claude-opus" },
|
||||
}
|
||||
const currentMessage: CurrentMessage = {
|
||||
agent: "Sisyphus",
|
||||
agent: "sisyphus",
|
||||
model: { providerID: "anthropic" },
|
||||
}
|
||||
|
||||
@@ -839,7 +840,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
const promptBody = buildNotificationPromptBody(task, currentMessage)
|
||||
|
||||
// #then - model not passed due to incomplete data
|
||||
expect(promptBody.agent).toBe("Sisyphus")
|
||||
expect(promptBody.agent).toBe("sisyphus")
|
||||
expect("model" in promptBody).toBe(false)
|
||||
})
|
||||
|
||||
@@ -856,7 +857,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
status: "completed",
|
||||
startedAt: new Date(),
|
||||
completedAt: new Date(),
|
||||
parentAgent: "Sisyphus",
|
||||
parentAgent: "sisyphus",
|
||||
parentModel: { providerID: "anthropic", modelID: "claude-opus" },
|
||||
}
|
||||
|
||||
@@ -864,7 +865,7 @@ describe("BackgroundManager.notifyParentSession - dynamic message lookup", () =>
|
||||
const promptBody = buildNotificationPromptBody(task, null)
|
||||
|
||||
// #then - falls back to task.parentAgent, no model
|
||||
expect(promptBody.agent).toBe("Sisyphus")
|
||||
expect(promptBody.agent).toBe("sisyphus")
|
||||
expect("model" in promptBody).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1053,6 +1054,7 @@ describe("BackgroundManager.resume model persistence", () => {
|
||||
promptCalls.push(args)
|
||||
return {}
|
||||
},
|
||||
abort: async () => ({}),
|
||||
},
|
||||
}
|
||||
manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||
@@ -1926,3 +1928,162 @@ describe("BackgroundManager.checkAndInterruptStaleTasks", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("BackgroundManager.shutdown session abort", () => {
|
||||
test("should call session.abort for all running tasks during shutdown", () => {
|
||||
// #given
|
||||
const abortedSessionIDs: string[] = []
|
||||
const client = {
|
||||
session: {
|
||||
prompt: async () => ({}),
|
||||
abort: async (args: { path: { id: string } }) => {
|
||||
abortedSessionIDs.push(args.path.id)
|
||||
return {}
|
||||
},
|
||||
},
|
||||
}
|
||||
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||
|
||||
const task1: BackgroundTask = {
|
||||
id: "task-1",
|
||||
sessionID: "session-1",
|
||||
parentSessionID: "parent-1",
|
||||
parentMessageID: "msg-1",
|
||||
description: "Running task 1",
|
||||
prompt: "Test",
|
||||
agent: "test-agent",
|
||||
status: "running",
|
||||
startedAt: new Date(),
|
||||
}
|
||||
const task2: BackgroundTask = {
|
||||
id: "task-2",
|
||||
sessionID: "session-2",
|
||||
parentSessionID: "parent-2",
|
||||
parentMessageID: "msg-2",
|
||||
description: "Running task 2",
|
||||
prompt: "Test",
|
||||
agent: "test-agent",
|
||||
status: "running",
|
||||
startedAt: new Date(),
|
||||
}
|
||||
|
||||
getTaskMap(manager).set(task1.id, task1)
|
||||
getTaskMap(manager).set(task2.id, task2)
|
||||
|
||||
// #when
|
||||
manager.shutdown()
|
||||
|
||||
// #then
|
||||
expect(abortedSessionIDs).toContain("session-1")
|
||||
expect(abortedSessionIDs).toContain("session-2")
|
||||
expect(abortedSessionIDs).toHaveLength(2)
|
||||
})
|
||||
|
||||
test("should not call session.abort for completed or cancelled tasks", () => {
|
||||
// #given
|
||||
const abortedSessionIDs: string[] = []
|
||||
const client = {
|
||||
session: {
|
||||
prompt: async () => ({}),
|
||||
abort: async (args: { path: { id: string } }) => {
|
||||
abortedSessionIDs.push(args.path.id)
|
||||
return {}
|
||||
},
|
||||
},
|
||||
}
|
||||
const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput)
|
||||
|
||||
const completedTask: BackgroundTask = {
|
||||
id: "task-completed",
|
||||
sessionID: "session-completed",
|
||||
parentSessionID: "parent-1",
|
||||
parentMessageID: "msg-1",
|
||||
description: "Completed task",
|
||||
prompt: "Test",
|
||||
agent: "test-agent",
|
||||
status: "completed",
|
||||
startedAt: new Date(),
|
||||
completedAt: new Date(),
|
||||
}
|
||||
const cancelledTask: BackgroundTask = {
|
||||
id: "task-cancelled",
|
||||
sessionID: "session-cancelled",
|
||||
parentSessionID: "parent-2",
|
||||
parentMessageID: "msg-2",
|
||||
description: "Cancelled task",
|
||||
prompt: "Test",
|
||||
agent: "test-agent",
|
||||
status: "cancelled",
|
||||
startedAt: new Date(),
|
||||
completedAt: new Date(),
|
||||
}
|
||||
const pendingTask: BackgroundTask = {
|
||||
id: "task-pending",
|
||||
parentSessionID: "parent-3",
|
||||
parentMessageID: "msg-3",
|
||||
description: "Pending task",
|
||||
prompt: "Test",
|
||||
agent: "test-agent",
|
||||
status: "pending",
|
||||
queuedAt: new Date(),
|
||||
}
|
||||
|
||||
getTaskMap(manager).set(completedTask.id, completedTask)
|
||||
getTaskMap(manager).set(cancelledTask.id, cancelledTask)
|
||||
getTaskMap(manager).set(pendingTask.id, pendingTask)
|
||||
|
||||
// #when
|
||||
manager.shutdown()
|
||||
|
||||
// #then
|
||||
expect(abortedSessionIDs).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("should call onShutdown callback during shutdown", () => {
|
||||
// #given
|
||||
let shutdownCalled = false
|
||||
const client = {
|
||||
session: {
|
||||
prompt: async () => ({}),
|
||||
abort: async () => ({}),
|
||||
},
|
||||
}
|
||||
const manager = new BackgroundManager(
|
||||
{ client, directory: tmpdir() } as unknown as PluginInput,
|
||||
undefined,
|
||||
{
|
||||
onShutdown: () => {
|
||||
shutdownCalled = true
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// #when
|
||||
manager.shutdown()
|
||||
|
||||
// #then
|
||||
expect(shutdownCalled).toBe(true)
|
||||
})
|
||||
|
||||
test("should not throw when onShutdown callback throws", () => {
|
||||
// #given
|
||||
const client = {
|
||||
session: {
|
||||
prompt: async () => ({}),
|
||||
abort: async () => ({}),
|
||||
},
|
||||
}
|
||||
const manager = new BackgroundManager(
|
||||
{ client, directory: tmpdir() } as unknown as PluginInput,
|
||||
undefined,
|
||||
{
|
||||
onShutdown: () => {
|
||||
throw new Error("cleanup failed")
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// #when / #then
|
||||
expect(() => manager.shutdown()).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import type {
|
||||
} from "./types"
|
||||
import { log, getAgentToolRestrictions } from "../../shared"
|
||||
import { ConcurrencyManager } from "./concurrency"
|
||||
import type { BackgroundTaskConfig } from "../../config/schema"
|
||||
import type { BackgroundTaskConfig, TmuxConfig } from "../../config/schema"
|
||||
import { isInsideTmux } from "../../shared/tmux"
|
||||
|
||||
import { subagentSessions } from "../claude-code-session-state"
|
||||
import { getTaskToastManager } from "../task-toast-manager"
|
||||
@@ -54,6 +55,14 @@ interface QueueItem {
|
||||
input: LaunchInput
|
||||
}
|
||||
|
||||
export interface SubagentSessionCreatedEvent {
|
||||
sessionID: string
|
||||
parentID: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export type OnSubagentSessionCreated = (event: SubagentSessionCreatedEvent) => Promise<void>
|
||||
|
||||
export class BackgroundManager {
|
||||
private static cleanupManagers = new Set<BackgroundManager>()
|
||||
private static cleanupRegistered = false
|
||||
@@ -68,12 +77,22 @@ export class BackgroundManager {
|
||||
private concurrencyManager: ConcurrencyManager
|
||||
private shutdownTriggered = false
|
||||
private config?: BackgroundTaskConfig
|
||||
|
||||
private tmuxEnabled: boolean
|
||||
private onSubagentSessionCreated?: OnSubagentSessionCreated
|
||||
private onShutdown?: () => void
|
||||
|
||||
private queuesByKey: Map<string, QueueItem[]> = new Map()
|
||||
private processingKeys: Set<string> = new Set()
|
||||
|
||||
constructor(ctx: PluginInput, config?: BackgroundTaskConfig) {
|
||||
constructor(
|
||||
ctx: PluginInput,
|
||||
config?: BackgroundTaskConfig,
|
||||
options?: {
|
||||
tmuxConfig?: TmuxConfig
|
||||
onSubagentSessionCreated?: OnSubagentSessionCreated
|
||||
onShutdown?: () => void
|
||||
}
|
||||
) {
|
||||
this.tasks = new Map()
|
||||
this.notifications = new Map()
|
||||
this.pendingByParent = new Map()
|
||||
@@ -81,6 +100,9 @@ export class BackgroundManager {
|
||||
this.directory = ctx.directory
|
||||
this.concurrencyManager = new ConcurrencyManager(config)
|
||||
this.config = config
|
||||
this.tmuxEnabled = options?.tmuxConfig?.enabled ?? false
|
||||
this.onSubagentSessionCreated = options?.onSubagentSessionCreated
|
||||
this.onShutdown = options?.onShutdown
|
||||
this.registerProcessCleanup()
|
||||
}
|
||||
|
||||
@@ -205,7 +227,10 @@ export class BackgroundManager {
|
||||
body: {
|
||||
parentID: input.parentSessionID,
|
||||
title: `Background: ${input.description}`,
|
||||
},
|
||||
permission: [
|
||||
{ permission: "question", action: "deny" as const, pattern: "*" },
|
||||
],
|
||||
} as any,
|
||||
query: {
|
||||
directory: parentDirectory,
|
||||
},
|
||||
@@ -222,6 +247,29 @@ export class BackgroundManager {
|
||||
const sessionID = createResult.data.id
|
||||
subagentSessions.add(sessionID)
|
||||
|
||||
log("[background-agent] tmux callback check", {
|
||||
hasCallback: !!this.onSubagentSessionCreated,
|
||||
tmuxEnabled: this.tmuxEnabled,
|
||||
isInsideTmux: isInsideTmux(),
|
||||
sessionID,
|
||||
parentID: input.parentSessionID,
|
||||
})
|
||||
|
||||
if (this.onSubagentSessionCreated && this.tmuxEnabled && isInsideTmux()) {
|
||||
log("[background-agent] Invoking tmux callback NOW", { sessionID })
|
||||
await this.onSubagentSessionCreated({
|
||||
sessionID,
|
||||
parentID: input.parentSessionID,
|
||||
title: input.description,
|
||||
}).catch((err) => {
|
||||
log("[background-agent] Failed to spawn tmux pane:", err)
|
||||
})
|
||||
log("[background-agent] tmux callback completed, waiting 200ms")
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
} else {
|
||||
log("[background-agent] SKIP tmux callback - conditions not met")
|
||||
}
|
||||
|
||||
// Update task to running state
|
||||
task.status = "running"
|
||||
task.startedAt = new Date()
|
||||
@@ -252,17 +300,26 @@ export class BackgroundManager {
|
||||
|
||||
// Use prompt() instead of promptAsync() to properly initialize agent loop (fire-and-forget)
|
||||
// Include model if caller provided one (e.g., from Sisyphus category configs)
|
||||
// IMPORTANT: variant must be a top-level field in the body, NOT nested inside model
|
||||
// OpenCode's PromptInput schema expects: { model: { providerID, modelID }, variant: "max" }
|
||||
const launchModel = input.model
|
||||
? { providerID: input.model.providerID, modelID: input.model.modelID }
|
||||
: undefined
|
||||
const launchVariant = input.model?.variant
|
||||
|
||||
this.client.session.prompt({
|
||||
path: { id: sessionID },
|
||||
body: {
|
||||
agent: input.agent,
|
||||
...(input.model ? { model: input.model } : {}),
|
||||
...(launchModel ? { model: launchModel } : {}),
|
||||
...(launchVariant ? { variant: launchVariant } : {}),
|
||||
system: input.skillContent,
|
||||
tools: {
|
||||
...getAgentToolRestrictions(input.agent),
|
||||
task: false,
|
||||
delegate_task: false,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
},
|
||||
parts: [{ type: "text", text: input.prompt }],
|
||||
},
|
||||
@@ -499,16 +556,24 @@ export class BackgroundManager {
|
||||
|
||||
// Use prompt() instead of promptAsync() to properly initialize agent loop
|
||||
// Include model if task has one (preserved from original launch with category config)
|
||||
// variant must be top-level in body, not nested inside model (OpenCode PromptInput schema)
|
||||
const resumeModel = existingTask.model
|
||||
? { providerID: existingTask.model.providerID, modelID: existingTask.model.modelID }
|
||||
: undefined
|
||||
const resumeVariant = existingTask.model?.variant
|
||||
|
||||
this.client.session.prompt({
|
||||
path: { id: existingTask.sessionID },
|
||||
body: {
|
||||
agent: existingTask.agent,
|
||||
...(existingTask.model ? { model: existingTask.model } : {}),
|
||||
...(resumeModel ? { model: resumeModel } : {}),
|
||||
...(resumeVariant ? { variant: resumeVariant } : {}),
|
||||
tools: {
|
||||
...getAgentToolRestrictions(existingTask.agent),
|
||||
task: false,
|
||||
delegate_task: false,
|
||||
call_omo_agent: true,
|
||||
question: false,
|
||||
},
|
||||
parts: [{ type: "text", text: input.prompt }],
|
||||
},
|
||||
@@ -1284,7 +1349,25 @@ Use \`background_output(task_id="${task.id}")\` to retrieve this result when rea
|
||||
log("[background-agent] Shutting down BackgroundManager")
|
||||
this.stopPolling()
|
||||
|
||||
// Release concurrency for all running tasks first
|
||||
// Abort all running sessions to prevent zombie processes (#1240)
|
||||
for (const task of this.tasks.values()) {
|
||||
if (task.status === "running" && task.sessionID) {
|
||||
this.client.session.abort({
|
||||
path: { id: task.sessionID },
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
// Notify shutdown listeners (e.g., tmux cleanup)
|
||||
if (this.onShutdown) {
|
||||
try {
|
||||
this.onShutdown()
|
||||
} catch (error) {
|
||||
log("[background-agent] Error in onShutdown callback:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Release concurrency for all running tasks
|
||||
for (const task of this.tasks.values()) {
|
||||
if (task.concurrencyKey) {
|
||||
this.concurrencyManager.release(task.concurrencyKey)
|
||||
|
||||
@@ -55,7 +55,6 @@ ${REFACTOR_TEMPLATE}
|
||||
},
|
||||
"start-work": {
|
||||
description: "(builtin) Start Sisyphus work session from Prometheus plan",
|
||||
agent: "Atlas",
|
||||
template: `<command-instruction>
|
||||
${START_WORK_TEMPLATE}
|
||||
</command-instruction>
|
||||
@@ -81,7 +80,7 @@ export function loadBuiltinCommands(
|
||||
for (const [name, definition] of Object.entries(BUILTIN_COMMAND_DEFINITIONS)) {
|
||||
if (!disabled.has(name as BuiltinCommandName)) {
|
||||
const { argumentHint: _argumentHint, ...openCodeCompatible } = definition
|
||||
commands[name] = openCodeCompatible as CommandDefinition
|
||||
commands[name] = { ...openCodeCompatible, name } as CommandDefinition
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const RALPH_LOOP_TEMPLATE = `You are starting a Ralph Loop - a self-refer
|
||||
|
||||
## Exit Conditions
|
||||
|
||||
1. **Completion**: Output \`<promise>DONE</promise>\` (or custom promise text) when fully complete
|
||||
1. **Completion**: Output your completion promise tag when fully complete
|
||||
2. **Max Iterations**: Loop stops automatically at limit
|
||||
3. **Cancel**: User runs \`/cancel-ralph\` command
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export const START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
5. **Read the plan file** and start executing tasks according to Orchestrator Sisyphus workflow
|
||||
5. **Read the plan file** and start executing tasks according to atlas workflow
|
||||
|
||||
## OUTPUT FORMAT
|
||||
|
||||
@@ -69,4 +69,4 @@ Reading plan and beginning execution...
|
||||
- The session_id is injected by the hook - use it directly
|
||||
- Always update boulder.json BEFORE starting work
|
||||
- Read the FULL plan file before delegating any tasks
|
||||
- Follow Orchestrator Sisyphus delegation protocols (7-section format)`
|
||||
- Follow atlas delegation protocols (7-section format)`
|
||||
|
||||
336
src/features/builtin-skills/agent-browser/SKILL.md
Normal file
336
src/features/builtin-skills/agent-browser/SKILL.md
Normal file
@@ -0,0 +1,336 @@
|
||||
---
|
||||
name: agent-browser
|
||||
description: Automates browser interactions for web testing, form filling, screenshots, and data extraction. Use when the user needs to navigate websites, interact with web pages, fill forms, take screenshots, test web applications, or extract information from web pages.
|
||||
---
|
||||
|
||||
# Browser Automation with agent-browser
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to page
|
||||
agent-browser snapshot -i # Get interactive elements with refs
|
||||
agent-browser click @e1 # Click element by ref
|
||||
agent-browser fill @e2 "text" # Fill input by ref
|
||||
agent-browser close # Close browser
|
||||
```
|
||||
|
||||
## Core workflow
|
||||
|
||||
1. Navigate: `agent-browser open <url>`
|
||||
2. Snapshot: `agent-browser snapshot -i` (returns elements with refs like `@e1`, `@e2`)
|
||||
3. Interact using refs from the snapshot
|
||||
4. Re-snapshot after navigation or significant DOM changes
|
||||
|
||||
## Commands
|
||||
|
||||
### Navigation
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to URL
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser
|
||||
```
|
||||
|
||||
### Snapshot (page analysis)
|
||||
```bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
agent-browser snapshot -c # Compact output
|
||||
agent-browser snapshot -d 3 # Limit depth to 3
|
||||
agent-browser snapshot -s "#main" # Scope to CSS selector
|
||||
```
|
||||
|
||||
### Interactions (use @refs from snapshot)
|
||||
```bash
|
||||
agent-browser click @e1 # Click
|
||||
agent-browser dblclick @e1 # Double-click
|
||||
agent-browser focus @e1 # Focus element
|
||||
agent-browser fill @e2 "text" # Clear and type
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser press Enter # Press key
|
||||
agent-browser press Control+a # Key combination
|
||||
agent-browser keydown Shift # Hold key down
|
||||
agent-browser keyup Shift # Release key
|
||||
agent-browser hover @e1 # Hover
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser uncheck @e1 # Uncheck checkbox
|
||||
agent-browser select @e1 "value" # Select dropdown
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scrollintoview @e1 # Scroll element into view
|
||||
agent-browser drag @e1 @e2 # Drag and drop
|
||||
agent-browser upload @e1 file.pdf # Upload files
|
||||
```
|
||||
|
||||
### Get information
|
||||
```bash
|
||||
agent-browser get text @e1 # Get element text
|
||||
agent-browser get html @e1 # Get innerHTML
|
||||
agent-browser get value @e1 # Get input value
|
||||
agent-browser get attr @e1 href # Get attribute
|
||||
agent-browser get title # Get page title
|
||||
agent-browser get url # Get current URL
|
||||
agent-browser get count ".item" # Count matching elements
|
||||
agent-browser get box @e1 # Get bounding box
|
||||
```
|
||||
|
||||
### Check state
|
||||
```bash
|
||||
agent-browser is visible @e1 # Check if visible
|
||||
agent-browser is enabled @e1 # Check if enabled
|
||||
agent-browser is checked @e1 # Check if checked
|
||||
```
|
||||
|
||||
### Screenshots & PDF
|
||||
```bash
|
||||
agent-browser screenshot # Screenshot to stdout
|
||||
agent-browser screenshot path.png # Save to file
|
||||
agent-browser screenshot --full # Full page
|
||||
agent-browser pdf output.pdf # Save as PDF
|
||||
```
|
||||
|
||||
### Video recording
|
||||
```bash
|
||||
agent-browser record start ./demo.webm # Start recording (uses current URL + state)
|
||||
agent-browser click @e1 # Perform actions
|
||||
agent-browser record stop # Stop and save video
|
||||
agent-browser record restart ./take2.webm # Stop current + start new recording
|
||||
```
|
||||
Recording creates a fresh context but preserves cookies/storage from your session.
|
||||
|
||||
### Wait
|
||||
```bash
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
agent-browser wait --text "Success" # Wait for text
|
||||
agent-browser wait --url "**/dashboard" # Wait for URL pattern
|
||||
agent-browser wait --load networkidle # Wait for network idle
|
||||
agent-browser wait --fn "window.ready" # Wait for JS condition
|
||||
```
|
||||
|
||||
### Mouse control
|
||||
```bash
|
||||
agent-browser mouse move 100 200 # Move mouse
|
||||
agent-browser mouse down left # Press button
|
||||
agent-browser mouse up left # Release button
|
||||
agent-browser mouse wheel 100 # Scroll wheel
|
||||
```
|
||||
|
||||
### Semantic locators (alternative to refs)
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
agent-browser find first ".item" click
|
||||
agent-browser find nth 2 "a" text
|
||||
```
|
||||
|
||||
### Browser settings
|
||||
```bash
|
||||
agent-browser set viewport 1920 1080 # Set viewport size
|
||||
agent-browser set device "iPhone 14" # Emulate device
|
||||
agent-browser set geo 37.7749 -122.4194 # Set geolocation
|
||||
agent-browser set offline on # Toggle offline mode
|
||||
agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers
|
||||
agent-browser set credentials user pass # HTTP basic auth
|
||||
agent-browser set media dark # Emulate color scheme
|
||||
```
|
||||
|
||||
### Cookies & Storage
|
||||
```bash
|
||||
agent-browser cookies # Get all cookies
|
||||
agent-browser cookies set name value # Set cookie
|
||||
agent-browser cookies clear # Clear cookies
|
||||
agent-browser storage local # Get all localStorage
|
||||
agent-browser storage local key # Get specific key
|
||||
agent-browser storage local set k v # Set value
|
||||
agent-browser storage local clear # Clear all
|
||||
agent-browser storage session # Get all sessionStorage
|
||||
agent-browser storage session key # Get specific key
|
||||
agent-browser storage session set k v # Set value
|
||||
agent-browser storage session clear # Clear all
|
||||
```
|
||||
|
||||
### Network
|
||||
```bash
|
||||
agent-browser network route <url> # Intercept requests
|
||||
agent-browser network route <url> --abort # Block requests
|
||||
agent-browser network route <url> --body '{}' # Mock response
|
||||
agent-browser network unroute [url] # Remove routes
|
||||
agent-browser network requests # View tracked requests
|
||||
agent-browser network requests --filter api # Filter requests
|
||||
```
|
||||
|
||||
### Tabs & Windows
|
||||
```bash
|
||||
agent-browser tab # List tabs
|
||||
agent-browser tab new [url] # New tab
|
||||
agent-browser tab 2 # Switch to tab
|
||||
agent-browser tab close # Close tab
|
||||
agent-browser window new # New window
|
||||
```
|
||||
|
||||
### Frames
|
||||
```bash
|
||||
agent-browser frame "#iframe" # Switch to iframe
|
||||
agent-browser frame main # Back to main frame
|
||||
```
|
||||
|
||||
### Dialogs
|
||||
```bash
|
||||
agent-browser dialog accept [text] # Accept dialog
|
||||
agent-browser dialog dismiss # Dismiss dialog
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
```bash
|
||||
agent-browser eval "document.title" # Run JavaScript
|
||||
```
|
||||
|
||||
## Global Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--session <name>` | Isolated browser session (`AGENT_BROWSER_SESSION` env) |
|
||||
| `--profile <path>` | Persistent browser profile (`AGENT_BROWSER_PROFILE` env) |
|
||||
| `--headers <json>` | HTTP headers scoped to URL's origin |
|
||||
| `--executable-path <path>` | Custom browser binary (`AGENT_BROWSER_EXECUTABLE_PATH` env) |
|
||||
| `--args <args>` | Browser launch args (`AGENT_BROWSER_ARGS` env) |
|
||||
| `--user-agent <ua>` | Custom User-Agent (`AGENT_BROWSER_USER_AGENT` env) |
|
||||
| `--proxy <url>` | Proxy server (`AGENT_BROWSER_PROXY` env) |
|
||||
| `--proxy-bypass <hosts>` | Hosts to bypass proxy (`AGENT_BROWSER_PROXY_BYPASS` env) |
|
||||
| `-p, --provider <name>` | Cloud browser provider (`AGENT_BROWSER_PROVIDER` env) |
|
||||
| `--json` | Machine-readable JSON output |
|
||||
| `--headed` | Show browser window (not headless) |
|
||||
| `--cdp <port\|wss://url>` | Connect via Chrome DevTools Protocol |
|
||||
| `--debug` | Debug output |
|
||||
|
||||
## Example: Form submission
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/form
|
||||
agent-browser snapshot -i
|
||||
# Output shows: textbox "Email" [ref=e1], textbox "Password" [ref=e2], button "Submit" [ref=e3]
|
||||
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser snapshot -i # Check result
|
||||
```
|
||||
|
||||
## Example: Authentication with saved state
|
||||
|
||||
```bash
|
||||
# Login once
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "username"
|
||||
agent-browser fill @e2 "password"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "**/dashboard"
|
||||
agent-browser state save auth.json
|
||||
|
||||
# Later sessions: load saved state
|
||||
agent-browser state load auth.json
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
### Header-based Auth (Skip login flows)
|
||||
```bash
|
||||
# Headers scoped to api.example.com only
|
||||
agent-browser open api.example.com --headers '{"Authorization": "Bearer <token>"}'
|
||||
# Navigate to another domain - headers NOT sent (safe)
|
||||
agent-browser open other-site.com
|
||||
# Global headers (all domains)
|
||||
agent-browser set headers '{"X-Custom-Header": "value"}'
|
||||
```
|
||||
|
||||
## Sessions & Persistent Profiles
|
||||
|
||||
### Sessions (parallel browsers)
|
||||
```bash
|
||||
agent-browser --session test1 open site-a.com
|
||||
agent-browser --session test2 open site-b.com
|
||||
agent-browser session list
|
||||
```
|
||||
|
||||
### Persistent Profiles
|
||||
Persists cookies, localStorage, IndexedDB, service workers, cache, login sessions across browser restarts.
|
||||
```bash
|
||||
agent-browser --profile ~/.myapp-profile open myapp.com
|
||||
# Or via env var
|
||||
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
|
||||
```
|
||||
- Use different profile paths for different projects
|
||||
- Login once → restart browser → still logged in
|
||||
- Stores: cookies, localStorage, IndexedDB, service workers, browser cache
|
||||
|
||||
## JSON output (for parsing)
|
||||
|
||||
Add `--json` for machine-readable output:
|
||||
```bash
|
||||
agent-browser snapshot -i --json
|
||||
agent-browser get text @e1 --json
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
agent-browser open example.com --headed # Show browser window
|
||||
agent-browser console # View console messages
|
||||
agent-browser errors # View page errors
|
||||
agent-browser record start ./debug.webm # Record from current page
|
||||
agent-browser record stop # Save recording
|
||||
agent-browser connect 9222 # Local CDP port
|
||||
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot # Remote via WebSocket
|
||||
agent-browser console --clear # Clear console
|
||||
agent-browser errors --clear # Clear errors
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser trace start # Start recording trace
|
||||
agent-browser trace stop trace.zip # Stop and save trace
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Step 1: Install agent-browser CLI
|
||||
|
||||
```bash
|
||||
bun add -g agent-browser
|
||||
```
|
||||
|
||||
### Step 2: Install Playwright browsers
|
||||
|
||||
**IMPORTANT**: `agent-browser install` may fail on some platforms (e.g., darwin-arm64) with "No binary found" error. In that case, install Playwright browsers directly:
|
||||
|
||||
```bash
|
||||
# Create a temp project and install playwright
|
||||
cd /tmp && bun init -y && bun add playwright
|
||||
|
||||
# Install Chromium browser
|
||||
bun playwright install chromium
|
||||
```
|
||||
|
||||
This downloads Chrome for Testing to `~/Library/Caches/ms-playwright/`.
|
||||
|
||||
### Verify installation
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com --headed
|
||||
```
|
||||
|
||||
If the browser opens successfully, installation is complete.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Error | Solution |
|
||||
|-------|----------|
|
||||
| `No binary found for darwin-arm64` | Run `bun playwright install chromium` in a project with playwright dependency |
|
||||
| `Executable doesn't exist at .../chromium-XXXX` | Re-run `bun playwright install chromium` |
|
||||
| Browser doesn't open | Ensure `--headed` flag is used for visible browser |
|
||||
|
||||
---
|
||||
Run `agent-browser --help` for all commands. Repo: https://github.com/vercel-labs/agent-browser
|
||||
213
src/features/builtin-skills/dev-browser/SKILL.md
Normal file
213
src/features/builtin-skills/dev-browser/SKILL.md
Normal file
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: dev-browser
|
||||
description: Browser automation with persistent page state. Use when users ask to navigate websites, fill forms, take screenshots, extract web data, test web apps, or automate browser workflows. Trigger phrases include "go to [url]", "click on", "fill out the form", "take a screenshot", "scrape", "automate", "test the website", "log into", or any browser interaction request.
|
||||
---
|
||||
|
||||
# Dev Browser Skill
|
||||
|
||||
Browser automation that maintains page state across script executions. Write small, focused scripts to accomplish tasks incrementally. Once you've proven out part of a workflow and there is repeated work to be done, you can write a script to do the repeated work in a single execution.
|
||||
|
||||
## Choosing Your Approach
|
||||
|
||||
- **Local/source-available sites**: Read the source code first to write selectors directly
|
||||
- **Unknown page layouts**: Use `getAISnapshot()` to discover elements and `selectSnapshotRef()` to interact with them
|
||||
- **Visual feedback**: Take screenshots to see what the user sees
|
||||
|
||||
## Setup
|
||||
|
||||
> **Installation**: See [references/installation.md](references/installation.md) for detailed setup instructions including Windows support.
|
||||
|
||||
Two modes available. Ask the user if unclear which to use.
|
||||
|
||||
### Standalone Mode (Default)
|
||||
|
||||
Launches a new Chromium browser for fresh automation sessions.
|
||||
|
||||
```bash
|
||||
./skills/dev-browser/server.sh &
|
||||
```
|
||||
|
||||
Add `--headless` flag if user requests it. **Wait for the `Ready` message before running scripts.**
|
||||
|
||||
### Extension Mode
|
||||
|
||||
Connects to user's existing Chrome browser. Use this when:
|
||||
|
||||
- The user is already logged into sites and wants you to do things behind an authed experience that isn't local dev.
|
||||
- The user asks you to use the extension
|
||||
|
||||
**Important**: The core flow is still the same. You create named pages inside of their browser.
|
||||
|
||||
**Start the relay server:**
|
||||
|
||||
```bash
|
||||
cd skills/dev-browser && npm i && npm run start-extension &
|
||||
```
|
||||
|
||||
Wait for `Waiting for extension to connect...` followed by `Extension connected` in the console. To know that a client has connected and the browser is ready to be controlled.
|
||||
**Workflow:**
|
||||
|
||||
1. Scripts call `client.page("name")` just like the normal mode to create new pages / connect to existing ones.
|
||||
2. Automation runs on the user's actual browser session
|
||||
|
||||
If the extension hasn't connected yet, tell the user to launch and activate it. Download link: https://github.com/SawyerHood/dev-browser/releases
|
||||
|
||||
## Writing Scripts
|
||||
|
||||
> **Run all scripts from `skills/dev-browser/` directory.** The `@/` import alias requires this directory's config.
|
||||
|
||||
Execute scripts inline using heredocs:
|
||||
|
||||
```bash
|
||||
cd skills/dev-browser && npx tsx <<'EOF'
|
||||
import { connect, waitForPageLoad } from "@/client.js";
|
||||
|
||||
const client = await connect();
|
||||
// Create page with custom viewport size (optional)
|
||||
const page = await client.page("example", { viewport: { width: 1920, height: 1080 } });
|
||||
|
||||
await page.goto("https://example.com");
|
||||
await waitForPageLoad(page);
|
||||
|
||||
console.log({ title: await page.title(), url: page.url() });
|
||||
await client.disconnect();
|
||||
EOF
|
||||
```
|
||||
|
||||
**Write to `tmp/` files only when** the script needs reuse, is complex, or user explicitly requests it.
|
||||
|
||||
### Key Principles
|
||||
|
||||
1. **Small scripts**: Each script does ONE thing (navigate, click, fill, check)
|
||||
2. **Evaluate state**: Log/return state at the end to decide next steps
|
||||
3. **Descriptive page names**: Use `"checkout"`, `"login"`, not `"main"`
|
||||
4. **Disconnect to exit**: `await client.disconnect()` - pages persist on server
|
||||
5. **Plain JS in evaluate**: `page.evaluate()` runs in browser - no TypeScript syntax
|
||||
|
||||
## Workflow Loop
|
||||
|
||||
Follow this pattern for complex tasks:
|
||||
|
||||
1. **Write a script** to perform one action
|
||||
2. **Run it** and observe the output
|
||||
3. **Evaluate** - did it work? What's the current state?
|
||||
4. **Decide** - is the task complete or do we need another script?
|
||||
5. **Repeat** until task is done
|
||||
|
||||
### No TypeScript in Browser Context
|
||||
|
||||
Code passed to `page.evaluate()` runs in the browser, which doesn't understand TypeScript:
|
||||
|
||||
```typescript
|
||||
// ✅ Correct: plain JavaScript
|
||||
const text = await page.evaluate(() => {
|
||||
return document.body.innerText;
|
||||
});
|
||||
|
||||
// ❌ Wrong: TypeScript syntax will fail at runtime
|
||||
const text = await page.evaluate(() => {
|
||||
const el: HTMLElement = document.body; // Type annotation breaks in browser!
|
||||
return el.innerText;
|
||||
});
|
||||
```
|
||||
|
||||
## Scraping Data
|
||||
|
||||
For scraping large datasets, intercept and replay network requests rather than scrolling the DOM. See [references/scraping.md](references/scraping.md) for the complete guide covering request capture, schema discovery, and paginated API replay.
|
||||
|
||||
## Client API
|
||||
|
||||
```typescript
|
||||
const client = await connect();
|
||||
|
||||
// Get or create named page (viewport only applies to new pages)
|
||||
const page = await client.page("name");
|
||||
const pageWithSize = await client.page("name", { viewport: { width: 1920, height: 1080 } });
|
||||
|
||||
const pages = await client.list(); // List all page names
|
||||
await client.close("name"); // Close a page
|
||||
await client.disconnect(); // Disconnect (pages persist)
|
||||
|
||||
// ARIA Snapshot methods
|
||||
const snapshot = await client.getAISnapshot("name"); // Get accessibility tree
|
||||
const element = await client.selectSnapshotRef("name", "e5"); // Get element by ref
|
||||
```
|
||||
|
||||
The `page` object is a standard Playwright Page.
|
||||
|
||||
## Waiting
|
||||
|
||||
```typescript
|
||||
import { waitForPageLoad } from "@/client.js";
|
||||
|
||||
await waitForPageLoad(page); // After navigation
|
||||
await page.waitForSelector(".results"); // For specific elements
|
||||
await page.waitForURL("**/success"); // For specific URL
|
||||
```
|
||||
|
||||
## Inspecting Page State
|
||||
|
||||
### Screenshots
|
||||
|
||||
```typescript
|
||||
await page.screenshot({ path: "tmp/screenshot.png" });
|
||||
await page.screenshot({ path: "tmp/full.png", fullPage: true });
|
||||
```
|
||||
|
||||
### ARIA Snapshot (Element Discovery)
|
||||
|
||||
Use `getAISnapshot()` to discover page elements. Returns YAML-formatted accessibility tree:
|
||||
|
||||
```yaml
|
||||
- banner:
|
||||
- link "Hacker News" [ref=e1]
|
||||
- navigation:
|
||||
- link "new" [ref=e2]
|
||||
- main:
|
||||
- list:
|
||||
- listitem:
|
||||
- link "Article Title" [ref=e8]
|
||||
- link "328 comments" [ref=e9]
|
||||
- contentinfo:
|
||||
- textbox [ref=e10]
|
||||
- /placeholder: "Search"
|
||||
```
|
||||
|
||||
**Interpreting refs:**
|
||||
|
||||
- `[ref=eN]` - Element reference for interaction (visible, clickable elements only)
|
||||
- `[checked]`, `[disabled]`, `[expanded]` - Element states
|
||||
- `[level=N]` - Heading level
|
||||
- `/url:`, `/placeholder:` - Element properties
|
||||
|
||||
**Interacting with refs:**
|
||||
|
||||
```typescript
|
||||
const snapshot = await client.getAISnapshot("hackernews");
|
||||
console.log(snapshot); // Find the ref you need
|
||||
|
||||
const element = await client.selectSnapshotRef("hackernews", "e2");
|
||||
await element.click();
|
||||
```
|
||||
|
||||
## Error Recovery
|
||||
|
||||
Page state persists after failures. Debug with:
|
||||
|
||||
```bash
|
||||
cd skills/dev-browser && npx tsx <<'EOF'
|
||||
import { connect } from "@/client.js";
|
||||
|
||||
const client = await connect();
|
||||
const page = await client.page("hackernews");
|
||||
|
||||
await page.screenshot({ path: "tmp/debug.png" });
|
||||
console.log({
|
||||
url: page.url(),
|
||||
title: await page.title(),
|
||||
bodyText: await page.textContent("body").then((t) => t?.slice(0, 200)),
|
||||
});
|
||||
|
||||
await client.disconnect();
|
||||
EOF
|
||||
```
|
||||
@@ -0,0 +1,193 @@
|
||||
# Dev Browser Installation Guide
|
||||
|
||||
This guide covers installation for all platforms: macOS, Linux, and Windows.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org) v18 or later with npm
|
||||
- Git (for cloning the skill)
|
||||
|
||||
## Installation
|
||||
|
||||
### Step 1: Clone the Skill
|
||||
|
||||
```bash
|
||||
# Clone dev-browser to a temporary location
|
||||
git clone https://github.com/sawyerhood/dev-browser /tmp/dev-browser-skill
|
||||
|
||||
# Copy to skills directory (adjust path as needed)
|
||||
# For oh-my-opencode: already bundled
|
||||
# For manual installation:
|
||||
mkdir -p ~/.config/opencode/skills
|
||||
cp -r /tmp/dev-browser-skill/skills/dev-browser ~/.config/opencode/skills/dev-browser
|
||||
|
||||
# Cleanup
|
||||
rm -rf /tmp/dev-browser-skill
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
# Clone dev-browser to temp location
|
||||
git clone https://github.com/sawyerhood/dev-browser $env:TEMP\dev-browser-skill
|
||||
|
||||
# Copy to skills directory
|
||||
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\opencode\skills"
|
||||
Copy-Item -Recurse "$env:TEMP\dev-browser-skill\skills\dev-browser" "$env:USERPROFILE\.config\opencode\skills\dev-browser"
|
||||
|
||||
# Cleanup
|
||||
Remove-Item -Recurse -Force "$env:TEMP\dev-browser-skill"
|
||||
```
|
||||
|
||||
### Step 2: Install Dependencies
|
||||
|
||||
```bash
|
||||
cd ~/.config/opencode/skills/dev-browser
|
||||
npm install
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
cd "$env:USERPROFILE\.config\opencode\skills\dev-browser"
|
||||
npm install
|
||||
```
|
||||
|
||||
### Step 3: Start the Server
|
||||
|
||||
#### Standalone Mode (New Browser Instance)
|
||||
|
||||
**macOS/Linux:**
|
||||
```bash
|
||||
cd ~/.config/opencode/skills/dev-browser
|
||||
./server.sh &
|
||||
# Or for headless:
|
||||
./server.sh --headless &
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
cd "$env:USERPROFILE\.config\opencode\skills\dev-browser"
|
||||
Start-Process -NoNewWindow -FilePath "node" -ArgumentList "server.js"
|
||||
# Or for headless:
|
||||
Start-Process -NoNewWindow -FilePath "node" -ArgumentList "server.js", "--headless"
|
||||
```
|
||||
|
||||
**Windows (CMD):**
|
||||
```cmd
|
||||
cd %USERPROFILE%\.config\opencode\skills\dev-browser
|
||||
start /B node server.js
|
||||
```
|
||||
|
||||
Wait for the `Ready` message before running scripts.
|
||||
|
||||
#### Extension Mode (Use Existing Chrome)
|
||||
|
||||
**macOS/Linux:**
|
||||
```bash
|
||||
cd ~/.config/opencode/skills/dev-browser
|
||||
npm run start-extension &
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
cd "$env:USERPROFILE\.config\opencode\skills\dev-browser"
|
||||
Start-Process -NoNewWindow -FilePath "npm" -ArgumentList "run", "start-extension"
|
||||
```
|
||||
|
||||
Wait for `Extension connected` message.
|
||||
|
||||
## Chrome Extension Setup (Optional)
|
||||
|
||||
The Chrome extension allows controlling your existing Chrome browser with all your logged-in sessions.
|
||||
|
||||
### Installation
|
||||
|
||||
1. Download `extension.zip` from [latest release](https://github.com/sawyerhood/dev-browser/releases/latest)
|
||||
2. Extract to a permanent location:
|
||||
- **macOS/Linux:** `~/.dev-browser-extension`
|
||||
- **Windows:** `%USERPROFILE%\.dev-browser-extension`
|
||||
3. Open Chrome → `chrome://extensions`
|
||||
4. Enable "Developer mode" (toggle in top right)
|
||||
5. Click "Load unpacked" → select the extracted folder
|
||||
|
||||
### Usage
|
||||
|
||||
1. Click the Dev Browser extension icon in Chrome toolbar
|
||||
2. Toggle to "Active"
|
||||
3. Start the extension relay server (see above)
|
||||
4. Use dev-browser scripts - they'll control your existing Chrome
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server Won't Start
|
||||
|
||||
**Check Node.js version:**
|
||||
```bash
|
||||
node --version # Should be v18+
|
||||
```
|
||||
|
||||
**Check port availability:**
|
||||
```bash
|
||||
# macOS/Linux
|
||||
lsof -i :3000
|
||||
|
||||
# Windows
|
||||
netstat -ano | findstr :3000
|
||||
```
|
||||
|
||||
### Playwright Installation Issues
|
||||
|
||||
If Chromium fails to install:
|
||||
```bash
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
### Windows-Specific Issues
|
||||
|
||||
**Execution Policy:**
|
||||
If PowerShell scripts are blocked:
|
||||
```powershell
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
```
|
||||
|
||||
**Path Issues:**
|
||||
Use forward slashes or escaped backslashes in paths:
|
||||
```powershell
|
||||
# Good
|
||||
cd "$env:USERPROFILE/.config/opencode/skills/dev-browser"
|
||||
# Also good
|
||||
cd "$env:USERPROFILE\.config\opencode\skills\dev-browser"
|
||||
```
|
||||
|
||||
### Extension Not Connecting
|
||||
|
||||
1. Ensure extension is "Active" (click icon to toggle)
|
||||
2. Check relay server is running (`npm run start-extension`)
|
||||
3. Look for `Extension connected` message in console
|
||||
4. Try reloading the extension in `chrome://extensions`
|
||||
|
||||
## Permissions
|
||||
|
||||
To skip permission prompts in Claude Code, add to `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"allow": ["Skill(dev-browser:dev-browser)", "Bash(npx tsx:*)"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
cd ~/.config/opencode/skills/dev-browser
|
||||
git pull
|
||||
npm install
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```powershell
|
||||
cd "$env:USERPROFILE\.config\opencode\skills\dev-browser"
|
||||
git pull
|
||||
npm install
|
||||
```
|
||||
155
src/features/builtin-skills/dev-browser/references/scraping.md
Normal file
155
src/features/builtin-skills/dev-browser/references/scraping.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Data Scraping Guide
|
||||
|
||||
For large datasets (followers, posts, search results), **intercept and replay network requests** rather than scrolling and parsing the DOM. This is faster, more reliable, and handles pagination automatically.
|
||||
|
||||
## Why Not Scroll?
|
||||
|
||||
Scrolling is slow, unreliable, and wastes time. APIs return structured data with pagination built in. Always prefer API replay.
|
||||
|
||||
## Start Small, Then Scale
|
||||
|
||||
**Don't try to automate everything at once.** Work incrementally:
|
||||
|
||||
1. **Capture one request** - verify you're intercepting the right endpoint
|
||||
2. **Inspect one response** - understand the schema before writing extraction code
|
||||
3. **Extract a few items** - make sure your parsing logic works
|
||||
4. **Then scale up** - add pagination loop only after the basics work
|
||||
|
||||
This prevents wasting time debugging a complex script when the issue is a simple path like `data.user.timeline` vs `data.user.result.timeline`.
|
||||
|
||||
## Step-by-Step Workflow
|
||||
|
||||
### 1. Capture Request Details
|
||||
|
||||
First, intercept a request to understand URL structure and required headers:
|
||||
|
||||
```typescript
|
||||
import { connect, waitForPageLoad } from "@/client.js";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
const client = await connect();
|
||||
const page = await client.page("site");
|
||||
|
||||
let capturedRequest = null;
|
||||
page.on("request", (request) => {
|
||||
const url = request.url();
|
||||
// Look for API endpoints (adjust pattern for your target site)
|
||||
if (url.includes("/api/") || url.includes("/graphql/")) {
|
||||
capturedRequest = {
|
||||
url: url,
|
||||
headers: request.headers(),
|
||||
method: request.method(),
|
||||
};
|
||||
fs.writeFileSync("tmp/request-details.json", JSON.stringify(capturedRequest, null, 2));
|
||||
console.log("Captured request:", url.substring(0, 80) + "...");
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto("https://example.com/profile");
|
||||
await waitForPageLoad(page);
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await client.disconnect();
|
||||
```
|
||||
|
||||
### 2. Capture Response to Understand Schema
|
||||
|
||||
Save a raw response to inspect the data structure:
|
||||
|
||||
```typescript
|
||||
page.on("response", async (response) => {
|
||||
const url = response.url();
|
||||
if (url.includes("UserTweets") || url.includes("/api/data")) {
|
||||
const json = await response.json();
|
||||
fs.writeFileSync("tmp/api-response.json", JSON.stringify(json, null, 2));
|
||||
console.log("Captured response");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Then analyze the structure to find:
|
||||
|
||||
- Where the data array lives (e.g., `data.user.result.timeline.instructions[].entries`)
|
||||
- Where pagination cursors are (e.g., `cursor-bottom` entries)
|
||||
- What fields you need to extract
|
||||
|
||||
### 3. Replay API with Pagination
|
||||
|
||||
Once you understand the schema, replay requests directly:
|
||||
|
||||
```typescript
|
||||
import { connect } from "@/client.js";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
const client = await connect();
|
||||
const page = await client.page("site");
|
||||
|
||||
const results = new Map(); // Use Map for deduplication
|
||||
const headers = JSON.parse(fs.readFileSync("tmp/request-details.json", "utf8")).headers;
|
||||
const baseUrl = "https://example.com/api/data";
|
||||
|
||||
let cursor = null;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
// Build URL with pagination cursor
|
||||
const params = { count: 20 };
|
||||
if (cursor) params.cursor = cursor;
|
||||
const url = `${baseUrl}?params=${encodeURIComponent(JSON.stringify(params))}`;
|
||||
|
||||
// Execute fetch in browser context (has auth cookies/headers)
|
||||
const response = await page.evaluate(
|
||||
async ({ url, headers }) => {
|
||||
const res = await fetch(url, { headers });
|
||||
return res.json();
|
||||
},
|
||||
{ url, headers }
|
||||
);
|
||||
|
||||
// Extract data and cursor (adjust paths for your API)
|
||||
const entries = response?.data?.entries || [];
|
||||
for (const entry of entries) {
|
||||
if (entry.type === "cursor-bottom") {
|
||||
cursor = entry.value;
|
||||
} else if (entry.id && !results.has(entry.id)) {
|
||||
results.set(entry.id, {
|
||||
id: entry.id,
|
||||
text: entry.content,
|
||||
timestamp: entry.created_at,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Fetched page, total: ${results.size}`);
|
||||
|
||||
// Check stop conditions
|
||||
if (!cursor || entries.length === 0) hasMore = false;
|
||||
|
||||
// Rate limiting - be respectful
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
// Export results
|
||||
const data = Array.from(results.values());
|
||||
fs.writeFileSync("tmp/results.json", JSON.stringify(data, null, 2));
|
||||
console.log(`Saved ${data.length} items`);
|
||||
|
||||
await client.disconnect();
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
| Pattern | Description |
|
||||
| ----------------------- | ------------------------------------------------------ |
|
||||
| `page.on('request')` | Capture outgoing request URL + headers |
|
||||
| `page.on('response')` | Capture response data to understand schema |
|
||||
| `page.evaluate(fetch)` | Replay requests in browser context (inherits auth) |
|
||||
| `Map` for deduplication | APIs often return overlapping data across pages |
|
||||
| Cursor-based pagination | Look for `cursor`, `next_token`, `offset` in responses |
|
||||
|
||||
## Tips
|
||||
|
||||
- **Extension mode**: `page.context().cookies()` doesn't work - capture auth headers from intercepted requests instead
|
||||
- **Rate limiting**: Add 500ms+ delays between requests to avoid blocks
|
||||
- **Stop conditions**: Check for empty results, missing cursor, or reaching a date/ID threshold
|
||||
- **GraphQL APIs**: URL params often include `variables` and `features` JSON objects - capture and reuse them
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: git-master
|
||||
description: "MUST USE for ANY git operations. Atomic commits, rebase/squash, history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with delegate_task(category='quick', skills=['git-master'], ...) to save context. Triggers: 'commit', 'rebase', 'squash', 'who wrote', 'when was X added', 'find the commit that'."
|
||||
description: "MUST USE for ANY git operations. Atomic commits, rebase/squash, history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with delegate_task(category='quick', load_skills=['git-master'], ...) to save context. Triggers: 'commit', 'rebase', 'squash', 'who wrote', 'when was X added', 'find the commit that'."
|
||||
---
|
||||
|
||||
# Git Master Agent
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./types"
|
||||
export { createBuiltinSkills } from "./skills"
|
||||
export { createBuiltinSkills, type CreateBuiltinSkillsOptions } from "./skills"
|
||||
|
||||
89
src/features/builtin-skills/skills.test.ts
Normal file
89
src/features/builtin-skills/skills.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import { createBuiltinSkills } from "./skills"
|
||||
|
||||
describe("createBuiltinSkills", () => {
|
||||
test("returns playwright skill by default", () => {
|
||||
// #given - no options (default)
|
||||
|
||||
// #when
|
||||
const skills = createBuiltinSkills()
|
||||
|
||||
// #then
|
||||
const browserSkill = skills.find((s) => s.name === "playwright")
|
||||
expect(browserSkill).toBeDefined()
|
||||
expect(browserSkill!.description).toContain("browser")
|
||||
expect(browserSkill!.mcpConfig).toHaveProperty("playwright")
|
||||
})
|
||||
|
||||
test("returns playwright skill when browserProvider is 'playwright'", () => {
|
||||
// #given
|
||||
const options = { browserProvider: "playwright" as const }
|
||||
|
||||
// #when
|
||||
const skills = createBuiltinSkills(options)
|
||||
|
||||
// #then
|
||||
const playwrightSkill = skills.find((s) => s.name === "playwright")
|
||||
const agentBrowserSkill = skills.find((s) => s.name === "agent-browser")
|
||||
expect(playwrightSkill).toBeDefined()
|
||||
expect(agentBrowserSkill).toBeUndefined()
|
||||
})
|
||||
|
||||
test("returns agent-browser skill when browserProvider is 'agent-browser'", () => {
|
||||
// #given
|
||||
const options = { browserProvider: "agent-browser" as const }
|
||||
|
||||
// #when
|
||||
const skills = createBuiltinSkills(options)
|
||||
|
||||
// #then
|
||||
const agentBrowserSkill = skills.find((s) => s.name === "agent-browser")
|
||||
const playwrightSkill = skills.find((s) => s.name === "playwright")
|
||||
expect(agentBrowserSkill).toBeDefined()
|
||||
expect(agentBrowserSkill!.description).toContain("browser")
|
||||
expect(agentBrowserSkill!.allowedTools).toContain("Bash(agent-browser:*)")
|
||||
expect(agentBrowserSkill!.template).toContain("agent-browser")
|
||||
expect(playwrightSkill).toBeUndefined()
|
||||
})
|
||||
|
||||
test("agent-browser skill template is inlined (not loaded from file)", () => {
|
||||
// #given
|
||||
const options = { browserProvider: "agent-browser" as const }
|
||||
|
||||
// #when
|
||||
const skills = createBuiltinSkills(options)
|
||||
const agentBrowserSkill = skills.find((s) => s.name === "agent-browser")
|
||||
|
||||
// #then - template should contain substantial content (inlined, not fallback)
|
||||
expect(agentBrowserSkill!.template).toContain("## Quick start")
|
||||
expect(agentBrowserSkill!.template).toContain("## Commands")
|
||||
expect(agentBrowserSkill!.template).toContain("agent-browser open")
|
||||
expect(agentBrowserSkill!.template).toContain("agent-browser snapshot")
|
||||
})
|
||||
|
||||
test("always includes frontend-ui-ux and git-master skills", () => {
|
||||
// #given - both provider options
|
||||
|
||||
// #when
|
||||
const defaultSkills = createBuiltinSkills()
|
||||
const agentBrowserSkills = createBuiltinSkills({ browserProvider: "agent-browser" })
|
||||
|
||||
// #then
|
||||
for (const skills of [defaultSkills, agentBrowserSkills]) {
|
||||
expect(skills.find((s) => s.name === "frontend-ui-ux")).toBeDefined()
|
||||
expect(skills.find((s) => s.name === "git-master")).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
test("returns exactly 4 skills regardless of provider", () => {
|
||||
// #given
|
||||
|
||||
// #when
|
||||
const defaultSkills = createBuiltinSkills()
|
||||
const agentBrowserSkills = createBuiltinSkills({ browserProvider: "agent-browser" })
|
||||
|
||||
// #then
|
||||
expect(defaultSkills).toHaveLength(4)
|
||||
expect(agentBrowserSkills).toHaveLength(4)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BuiltinSkill } from "./types"
|
||||
import type { BrowserAutomationProvider } from "../../config/schema"
|
||||
|
||||
const playwrightSkill: BuiltinSkill = {
|
||||
name: "playwright",
|
||||
@@ -14,6 +15,303 @@ This skill provides browser automation capabilities via the Playwright MCP serve
|
||||
},
|
||||
}
|
||||
|
||||
const agentBrowserSkill: BuiltinSkill = {
|
||||
name: "agent-browser",
|
||||
description: "MUST USE for any browser-related tasks. Browser automation via agent-browser CLI - verification, browsing, information gathering, web scraping, testing, screenshots, and all browser interactions.",
|
||||
template: `# Browser Automation with agent-browser
|
||||
|
||||
## Quick start
|
||||
|
||||
\`\`\`bash
|
||||
agent-browser open <url> # Navigate to page
|
||||
agent-browser snapshot -i # Get interactive elements with refs
|
||||
agent-browser click @e1 # Click element by ref
|
||||
agent-browser fill @e2 "text" # Fill input by ref
|
||||
agent-browser close # Close browser
|
||||
\`\`\`
|
||||
|
||||
## Core workflow
|
||||
|
||||
1. Navigate: \`agent-browser open <url>\`
|
||||
2. Snapshot: \`agent-browser snapshot -i\` (returns elements with refs like \`@e1\`, \`@e2\`)
|
||||
3. Interact using refs from the snapshot
|
||||
4. Re-snapshot after navigation or significant DOM changes
|
||||
|
||||
## Commands
|
||||
|
||||
### Navigation
|
||||
\`\`\`bash
|
||||
agent-browser open <url> # Navigate to URL
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser
|
||||
\`\`\`
|
||||
|
||||
### Snapshot (page analysis)
|
||||
\`\`\`bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
agent-browser snapshot -c # Compact output
|
||||
agent-browser snapshot -d 3 # Limit depth to 3
|
||||
agent-browser snapshot -s "#main" # Scope to CSS selector
|
||||
\`\`\`
|
||||
|
||||
### Interactions (use @refs from snapshot)
|
||||
\`\`\`bash
|
||||
agent-browser click @e1 # Click
|
||||
agent-browser dblclick @e1 # Double-click
|
||||
agent-browser focus @e1 # Focus element
|
||||
agent-browser fill @e2 "text" # Clear and type
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser press Enter # Press key
|
||||
agent-browser press Control+a # Key combination
|
||||
agent-browser keydown Shift # Hold key down
|
||||
agent-browser keyup Shift # Release key
|
||||
agent-browser hover @e1 # Hover
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser uncheck @e1 # Uncheck checkbox
|
||||
agent-browser select @e1 "value" # Select dropdown
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scrollintoview @e1 # Scroll element into view
|
||||
agent-browser drag @e1 @e2 # Drag and drop
|
||||
agent-browser upload @e1 file.pdf # Upload files
|
||||
\`\`\`
|
||||
|
||||
### Get information
|
||||
\`\`\`bash
|
||||
agent-browser get text @e1 # Get element text
|
||||
agent-browser get html @e1 # Get innerHTML
|
||||
agent-browser get value @e1 # Get input value
|
||||
agent-browser get attr @e1 href # Get attribute
|
||||
agent-browser get title # Get page title
|
||||
agent-browser get url # Get current URL
|
||||
agent-browser get count ".item" # Count matching elements
|
||||
agent-browser get box @e1 # Get bounding box
|
||||
\`\`\`
|
||||
|
||||
### Check state
|
||||
\`\`\`bash
|
||||
agent-browser is visible @e1 # Check if visible
|
||||
agent-browser is enabled @e1 # Check if enabled
|
||||
agent-browser is checked @e1 # Check if checked
|
||||
\`\`\`
|
||||
|
||||
### Screenshots & PDF
|
||||
\`\`\`bash
|
||||
agent-browser screenshot # Screenshot to stdout
|
||||
agent-browser screenshot path.png # Save to file
|
||||
agent-browser screenshot --full # Full page
|
||||
agent-browser pdf output.pdf # Save as PDF
|
||||
\`\`\`
|
||||
|
||||
### Video recording
|
||||
\`\`\`bash
|
||||
agent-browser record start ./demo.webm # Start recording (uses current URL + state)
|
||||
agent-browser click @e1 # Perform actions
|
||||
agent-browser record stop # Stop and save video
|
||||
agent-browser record restart ./take2.webm # Stop current + start new recording
|
||||
\`\`\`
|
||||
Recording creates a fresh context but preserves cookies/storage from your session.
|
||||
|
||||
### Wait
|
||||
\`\`\`bash
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
agent-browser wait --text "Success" # Wait for text
|
||||
agent-browser wait --url "**/dashboard" # Wait for URL pattern
|
||||
agent-browser wait --load networkidle # Wait for network idle
|
||||
agent-browser wait --fn "window.ready" # Wait for JS condition
|
||||
\`\`\`
|
||||
|
||||
### Mouse control
|
||||
\`\`\`bash
|
||||
agent-browser mouse move 100 200 # Move mouse
|
||||
agent-browser mouse down left # Press button
|
||||
agent-browser mouse up left # Release button
|
||||
agent-browser mouse wheel 100 # Scroll wheel
|
||||
\`\`\`
|
||||
|
||||
### Semantic locators (alternative to refs)
|
||||
\`\`\`bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
agent-browser find first ".item" click
|
||||
agent-browser find nth 2 "a" text
|
||||
\`\`\`
|
||||
|
||||
### Browser settings
|
||||
\`\`\`bash
|
||||
agent-browser set viewport 1920 1080 # Set viewport size
|
||||
agent-browser set device "iPhone 14" # Emulate device
|
||||
agent-browser set geo 37.7749 -122.4194 # Set geolocation
|
||||
agent-browser set offline on # Toggle offline mode
|
||||
agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers
|
||||
agent-browser set credentials user pass # HTTP basic auth
|
||||
agent-browser set media dark # Emulate color scheme
|
||||
\`\`\`
|
||||
|
||||
### Cookies & Storage
|
||||
\`\`\`bash
|
||||
agent-browser cookies # Get all cookies
|
||||
agent-browser cookies set name value # Set cookie
|
||||
agent-browser cookies clear # Clear cookies
|
||||
agent-browser storage local # Get all localStorage
|
||||
agent-browser storage local key # Get specific key
|
||||
agent-browser storage local set k v # Set value
|
||||
agent-browser storage local clear # Clear all
|
||||
agent-browser storage session # Get all sessionStorage
|
||||
agent-browser storage session key # Get specific key
|
||||
agent-browser storage session set k v # Set value
|
||||
agent-browser storage session clear # Clear all
|
||||
\`\`\`
|
||||
|
||||
### Network
|
||||
\`\`\`bash
|
||||
agent-browser network route <url> # Intercept requests
|
||||
agent-browser network route <url> --abort # Block requests
|
||||
agent-browser network route <url> --body '{}' # Mock response
|
||||
agent-browser network unroute [url] # Remove routes
|
||||
agent-browser network requests # View tracked requests
|
||||
agent-browser network requests --filter api # Filter requests
|
||||
\`\`\`
|
||||
|
||||
### Tabs & Windows
|
||||
\`\`\`bash
|
||||
agent-browser tab # List tabs
|
||||
agent-browser tab new [url] # New tab
|
||||
agent-browser tab 2 # Switch to tab
|
||||
agent-browser tab close # Close tab
|
||||
agent-browser window new # New window
|
||||
\`\`\`
|
||||
|
||||
### Frames
|
||||
\`\`\`bash
|
||||
agent-browser frame "#iframe" # Switch to iframe
|
||||
agent-browser frame main # Back to main frame
|
||||
\`\`\`
|
||||
|
||||
### Dialogs
|
||||
\`\`\`bash
|
||||
agent-browser dialog accept [text] # Accept dialog
|
||||
agent-browser dialog dismiss # Dismiss dialog
|
||||
\`\`\`
|
||||
|
||||
### JavaScript
|
||||
\`\`\`bash
|
||||
agent-browser eval "document.title" # Run JavaScript
|
||||
\`\`\`
|
||||
|
||||
## Global Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| \`--session <name>\` | Isolated browser session (\`AGENT_BROWSER_SESSION\` env) |
|
||||
| \`--profile <path>\` | Persistent browser profile (\`AGENT_BROWSER_PROFILE\` env) |
|
||||
| \`--headers <json>\` | HTTP headers scoped to URL's origin |
|
||||
| \`--executable-path <path>\` | Custom browser binary (\`AGENT_BROWSER_EXECUTABLE_PATH\` env) |
|
||||
| \`--args <args>\` | Browser launch args (\`AGENT_BROWSER_ARGS\` env) |
|
||||
| \`--user-agent <ua>\` | Custom User-Agent (\`AGENT_BROWSER_USER_AGENT\` env) |
|
||||
| \`--proxy <url>\` | Proxy server (\`AGENT_BROWSER_PROXY\` env) |
|
||||
| \`--proxy-bypass <hosts>\` | Hosts to bypass proxy (\`AGENT_BROWSER_PROXY_BYPASS\` env) |
|
||||
| \`-p, --provider <name>\` | Cloud browser provider (\`AGENT_BROWSER_PROVIDER\` env) |
|
||||
| \`--json\` | Machine-readable JSON output |
|
||||
| \`--headed\` | Show browser window (not headless) |
|
||||
| \`--cdp <port\\|wss://url>\` | Connect via Chrome DevTools Protocol |
|
||||
| \`--debug\` | Debug output |
|
||||
|
||||
## Example: Form submission
|
||||
|
||||
\`\`\`bash
|
||||
agent-browser open https://example.com/form
|
||||
agent-browser snapshot -i
|
||||
# Output shows: textbox "Email" [ref=e1], textbox "Password" [ref=e2], button "Submit" [ref=e3]
|
||||
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser snapshot -i # Check result
|
||||
\`\`\`
|
||||
|
||||
## Example: Authentication with saved state
|
||||
|
||||
\`\`\`bash
|
||||
# Login once
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "username"
|
||||
agent-browser fill @e2 "password"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "**/dashboard"
|
||||
agent-browser state save auth.json
|
||||
|
||||
# Later sessions: load saved state
|
||||
agent-browser state load auth.json
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
\`\`\`
|
||||
|
||||
### Header-based Auth (Skip login flows)
|
||||
\`\`\`bash
|
||||
# Headers scoped to api.example.com only
|
||||
agent-browser open api.example.com --headers '{"Authorization": "Bearer <token>"}'
|
||||
# Navigate to another domain - headers NOT sent (safe)
|
||||
agent-browser open other-site.com
|
||||
# Global headers (all domains)
|
||||
agent-browser set headers '{"X-Custom-Header": "value"}'
|
||||
\`\`\`
|
||||
|
||||
## Sessions & Persistent Profiles
|
||||
|
||||
### Sessions (parallel browsers)
|
||||
\`\`\`bash
|
||||
agent-browser --session test1 open site-a.com
|
||||
agent-browser --session test2 open site-b.com
|
||||
agent-browser session list
|
||||
\`\`\`
|
||||
|
||||
### Persistent Profiles
|
||||
Persists cookies, localStorage, IndexedDB, service workers, cache, login sessions across browser restarts.
|
||||
\`\`\`bash
|
||||
agent-browser --profile ~/.myapp-profile open myapp.com
|
||||
# Or via env var
|
||||
AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com
|
||||
\`\`\`
|
||||
- Use different profile paths for different projects
|
||||
- Login once → restart browser → still logged in
|
||||
- Stores: cookies, localStorage, IndexedDB, service workers, browser cache
|
||||
|
||||
## JSON output (for parsing)
|
||||
|
||||
Add \`--json\` for machine-readable output:
|
||||
\`\`\`bash
|
||||
agent-browser snapshot -i --json
|
||||
agent-browser get text @e1 --json
|
||||
\`\`\`
|
||||
|
||||
## Debugging
|
||||
|
||||
\`\`\`bash
|
||||
agent-browser open example.com --headed # Show browser window
|
||||
agent-browser console # View console messages
|
||||
agent-browser errors # View page errors
|
||||
agent-browser record start ./debug.webm # Record from current page
|
||||
agent-browser record stop # Save recording
|
||||
agent-browser connect 9222 # Local CDP port
|
||||
agent-browser --cdp "wss://browser-service.com/cdp?token=..." snapshot # Remote via WebSocket
|
||||
agent-browser console --clear # Clear console
|
||||
agent-browser errors --clear # Clear errors
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser trace start # Start recording trace
|
||||
agent-browser trace stop trace.zip # Stop and save trace
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
Install: \`bun add -g agent-browser && agent-browser install\`. Run \`agent-browser --help\` for all commands. Repo: https://github.com/vercel-labs/agent-browser`,
|
||||
allowedTools: ["Bash(agent-browser:*)"],
|
||||
}
|
||||
|
||||
const frontendUiUxSkill: BuiltinSkill = {
|
||||
name: "frontend-ui-ux",
|
||||
description: "Designer-turned-developer who crafts stunning UI/UX even without design mockups",
|
||||
@@ -95,7 +393,7 @@ Interpret creatively and make unexpected choices that feel genuinely designed fo
|
||||
const gitMasterSkill: BuiltinSkill = {
|
||||
name: "git-master",
|
||||
description:
|
||||
"MUST USE for ANY git operations. Atomic commits, rebase/squash, history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with delegate_task(category='quick', skills=['git-master'], ...) to save context. Triggers: 'commit', 'rebase', 'squash', 'who wrote', 'when was X added', 'find the commit that'.",
|
||||
"MUST USE for ANY git operations. Atomic commits, rebase/squash, history search (blame, bisect, log -S). STRONGLY RECOMMENDED: Use with delegate_task(category='quick', load_skills=['git-master'], ...) to save context. Triggers: 'commit', 'rebase', 'squash', 'who wrote', 'when was X added', 'find the commit that'.",
|
||||
template: `# Git Master Agent
|
||||
|
||||
You are a Git expert combining three specializations:
|
||||
@@ -1198,6 +1496,234 @@ POTENTIAL ACTIONS:
|
||||
- Bisect without proper good/bad boundaries -> Wasted time`,
|
||||
}
|
||||
|
||||
export function createBuiltinSkills(): BuiltinSkill[] {
|
||||
return [playwrightSkill, frontendUiUxSkill, gitMasterSkill]
|
||||
const devBrowserSkill: BuiltinSkill = {
|
||||
name: "dev-browser",
|
||||
description:
|
||||
"Browser automation with persistent page state. Use when users ask to navigate websites, fill forms, take screenshots, extract web data, test web apps, or automate browser workflows. Trigger phrases include 'go to [url]', 'click on', 'fill out the form', 'take a screenshot', 'scrape', 'automate', 'test the website', 'log into', or any browser interaction request.",
|
||||
template: `# Dev Browser Skill
|
||||
|
||||
Browser automation that maintains page state across script executions. Write small, focused scripts to accomplish tasks incrementally. Once you've proven out part of a workflow and there is repeated work to be done, you can write a script to do the repeated work in a single execution.
|
||||
|
||||
## Choosing Your Approach
|
||||
|
||||
- **Local/source-available sites**: Read the source code first to write selectors directly
|
||||
- **Unknown page layouts**: Use \`getAISnapshot()\` to discover elements and \`selectSnapshotRef()\` to interact with them
|
||||
- **Visual feedback**: Take screenshots to see what the user sees
|
||||
|
||||
## Setup
|
||||
|
||||
**IMPORTANT**: Before using this skill, ensure the server is running. See [references/installation.md](references/installation.md) for platform-specific setup instructions (macOS, Linux, Windows).
|
||||
|
||||
Two modes available. Ask the user if unclear which to use.
|
||||
|
||||
### Standalone Mode (Default)
|
||||
|
||||
Launches a new Chromium browser for fresh automation sessions.
|
||||
|
||||
**macOS/Linux:**
|
||||
\`\`\`bash
|
||||
./skills/dev-browser/server.sh &
|
||||
\`\`\`
|
||||
|
||||
**Windows (PowerShell):**
|
||||
\`\`\`powershell
|
||||
Start-Process -NoNewWindow -FilePath "node" -ArgumentList "skills/dev-browser/server.js"
|
||||
\`\`\`
|
||||
|
||||
Add \`--headless\` flag if user requests it. **Wait for the \`Ready\` message before running scripts.**
|
||||
|
||||
### Extension Mode
|
||||
|
||||
Connects to user's existing Chrome browser. Use this when:
|
||||
|
||||
- The user is already logged into sites and wants you to do things behind an authed experience that isn't local dev.
|
||||
- The user asks you to use the extension
|
||||
|
||||
**Important**: The core flow is still the same. You create named pages inside of their browser.
|
||||
|
||||
**Start the relay server:**
|
||||
|
||||
**macOS/Linux:**
|
||||
\`\`\`bash
|
||||
cd skills/dev-browser && npm i && npm run start-extension &
|
||||
\`\`\`
|
||||
|
||||
**Windows (PowerShell):**
|
||||
\`\`\`powershell
|
||||
cd skills/dev-browser; npm i; Start-Process -NoNewWindow -FilePath "npm" -ArgumentList "run", "start-extension"
|
||||
\`\`\`
|
||||
|
||||
Wait for \`Waiting for extension to connect...\` followed by \`Extension connected\` in the console.
|
||||
|
||||
If the extension hasn't connected yet, tell the user to launch and activate it. Download link: https://github.com/SawyerHood/dev-browser/releases
|
||||
|
||||
## Writing Scripts
|
||||
|
||||
> **Run all scripts from \`skills/dev-browser/\` directory.** The \`@/\` import alias requires this directory's config.
|
||||
|
||||
Execute scripts inline using heredocs:
|
||||
|
||||
**macOS/Linux:**
|
||||
\`\`\`bash
|
||||
cd skills/dev-browser && npx tsx <<'EOF'
|
||||
import { connect, waitForPageLoad } from "@/client.js";
|
||||
|
||||
const client = await connect();
|
||||
const page = await client.page("example", { viewport: { width: 1920, height: 1080 } });
|
||||
|
||||
await page.goto("https://example.com");
|
||||
await waitForPageLoad(page);
|
||||
|
||||
console.log({ title: await page.title(), url: page.url() });
|
||||
await client.disconnect();
|
||||
EOF
|
||||
\`\`\`
|
||||
|
||||
**Windows (PowerShell):**
|
||||
\`\`\`powershell
|
||||
cd skills/dev-browser
|
||||
@"
|
||||
import { connect, waitForPageLoad } from "@/client.js";
|
||||
|
||||
const client = await connect();
|
||||
const page = await client.page("example", { viewport: { width: 1920, height: 1080 } });
|
||||
|
||||
await page.goto("https://example.com");
|
||||
await waitForPageLoad(page);
|
||||
|
||||
console.log({ title: await page.title(), url: page.url() });
|
||||
await client.disconnect();
|
||||
"@ | npx tsx --input-type=module
|
||||
\`\`\`
|
||||
|
||||
### Key Principles
|
||||
|
||||
1. **Small scripts**: Each script does ONE thing (navigate, click, fill, check)
|
||||
2. **Evaluate state**: Log/return state at the end to decide next steps
|
||||
3. **Descriptive page names**: Use \`"checkout"\`, \`"login"\`, not \`"main"\`
|
||||
4. **Disconnect to exit**: \`await client.disconnect()\` - pages persist on server
|
||||
5. **Plain JS in evaluate**: \`page.evaluate()\` runs in browser - no TypeScript syntax
|
||||
|
||||
## Workflow Loop
|
||||
|
||||
1. **Write a script** to perform one action
|
||||
2. **Run it** and observe the output
|
||||
3. **Evaluate** - did it work? What's the current state?
|
||||
4. **Decide** - is the task complete or do we need another script?
|
||||
5. **Repeat** until task is done
|
||||
|
||||
### No TypeScript in Browser Context
|
||||
|
||||
Code passed to \`page.evaluate()\` runs in the browser, which doesn't understand TypeScript:
|
||||
|
||||
\`\`\`typescript
|
||||
// Correct: plain JavaScript
|
||||
const text = await page.evaluate(() => {
|
||||
return document.body.innerText;
|
||||
});
|
||||
|
||||
// Wrong: TypeScript syntax will fail at runtime
|
||||
const text = await page.evaluate(() => {
|
||||
const el: HTMLElement = document.body; // Type annotation breaks in browser!
|
||||
return el.innerText;
|
||||
});
|
||||
\`\`\`
|
||||
|
||||
## Scraping Data
|
||||
|
||||
For scraping large datasets, intercept and replay network requests rather than scrolling the DOM. See [references/scraping.md](references/scraping.md) for the complete guide.
|
||||
|
||||
## Client API
|
||||
|
||||
\`\`\`typescript
|
||||
const client = await connect();
|
||||
|
||||
// Get or create named page
|
||||
const page = await client.page("name");
|
||||
const pageWithSize = await client.page("name", { viewport: { width: 1920, height: 1080 } });
|
||||
|
||||
const pages = await client.list(); // List all page names
|
||||
await client.close("name"); // Close a page
|
||||
await client.disconnect(); // Disconnect (pages persist)
|
||||
|
||||
// ARIA Snapshot methods
|
||||
const snapshot = await client.getAISnapshot("name"); // Get accessibility tree
|
||||
const element = await client.selectSnapshotRef("name", "e5"); // Get element by ref
|
||||
\`\`\`
|
||||
|
||||
## Waiting
|
||||
|
||||
\`\`\`typescript
|
||||
import { waitForPageLoad } from "@/client.js";
|
||||
|
||||
await waitForPageLoad(page); // After navigation
|
||||
await page.waitForSelector(".results"); // For specific elements
|
||||
await page.waitForURL("**/success"); // For specific URL
|
||||
\`\`\`
|
||||
|
||||
## Screenshots
|
||||
|
||||
\`\`\`typescript
|
||||
await page.screenshot({ path: "tmp/screenshot.png" });
|
||||
await page.screenshot({ path: "tmp/full.png", fullPage: true });
|
||||
\`\`\`
|
||||
|
||||
## ARIA Snapshot (Element Discovery)
|
||||
|
||||
Use \`getAISnapshot()\` to discover page elements. Returns YAML-formatted accessibility tree:
|
||||
|
||||
\`\`\`yaml
|
||||
- banner:
|
||||
- link "Hacker News" [ref=e1]
|
||||
- navigation:
|
||||
- link "new" [ref=e2]
|
||||
- main:
|
||||
- list:
|
||||
- listitem:
|
||||
- link "Article Title" [ref=e8]
|
||||
\`\`\`
|
||||
|
||||
**Interacting with refs:**
|
||||
|
||||
\`\`\`typescript
|
||||
const snapshot = await client.getAISnapshot("hackernews");
|
||||
console.log(snapshot); // Find the ref you need
|
||||
|
||||
const element = await client.selectSnapshotRef("hackernews", "e2");
|
||||
await element.click();
|
||||
\`\`\`
|
||||
|
||||
## Error Recovery
|
||||
|
||||
Page state persists after failures. Debug with:
|
||||
|
||||
\`\`\`bash
|
||||
cd skills/dev-browser && npx tsx <<'EOF'
|
||||
import { connect } from "@/client.js";
|
||||
|
||||
const client = await connect();
|
||||
const page = await client.page("hackernews");
|
||||
|
||||
await page.screenshot({ path: "tmp/debug.png" });
|
||||
console.log({
|
||||
url: page.url(),
|
||||
title: await page.title(),
|
||||
bodyText: await page.textContent("body").then((t) => t?.slice(0, 200)),
|
||||
});
|
||||
|
||||
await client.disconnect();
|
||||
EOF
|
||||
\`\`\``,
|
||||
}
|
||||
|
||||
export interface CreateBuiltinSkillsOptions {
|
||||
browserProvider?: BrowserAutomationProvider
|
||||
}
|
||||
|
||||
export function createBuiltinSkills(options: CreateBuiltinSkillsOptions = {}): BuiltinSkill[] {
|
||||
const { browserProvider = "playwright" } = options
|
||||
|
||||
const browserSkill = browserProvider === "agent-browser" ? agentBrowserSkill : playwrightSkill
|
||||
|
||||
return [browserSkill, frontendUiUxSkill, gitMasterSkill, devBrowserSkill]
|
||||
}
|
||||
|
||||
@@ -77,7 +77,13 @@ export async function loadMcpConfigs(): Promise<McpLoadResult> {
|
||||
|
||||
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
||||
if (serverConfig.disabled) {
|
||||
log(`Skipping disabled MCP server "${name}"`, { path })
|
||||
log(`Disabling MCP server "${name}"`, { path })
|
||||
delete servers[name]
|
||||
const existingIndex = loadedServers.findIndex((s) => s.name === name)
|
||||
if (existingIndex !== -1) {
|
||||
loadedServers.splice(existingIndex, 1)
|
||||
log(`Removed previously loaded MCP server "${name}"`, { path })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ export interface ClaudeCodeMcpServer {
|
||||
args?: string[]
|
||||
env?: Record<string, string>
|
||||
headers?: Record<string, string>
|
||||
oauth?: {
|
||||
clientId?: string
|
||||
scopes?: string[]
|
||||
}
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, test, expect, beforeEach } from "bun:test"
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import {
|
||||
setSessionAgent,
|
||||
getSessionAgent,
|
||||
@@ -13,9 +13,11 @@ describe("claude-code-session-state", () => {
|
||||
beforeEach(() => {
|
||||
// #given - clean state before each test
|
||||
_resetForTesting()
|
||||
clearSessionAgent("test-session-1")
|
||||
clearSessionAgent("test-session-2")
|
||||
clearSessionAgent("test-prometheus-session")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// #then - cleanup after each test to prevent pollution
|
||||
_resetForTesting()
|
||||
})
|
||||
|
||||
describe("setSessionAgent", () => {
|
||||
@@ -37,7 +39,7 @@ describe("claude-code-session-state", () => {
|
||||
setSessionAgent(sessionID, "Prometheus (Planner)")
|
||||
|
||||
// #when - try to overwrite
|
||||
setSessionAgent(sessionID, "Sisyphus")
|
||||
setSessionAgent(sessionID, "sisyphus")
|
||||
|
||||
// #then - first agent preserved
|
||||
expect(getSessionAgent(sessionID)).toBe("Prometheus (Planner)")
|
||||
@@ -58,10 +60,10 @@ describe("claude-code-session-state", () => {
|
||||
setSessionAgent(sessionID, "Prometheus (Planner)")
|
||||
|
||||
// #when - force update
|
||||
updateSessionAgent(sessionID, "Sisyphus")
|
||||
updateSessionAgent(sessionID, "sisyphus")
|
||||
|
||||
// #then
|
||||
expect(getSessionAgent(sessionID)).toBe("Sisyphus")
|
||||
expect(getSessionAgent(sessionID)).toBe("sisyphus")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -92,9 +94,9 @@ describe("claude-code-session-state", () => {
|
||||
expect(getMainSessionID()).toBe(mainID)
|
||||
})
|
||||
|
||||
test.skip("should return undefined when not set", () => {
|
||||
// #given - not set
|
||||
// TODO: Fix flaky test - parallel test execution causes state pollution
|
||||
test("should return undefined when not set", () => {
|
||||
// #given - explicit reset to ensure clean state (parallel test isolation)
|
||||
_resetForTesting()
|
||||
// #then
|
||||
expect(getMainSessionID()).toBeUndefined()
|
||||
})
|
||||
@@ -129,7 +131,7 @@ describe("claude-code-session-state", () => {
|
||||
// #given - user switches to custom agent "MyCustomAgent"
|
||||
const sessionID = "test-session-custom"
|
||||
const customAgent = "MyCustomAgent"
|
||||
const defaultAgent = "Sisyphus"
|
||||
const defaultAgent = "sisyphus"
|
||||
|
||||
// User switches to custom agent (via UI)
|
||||
setSessionAgent(sessionID, customAgent)
|
||||
|
||||
@@ -14,6 +14,7 @@ export function getMainSessionID(): string | undefined {
|
||||
export function _resetForTesting(): void {
|
||||
_mainSessionID = undefined
|
||||
subagentSessions.clear()
|
||||
sessionAgentMap.clear()
|
||||
}
|
||||
|
||||
const sessionAgentMap = new Map<string, string>()
|
||||
|
||||
@@ -21,7 +21,7 @@ describe("createContextInjectorMessagesTransformHook", () => {
|
||||
sessionID,
|
||||
role,
|
||||
time: { created: Date.now() },
|
||||
agent: "Sisyphus",
|
||||
agent: "sisyphus",
|
||||
model: { providerID: "test", modelID: "test" },
|
||||
path: { cwd: "/", root: "/" },
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { MessageMeta, OriginalMessageContext, TextPart, ToolPermission } fr
|
||||
|
||||
export interface StoredMessage {
|
||||
agent?: string
|
||||
model?: { providerID?: string; modelID?: string }
|
||||
model?: { providerID?: string; modelID?: string; variant?: string }
|
||||
tools?: Record<string, ToolPermission>
|
||||
}
|
||||
|
||||
@@ -141,9 +141,17 @@ export function injectHookMessage(
|
||||
const resolvedAgent = originalMessage.agent ?? fallback?.agent ?? "general"
|
||||
const resolvedModel =
|
||||
originalMessage.model?.providerID && originalMessage.model?.modelID
|
||||
? { providerID: originalMessage.model.providerID, modelID: originalMessage.model.modelID }
|
||||
? {
|
||||
providerID: originalMessage.model.providerID,
|
||||
modelID: originalMessage.model.modelID,
|
||||
...(originalMessage.model.variant ? { variant: originalMessage.model.variant } : {})
|
||||
}
|
||||
: fallback?.model?.providerID && fallback?.model?.modelID
|
||||
? { providerID: fallback.model.providerID, modelID: fallback.model.modelID }
|
||||
? {
|
||||
providerID: fallback.model.providerID,
|
||||
modelID: fallback.model.modelID,
|
||||
...(fallback.model.variant ? { variant: fallback.model.variant } : {})
|
||||
}
|
||||
: undefined
|
||||
const resolvedTools = originalMessage.tools ?? fallback?.tools
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user