Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2920d5fe65 | ||
|
|
7fd52e27ce | ||
|
|
08481c046f | ||
|
|
192e8adf18 | ||
|
|
5dd4d97c94 | ||
|
|
b1abb7999b | ||
|
|
8618d57d95 | ||
|
|
4b6b725f13 | ||
|
|
1aaa6e6ba2 | ||
|
|
7cb8210e65 | ||
|
|
7e4b633bbd | ||
|
|
f44555a021 | ||
|
|
cccc7b7443 | ||
|
|
056b144174 | ||
|
|
7fef07da2e | ||
|
|
62307d987c | ||
|
|
24f2ee0c92 | ||
|
|
e836ad18ce | ||
|
|
0c237064b5 | ||
|
|
58279897ae | ||
|
|
3e4d3fafd2 | ||
|
|
f1b9a38698 | ||
|
|
d1f6f9d41f | ||
|
|
4b35bf795a | ||
|
|
3adedca810 | ||
|
|
3dea568007 | ||
|
|
00b938d20d | ||
|
|
35d53cc74a | ||
|
|
9a1a22d1c5 | ||
|
|
96088381e2 | ||
|
|
c2d6e03b92 | ||
|
|
7f27fbc890 | ||
|
|
2806c64675 | ||
|
|
ed76c502c3 | ||
|
|
c4f2b63890 | ||
|
|
030277b8dd | ||
|
|
5e8e42fb74 | ||
|
|
a633c4dfbe | ||
|
|
0c8a500de4 | ||
|
|
2292a61887 | ||
|
|
d1a527c700 | ||
|
|
0fcfe21b27 | ||
|
|
25a5c2eeb4 | ||
|
|
521bcd5667 | ||
|
|
d3e317663e | ||
|
|
7938316a61 | ||
|
|
8a7469ef2b | ||
|
|
dba0c46417 | ||
|
|
fcf3f0cc7f | ||
|
|
b00b8238f4 | ||
|
|
53d8cf12f2 | ||
|
|
8681f16c52 | ||
|
|
ed66ba5f55 | ||
|
|
0f2bd63732 | ||
|
|
bc20853d83 | ||
|
|
7882f77a90 | ||
|
|
92c69f4167 | ||
|
|
27403f2682 | ||
|
|
44ce343708 | ||
|
|
ff48ac0745 | ||
|
|
b24b00fad2 | ||
|
|
f3b2fccba7 | ||
|
|
2c6dfeadce | ||
|
|
64b53c0e1c |
BIN
.github/assets/hero.jpg
vendored
Normal file
BIN
.github/assets/hero.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 805 KiB |
BIN
.github/assets/preview.png
vendored
Normal file
BIN
.github/assets/preview.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1021 KiB |
84
.opencode/command/get-unpublished-changes.md
Normal file
84
.opencode/command/get-unpublished-changes.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
description: Compare HEAD with the latest published npm version and list all unpublished changes
|
||||
model: anthropic/claude-haiku-4-5
|
||||
---
|
||||
|
||||
<command-instruction>
|
||||
IMMEDIATELY output the analysis. NO questions. NO preamble.
|
||||
|
||||
## CRITICAL: DO NOT just copy commit messages!
|
||||
|
||||
For each commit, you MUST:
|
||||
1. Read the actual diff to understand WHAT CHANGED
|
||||
2. Describe the REAL change in plain language
|
||||
3. Explain WHY it matters (if not obvious)
|
||||
|
||||
## Steps:
|
||||
1. Run `git diff v{published-version}..HEAD` to see actual changes
|
||||
2. Group by type (feat/fix/refactor/docs) with REAL descriptions
|
||||
3. Note breaking changes if any
|
||||
4. Recommend version bump (major/minor/patch)
|
||||
|
||||
## Output Format:
|
||||
- feat: "Added X that does Y" (not just "add X feature")
|
||||
- fix: "Fixed bug where X happened, now Y" (not just "fix X bug")
|
||||
- refactor: "Changed X from A to B, now supports C" (not just "rename X")
|
||||
</command-instruction>
|
||||
|
||||
<version-context>
|
||||
<published-version>
|
||||
!`npm view oh-my-opencode version 2>/dev/null || echo "not published"`
|
||||
</published-version>
|
||||
<local-version>
|
||||
!`node -p "require('./package.json').version" 2>/dev/null || echo "unknown"`
|
||||
</local-version>
|
||||
<latest-tag>
|
||||
!`git tag --sort=-v:refname | head -1 2>/dev/null || echo "no tags"`
|
||||
</latest-tag>
|
||||
</version-context>
|
||||
|
||||
<git-context>
|
||||
<commits-since-release>
|
||||
!`npm view oh-my-opencode version 2>/dev/null | xargs -I{} git log "v{}"..HEAD --oneline 2>/dev/null || echo "no commits since release"`
|
||||
</commits-since-release>
|
||||
<diff-stat>
|
||||
!`npm view oh-my-opencode version 2>/dev/null | xargs -I{} git diff "v{}"..HEAD --stat 2>/dev/null || echo "no diff available"`
|
||||
</diff-stat>
|
||||
<files-changed-summary>
|
||||
!`npm view oh-my-opencode version 2>/dev/null | xargs -I{} git diff "v{}"..HEAD --stat 2>/dev/null | tail -1 || echo ""`
|
||||
</files-changed-summary>
|
||||
</git-context>
|
||||
|
||||
<output-format>
|
||||
## Unpublished Changes (v{published} → HEAD)
|
||||
|
||||
### feat
|
||||
| Scope | What Changed |
|
||||
|-------|--------------|
|
||||
| X | 실제 변경 내용 설명 |
|
||||
|
||||
### fix
|
||||
| Scope | What Changed |
|
||||
|-------|--------------|
|
||||
| X | 실제 변경 내용 설명 |
|
||||
|
||||
### refactor
|
||||
| Scope | What Changed |
|
||||
|-------|--------------|
|
||||
| X | 실제 변경 내용 설명 |
|
||||
|
||||
### docs
|
||||
| Scope | What Changed |
|
||||
|-------|--------------|
|
||||
| X | 실제 변경 내용 설명 |
|
||||
|
||||
### Breaking Changes
|
||||
None 또는 목록
|
||||
|
||||
### Files Changed
|
||||
{diff-stat}
|
||||
|
||||
### Suggested Version Bump
|
||||
- **Recommendation**: patch|minor|major
|
||||
- **Reason**: 이유
|
||||
</output-format>
|
||||
37
.opencode/command/omomomo.md
Normal file
37
.opencode/command/omomomo.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
description: Easter egg command - about oh-my-opencode
|
||||
---
|
||||
|
||||
<command-instruction>
|
||||
You found an easter egg! 🥚✨
|
||||
|
||||
Print the following message to the user EXACTLY as written (in a friendly, celebratory tone):
|
||||
|
||||
---
|
||||
|
||||
# 🎉 oMoMoMoMoMo···
|
||||
|
||||
**You found the easter egg!** 🥚✨
|
||||
|
||||
## What is Oh My OpenCode?
|
||||
|
||||
**Oh My OpenCode** is a powerful OpenCode plugin that transforms your AI agent into a full development team:
|
||||
|
||||
- 🤖 **Multi-Agent Orchestration**: Oracle (GPT-5.2), Librarian (Claude), Explore (Grok), Frontend Engineer (Gemini), and more
|
||||
- 🔧 **LSP Tools**: Full IDE capabilities for your agents - hover, goto definition, find references, rename, code actions
|
||||
- 🔍 **AST-Grep**: Structural code search and replace across 25 languages
|
||||
- 📚 **Built-in MCPs**: Context7 for docs, Exa for web search, grep.app for GitHub code search
|
||||
- 🔄 **Background Agents**: Run multiple agents in parallel like a real dev team
|
||||
- 🎯 **Claude Code Compatibility**: Your existing Claude Code config just works
|
||||
|
||||
## Who Made This?
|
||||
|
||||
Created with ❤️ by **[code-yeongyu](https://github.com/code-yeongyu)**
|
||||
|
||||
🔗 **GitHub**: https://github.com/code-yeongyu/oh-my-opencode
|
||||
|
||||
---
|
||||
|
||||
*Enjoy coding on steroids!* 🚀
|
||||
|
||||
</command-instruction>
|
||||
258
.opencode/command/publish.md
Normal file
258
.opencode/command/publish.md
Normal file
@@ -0,0 +1,258 @@
|
||||
---
|
||||
description: Publish oh-my-opencode to npm via GitHub Actions workflow
|
||||
argument-hint: <patch|minor|major>
|
||||
model: opencode/big-pickle
|
||||
---
|
||||
|
||||
<command-instruction>
|
||||
You are the release manager for oh-my-opencode. Execute the FULL publish workflow from start to finish.
|
||||
|
||||
## CRITICAL: ARGUMENT REQUIREMENT
|
||||
|
||||
**You MUST receive a version bump type from the user.** Valid options:
|
||||
- `patch`: Bug fixes, backward-compatible (1.1.7 → 1.1.8)
|
||||
- `minor`: New features, backward-compatible (1.1.7 → 1.2.0)
|
||||
- `major`: Breaking changes (1.1.7 → 2.0.0)
|
||||
|
||||
**If the user did not provide a bump type argument, STOP IMMEDIATELY and ask:**
|
||||
> "배포를 진행하려면 버전 범프 타입을 지정해주세요: `patch`, `minor`, 또는 `major`"
|
||||
|
||||
**DO NOT PROCEED without explicit user confirmation of bump type.**
|
||||
|
||||
---
|
||||
|
||||
## STEP 0: REGISTER TODO LIST (MANDATORY FIRST ACTION)
|
||||
|
||||
**Before doing ANYTHING else**, create a detailed todo list using TodoWrite:
|
||||
|
||||
```
|
||||
[
|
||||
{ "id": "confirm-bump", "content": "Confirm version bump type with user (patch/minor/major)", "status": "in_progress", "priority": "high" },
|
||||
{ "id": "check-uncommitted", "content": "Check for uncommitted changes and commit if needed", "status": "pending", "priority": "high" },
|
||||
{ "id": "sync-remote", "content": "Sync with remote (pull --rebase && push if unpushed commits)", "status": "pending", "priority": "high" },
|
||||
{ "id": "run-workflow", "content": "Trigger GitHub Actions publish workflow", "status": "pending", "priority": "high" },
|
||||
{ "id": "wait-workflow", "content": "Wait for workflow completion (poll every 30s)", "status": "pending", "priority": "high" },
|
||||
{ "id": "verify-release", "content": "Verify GitHub release was created", "status": "pending", "priority": "high" },
|
||||
{ "id": "draft-release-notes", "content": "Draft enhanced release notes content", "status": "pending", "priority": "high" },
|
||||
{ "id": "update-release-notes", "content": "Update GitHub release with enhanced notes", "status": "pending", "priority": "high" },
|
||||
{ "id": "verify-npm", "content": "Verify npm package published successfully", "status": "pending", "priority": "high" },
|
||||
{ "id": "final-confirmation", "content": "Final confirmation to user with links", "status": "pending", "priority": "low" }
|
||||
]
|
||||
```
|
||||
|
||||
**Mark each todo as `in_progress` when starting, `completed` when done. ONE AT A TIME.**
|
||||
|
||||
---
|
||||
|
||||
## STEP 1: CONFIRM BUMP TYPE
|
||||
|
||||
If bump type provided as argument, confirm with user:
|
||||
> "버전 범프 타입: `{bump}`. 진행할까요? (y/n)"
|
||||
|
||||
Wait for user confirmation before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## STEP 2: CHECK UNCOMMITTED CHANGES
|
||||
|
||||
Run: `git status --porcelain`
|
||||
|
||||
- If there are uncommitted changes, warn user and ask if they want to commit first
|
||||
- If clean, proceed
|
||||
|
||||
---
|
||||
|
||||
## STEP 2.5: SYNC WITH REMOTE (MANDATORY)
|
||||
|
||||
Check if there are unpushed commits:
|
||||
```bash
|
||||
git log origin/master..HEAD --oneline
|
||||
```
|
||||
|
||||
**If there are unpushed commits, you MUST sync before triggering workflow:**
|
||||
```bash
|
||||
git pull --rebase && git push
|
||||
```
|
||||
|
||||
This ensures the GitHub Actions workflow runs on the latest code including all local commits.
|
||||
|
||||
---
|
||||
|
||||
## STEP 3: TRIGGER GITHUB ACTIONS WORKFLOW
|
||||
|
||||
Run the publish workflow:
|
||||
```bash
|
||||
gh workflow run publish -f bump={bump_type}
|
||||
```
|
||||
|
||||
Wait 3 seconds, then get the run ID:
|
||||
```bash
|
||||
gh run list --workflow=publish --limit=1 --json databaseId,status --jq '.[0]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## STEP 4: WAIT FOR WORKFLOW COMPLETION
|
||||
|
||||
Poll workflow status every 30 seconds until completion:
|
||||
```bash
|
||||
gh run view {run_id} --json status,conclusion --jq '{status: .status, conclusion: .conclusion}'
|
||||
```
|
||||
|
||||
Status flow: `queued` → `in_progress` → `completed`
|
||||
|
||||
**IMPORTANT: Use polling loop, NOT sleep commands.**
|
||||
|
||||
If conclusion is `failure`, show error and stop:
|
||||
```bash
|
||||
gh run view {run_id} --log-failed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## STEP 5: VERIFY GITHUB RELEASE
|
||||
|
||||
Get the new version and verify release exists:
|
||||
```bash
|
||||
# Get new version from package.json (workflow updates it)
|
||||
git pull --rebase
|
||||
NEW_VERSION=$(node -p "require('./package.json').version")
|
||||
gh release view "v${NEW_VERSION}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## STEP 6: DRAFT ENHANCED RELEASE NOTES
|
||||
|
||||
Analyze commits since the previous version and draft release notes following project conventions:
|
||||
|
||||
### For PATCH releases:
|
||||
Keep simple format - just list commits:
|
||||
```markdown
|
||||
- {hash} {conventional commit message}
|
||||
- ...
|
||||
```
|
||||
|
||||
### For MINOR releases:
|
||||
Use feature-focused format:
|
||||
```markdown
|
||||
## New Features
|
||||
|
||||
### Feature Name
|
||||
- Description of what it does
|
||||
- Why it matters
|
||||
|
||||
## Bug Fixes
|
||||
- fix(scope): description
|
||||
|
||||
## Improvements
|
||||
- refactor(scope): description
|
||||
```
|
||||
|
||||
### For MAJOR releases:
|
||||
Full changelog format:
|
||||
```markdown
|
||||
# v{version}
|
||||
|
||||
Brief description of the release.
|
||||
|
||||
## What's New Since v{previous}
|
||||
|
||||
### Breaking Changes
|
||||
- Description of breaking change
|
||||
|
||||
### Features
|
||||
- **Feature Name**: Description
|
||||
|
||||
### Bug Fixes
|
||||
- Description
|
||||
|
||||
### Documentation
|
||||
- Description
|
||||
|
||||
## Migration Guide (if applicable)
|
||||
...
|
||||
```
|
||||
|
||||
**CRITICAL: The enhanced notes must ADD to existing workflow-generated notes, not replace them.**
|
||||
|
||||
---
|
||||
|
||||
## STEP 7: UPDATE GITHUB RELEASE
|
||||
|
||||
**ZERO CONTENT LOSS POLICY:**
|
||||
- First, fetch the existing release body with `gh release view`
|
||||
- Your enhanced notes must be PREPENDED to the existing content
|
||||
- **NOT A SINGLE CHARACTER of existing content may be removed or modified**
|
||||
- The final release body = `{your_enhanced_notes}\n\n---\n\n{existing_body_exactly_as_is}`
|
||||
|
||||
```bash
|
||||
# Get existing body
|
||||
EXISTING_BODY=$(gh release view "v${NEW_VERSION}" --json body --jq '.body')
|
||||
|
||||
# Write enhanced notes to temp file (prepend to existing)
|
||||
cat > /tmp/release-notes-v${NEW_VERSION}.md << 'EOF'
|
||||
{your_enhanced_notes}
|
||||
|
||||
---
|
||||
|
||||
EOF
|
||||
|
||||
# Append existing body EXACTLY as-is (zero modifications)
|
||||
echo "$EXISTING_BODY" >> /tmp/release-notes-v${NEW_VERSION}.md
|
||||
|
||||
# Update release
|
||||
gh release edit "v${NEW_VERSION}" --notes-file /tmp/release-notes-v${NEW_VERSION}.md
|
||||
```
|
||||
|
||||
**CRITICAL: This is ADDITIVE ONLY. You are adding your notes on top. The existing content remains 100% intact.**
|
||||
|
||||
---
|
||||
|
||||
## STEP 8: VERIFY NPM PUBLICATION
|
||||
|
||||
Poll npm registry until the new version appears:
|
||||
```bash
|
||||
npm view oh-my-opencode version
|
||||
```
|
||||
|
||||
Compare with expected version. If not matching after 2 minutes, warn user about npm propagation delay.
|
||||
|
||||
---
|
||||
|
||||
## STEP 9: FINAL CONFIRMATION
|
||||
|
||||
Report success to user with:
|
||||
- New version number
|
||||
- GitHub release URL: https://github.com/code-yeongyu/oh-my-opencode/releases/tag/v{version}
|
||||
- npm package URL: https://www.npmjs.com/package/oh-my-opencode
|
||||
|
||||
---
|
||||
|
||||
## ERROR HANDLING
|
||||
|
||||
- **Workflow fails**: Show failed logs, suggest checking Actions tab
|
||||
- **Release not found**: Wait and retry, may be propagation delay
|
||||
- **npm not updated**: npm can take 1-5 minutes to propagate, inform user
|
||||
- **Permission denied**: User may need to re-authenticate with `gh auth login`
|
||||
|
||||
## LANGUAGE
|
||||
|
||||
Respond to user in Korean (한국어).
|
||||
|
||||
</command-instruction>
|
||||
|
||||
<current-context>
|
||||
<published-version>
|
||||
!`npm view oh-my-opencode version 2>/dev/null || echo "not published"`
|
||||
</published-version>
|
||||
<local-version>
|
||||
!`node -p "require('./package.json').version" 2>/dev/null || echo "unknown"`
|
||||
</local-version>
|
||||
<git-status>
|
||||
!`git status --porcelain`
|
||||
</git-status>
|
||||
<recent-commits>
|
||||
!`npm view oh-my-opencode version 2>/dev/null | xargs -I{} git log "v{}"..HEAD --oneline 2>/dev/null | head -15 || echo "no commits"`
|
||||
</recent-commits>
|
||||
</current-context>
|
||||
111
AGENTS.md
111
AGENTS.md
@@ -1,76 +1,88 @@
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
**Generated:** 2025-12-05T01:16:20+09:00
|
||||
**Commit:** 6c9a2ee
|
||||
**Generated:** 2025-12-14T17:16:30+09:00
|
||||
**Commit:** 7f27fbc
|
||||
**Branch:** master
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
OpenCode plugin distribution implementing Claude Code/AmpCode features. Provides multi-model agent orchestration, LSP tools, AST-Grep search, and safe-grep utilities.
|
||||
OpenCode plugin implementing Claude Code/AmpCode features. Multi-model agent orchestration (GPT-5.2, Claude, Gemini, Grok), LSP tools (11), AST-Grep search, MCP integrations (context7, websearch_exa, grep_app). "oh-my-zsh" for OpenCode.
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
oh-my-opencode/
|
||||
├── src/
|
||||
│ ├── agents/ # AI agent definitions (oracle, librarian, explore, etc.)
|
||||
│ ├── hooks/ # Plugin lifecycle hooks
|
||||
│ ├── tools/ # LSP, AST-Grep, Safe-Grep tool implementations
|
||||
│ │ ├── lsp/ # 11 LSP tools (hover, definition, references, etc.)
|
||||
│ │ ├── ast-grep/ # AST-aware code search
|
||||
│ │ └── safe-grep/ # Safe grep with limits
|
||||
│ └── features/ # Terminal features
|
||||
├── dist/ # Build output (bun + tsc declarations)
|
||||
└── test-rule.yml # AST-Grep test rules
|
||||
│ ├── agents/ # AI agents (OmO, oracle, librarian, explore, frontend, document-writer, multimodal-looker)
|
||||
│ ├── hooks/ # 19 lifecycle hooks (comment-checker, rules-injector, keyword-detector, etc.)
|
||||
│ ├── tools/ # LSP (11), AST-Grep, Grep, background-task, glob, look-at, skill, slashcommand
|
||||
│ ├── mcp/ # MCP servers (context7, websearch_exa, grep_app)
|
||||
│ ├── features/ # Terminal features, Claude Code loaders (agent, command, skill, mcp, session-state)
|
||||
│ ├── config/ # Zod schema, TypeScript types
|
||||
│ ├── auth/ # Google Antigravity OAuth
|
||||
│ ├── shared/ # Utilities (deep-merge, pattern-matcher, logger, etc.)
|
||||
│ └── index.ts # Main plugin entry (OhMyOpenCodePlugin)
|
||||
├── script/ # build-schema.ts, publish.ts
|
||||
├── assets/ # JSON schema
|
||||
└── dist/ # Build output (ESM + .d.ts)
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| Add new agent | `src/agents/` | Export from index.ts |
|
||||
| Add new hook | `src/hooks/` | Export from index.ts |
|
||||
| Add new tool | `src/tools/` | Follow lsp/ pattern: index, types, tools, utils |
|
||||
| Modify LSP behavior | `src/tools/lsp/` | client.ts for connection logic |
|
||||
| AST-Grep patterns | `src/tools/ast-grep/` | napi.ts for @ast-grep/napi |
|
||||
| Terminal features | `src/features/terminal/` | title.ts |
|
||||
| Google Antigravity auth | `src/auth/antigravity/` | OAuth plugin for Google models |
|
||||
| Add new agent | `src/agents/` | Create .ts file, add to builtinAgents in index.ts, update types.ts |
|
||||
| Add new hook | `src/hooks/` | Create dir with createXXXHook(), export from index.ts |
|
||||
| Add new tool | `src/tools/` | Dir with index/types/constants/tools.ts, add to builtinTools |
|
||||
| Add MCP server | `src/mcp/` | Create config, add to index.ts |
|
||||
| Modify LSP behavior | `src/tools/lsp/` | client.ts for connection, tools.ts for handlers |
|
||||
| AST-Grep patterns | `src/tools/ast-grep/` | napi.ts for @ast-grep/napi binding |
|
||||
| Google OAuth | `src/auth/antigravity/` | OAuth plugin for Google models |
|
||||
| Config schema | `src/config/schema.ts` | Zod schema, run `bun run build:schema` after changes |
|
||||
| Claude Code compat | `src/features/claude-code-*-loader/` | Command, skill, agent, mcp loaders |
|
||||
|
||||
## CONVENTIONS
|
||||
|
||||
- **Package manager**: Bun only (not npm/yarn)
|
||||
- **Build**: Dual output - `bun build` + `tsc --emitDeclarationOnly`
|
||||
- **Package manager**: Bun only (`bun run`, `bun build`, `bunx`)
|
||||
- **Types**: bun-types (not @types/node)
|
||||
- **Build**: Dual output - `bun build` (ESM) + `tsc --emitDeclarationOnly`
|
||||
- **Exports**: Barrel pattern - `export * from "./module"` in index.ts
|
||||
- **Module structure**: index.ts, types.ts, constants.ts, utils.ts, tools.ts per tool
|
||||
- **Directory naming**: kebab-case (`ast-grep/`, `claude-code-hooks/`)
|
||||
- **Tool structure**: Each tool has index.ts, types.ts, constants.ts, tools.ts, utils.ts
|
||||
- **Hook pattern**: `createXXXHook(input: PluginInput)` returning event handlers
|
||||
|
||||
## ANTI-PATTERNS (THIS PROJECT)
|
||||
|
||||
- **Bash file operations**: Never use mkdir/touch/rm/cp/mv for file creation
|
||||
- **npm/yarn**: Use bun exclusively
|
||||
- **@types/node**: Use bun-types instead
|
||||
- **@types/node**: Use bun-types
|
||||
- **Bash file operations**: Never use mkdir/touch/rm/cp/mv for file creation in code
|
||||
- **Generic AI aesthetics**: No Space Grotesk, avoid typical AI-generated UI patterns
|
||||
- **Direct bun publish**: Use GitHub Actions workflow_dispatch only (OIDC provenance)
|
||||
- **Local version bump**: Version managed by CI workflow, never modify locally
|
||||
- **Rush completion**: Never mark tasks complete without verification
|
||||
- **Interrupting work**: Complete tasks fully before stopping
|
||||
|
||||
## UNIQUE STYLES
|
||||
|
||||
- **Directory naming**: kebab-case (`ast-grep/`, `safe-grep/`)
|
||||
- **Tool organization**: Each tool has cli.ts, constants.ts, index.ts, napi.ts/tools.ts, types.ts, utils.ts
|
||||
- **Platform handling**: Union type `"darwin" | "linux" | "win32" | "unsupported"`
|
||||
- **Error handling**: Consistent try/catch with async/await
|
||||
- **Optional props**: Extensive use of `?` for optional interface properties
|
||||
- **Flexible objects**: `Record<string, unknown>` for dynamic configs
|
||||
- **Error handling**: Consistent try/catch with async/await in all tools
|
||||
- **Agent tools restriction**: Use `tools: { include: [...] }` or `tools: { exclude: [...] }`
|
||||
- **Temperature**: Most agents use `0.1` for consistency
|
||||
- **Hook naming**: `createXXXHook` function naming convention
|
||||
|
||||
## AGENT MODELS
|
||||
|
||||
| Agent | Model | Purpose |
|
||||
|-------|-------|---------|
|
||||
| oracle | GPT-5.2 | Code review, strategic planning |
|
||||
| librarian | Claude Haiku | Documentation, example lookup |
|
||||
| explore | Grok | File/codebase exploration |
|
||||
| frontend-ui-ux-engineer | Gemini | UI generation |
|
||||
| document-writer | Gemini | Documentation writing |
|
||||
| OmO | anthropic/claude-opus-4-5 | Primary orchestrator, team leader |
|
||||
| oracle | openai/gpt-5.2 | Strategic advisor, code review, architecture |
|
||||
| librarian | opencode/big-pickle | Multi-repo analysis, docs lookup, GitHub examples |
|
||||
| explore | opencode/grok-code | Fast codebase exploration, file patterns |
|
||||
| frontend-ui-ux-engineer | google/gemini-3-pro-preview | UI generation, design-focused |
|
||||
| document-writer | google/gemini-3-pro-preview | Technical documentation |
|
||||
| multimodal-looker | google/gemini-2.5-flash | PDF/image/diagram analysis |
|
||||
|
||||
## COMMANDS
|
||||
|
||||
@@ -78,38 +90,43 @@ oh-my-opencode/
|
||||
# Type check
|
||||
bun run typecheck
|
||||
|
||||
# Build
|
||||
# Build (ESM + declarations + schema)
|
||||
bun run build
|
||||
|
||||
# Clean + Build
|
||||
bun run rebuild
|
||||
|
||||
# Build schema only
|
||||
bun run build:schema
|
||||
```
|
||||
|
||||
## DEPLOYMENT
|
||||
|
||||
**배포는 GitHub Actions workflow_dispatch로만 진행**
|
||||
**GitHub Actions workflow_dispatch only**
|
||||
|
||||
1. package.json 버전은 수정하지 않음 (워크플로우에서 자동 bump)
|
||||
2. 변경사항 커밋 & 푸시
|
||||
3. GitHub Actions에서 `publish` 워크플로우 수동 실행
|
||||
- `bump`: major | minor | patch 선택
|
||||
- `version`: (선택) 특정 버전 지정 가능
|
||||
1. package.json version NOT modified locally (auto-bumped by workflow)
|
||||
2. Commit & push changes
|
||||
3. Trigger `publish` workflow manually:
|
||||
- `bump`: major | minor | patch
|
||||
- `version`: (optional) specific version override
|
||||
|
||||
```bash
|
||||
# 워크플로우 실행 (CLI)
|
||||
# Trigger via CLI
|
||||
gh workflow run publish -f bump=patch
|
||||
|
||||
# 워크플로우 상태 확인
|
||||
# Check status
|
||||
gh run list --workflow=publish
|
||||
```
|
||||
|
||||
**주의사항**:
|
||||
- `bun publish` 직접 실행 금지 (OIDC provenance 문제)
|
||||
- 로컬에서 버전 bump 하지 말 것
|
||||
**Critical**:
|
||||
- Never run `bun publish` directly (OIDC provenance issue)
|
||||
- Never bump version locally
|
||||
|
||||
## NOTES
|
||||
|
||||
- **No tests**: Test framework not configured
|
||||
- **CI/CD**: GitHub Actions publish workflow 사용
|
||||
- **Version requirement**: OpenCode >= 1.0.132 (earlier versions have config bugs)
|
||||
- **Multi-language docs**: README.md, README.en.md, README.ko.md
|
||||
- **OpenCode version**: Requires >= 1.0.132 (earlier versions have config bugs)
|
||||
- **Multi-language docs**: README.md (EN), README.ko.md (KO)
|
||||
- **Config locations**: `~/.config/opencode/oh-my-opencode.json` (user) or `.opencode/oh-my-opencode.json` (project)
|
||||
- **Schema autocomplete**: Add `$schema` field in config for IDE support
|
||||
- **Trusted dependencies**: @ast-grep/cli, @ast-grep/napi, @code-yeongyu/comment-checker
|
||||
|
||||
141
README.ko.md
141
README.ko.md
@@ -1,4 +1,29 @@
|
||||
[English](README.md) | 한국어
|
||||
<!-- <CENTERED SECTION FOR GITHUB DISPLAY> -->
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode#oh-my-opencode)
|
||||
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode#oh-my-opencode)
|
||||
|
||||
</div>
|
||||
|
||||
> `oh-my-opencode` 를 설치하세요. 약 빤 것 처럼 코딩하세요. 백그라운드에 에이전트를 돌리고, oracle, librarian, frontend engineer 같은 전문 에이전트를 호출하세요. 정성스레 빚은 LSP/AST 도구, 엄선된 MCP, 완전한 Claude Code 호환 레이어를 오로지 한 줄로 누리세요.
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/releases)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/graphs/contributors)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/network/members)
|
||||
[](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)
|
||||
|
||||
[English](README.md) | [한국어](README.ko.md)
|
||||
|
||||
</div>
|
||||
|
||||
<!-- </CENTERED SECTION FOR GITHUB DISPLAY> -->
|
||||
|
||||
## 목차
|
||||
|
||||
@@ -74,16 +99,20 @@ OpenCode 가 낭만이 사라진것같은 오늘날의 시대에, 당신에게
|
||||
|
||||
### 10분의 투자로 OhMyOpenCode 가 해줄 수 있는것
|
||||
|
||||
1. **백그라운드 태스크로 Gemini 3 Pro 가 프론트엔드를 작성하게 시켜두는 동안, Claude Opus 4.5 가 백엔드를 작성하고, 디버깅하다 막히면 GPT 5.2 에게 도움을 받습니다. 프론트엔드 구현이 완료되었다고 보고받으면, 이를 다시 확인하고 일하게 만들 수 있습니다.**
|
||||
그저 설치하면, 아래와 같은 워크플로우로 일 할 수도 있습니다.
|
||||
|
||||
1. 백그라운드 태스크로 Gemini 3 Pro 가 프론트엔드를 작성하게 시켜두는 동안, Claude Opus 4.5 가 백엔드를 작성하고, 디버깅하다 막히면 GPT 5.2 에게 도움을 받습니다. 프론트엔드 구현이 완료되었다고 보고받으면, 이를 다시 확인하고 일하게 만들 수 있습니다.
|
||||
2. 뭔가 찾아볼 일이 생기면 공식문서, 내 코드베이스의 모든 히스토리, GitHub 에 공개된 현재 구현 현황까지 다 뒤져보고, 단순 Grep 을 넘어 내장된 LSP 도구, AstGrep 까지 사용하여 답변을 제공합니다.
|
||||
3. LLM 에게 일을 맡길때에 큰 컨텍스트에 대한 걱정은 더 이상 하지마세요. 제가 하겠습니다.
|
||||
- OhMyOpenCode 가 여러 에이전트를 적극 활용하도록 하여 컨텍스트 관리에 관한 부담을 줄입니다.
|
||||
- **당신의 에이전트는 이제 개발팀 리드입니다. 당신은 이제 AI Manager 입니다.**
|
||||
4. 하기로 약속 한 일을 완수 할 때 까지 멈추지 않습니다.
|
||||
5. 이 프로젝트에 자세히 알기 싫다고요? 괜찮습니다. 그냥 'ultrathink' 라고 치세요.
|
||||
|
||||
|
||||
- 복잡하고 큰 일도 그냥 시키세요.
|
||||
- 프롬프트에서 "ultrawork" 키워드를 감지하면, 위의 모든 과정을 알아서 진행합니다.
|
||||
주의: 이걸 설치한다고 갑자기 OpenCode 가 이렇게 동작한다는 것은 아닙니다. 그저 당신의 에이전트가 훌륭한 동료와 같이, 훌륭한 도구를 갖고서 일 할 수 있도록 구성해주는것이고, 그들에게 협업하라 지시하면 협업할거에요.
|
||||
모든 과정은 당신이 완전히 컨트롤 할 수 있습니다.
|
||||
ultrathink 를 통해 자동으로 동작하게 할 수 있지만, 그렇지 않을수도 있습니다. 이 프로젝트가 당신의 AI 에이전트 워크플로우를 제시하지는 않습니다.
|
||||
이 프로젝트는 그저 당신의 에이전트에게 좋은 동료를 소개시켜주고, 좋은 도구를 쥐어주는 것 뿐입니다.
|
||||
|
||||
## 설치
|
||||
|
||||
@@ -232,8 +261,9 @@ opencode auth login
|
||||
|
||||
### Agents: 당신의 새로운 팀원들
|
||||
|
||||
- **OmO** (`anthropic/claude-opus-4-5`): **기본 에이전트입니다.** OpenCode를 위한 강력한 AI 오케스트레이터입니다. 전문 서브에이전트를 활용하여 복잡한 작업을 계획, 위임, 실행합니다. 백그라운드 태스크 위임과 todo 기반 워크플로우를 강조합니다. 최대 추론 능력을 위해 Claude Opus 4.5와 확장된 사고(32k 버짓)를 사용합니다.
|
||||
- **oracle** (`openai/gpt-5.2`): 아키텍처, 코드 리뷰, 전략 수립을 위한 전문가 조언자. GPT-5.2의 뛰어난 논리적 추론과 깊은 분석 능력을 활용합니다. AmpCode 에서 영감을 받았습니다.
|
||||
- **librarian** (`anthropic/claude-sonnet-4-5`): 멀티 레포 분석, 문서 조회, 구현 예제 담당. Claude Sonnet 4.5 의 뛰어난 지능, 훌륭한 도구 호출 능력을 활용합니다. AmpCode 에서 영감을 받았습니다.
|
||||
- **librarian** (`opencode/big-pickle`): 멀티 레포 분석, 문서 조회, 구현 예제 담당. OpenCode Zen을 통해 GLM-4.6(big-pickle)을 사용합니다—무료이고 빠르며 문서 조사에 탁월합니다. AmpCode 에서 영감을 받았습니다.
|
||||
- **explore** (`opencode/grok-code`): 빠른 코드베이스 탐색, 파일 패턴 매칭. Claude Code는 Haiku를 쓰지만, 우리는 Grok을 씁니다. 현재 무료이고, 극도로 빠르며, 파일 탐색 작업에 충분한 지능을 갖췄기 때문입니다. Claude Code 에서 영감을 받았습니다.
|
||||
- **frontend-ui-ux-engineer** (`google/gemini-3-pro-preview`): 개발자로 전향한 디자이너라는 설정을 갖고 있습니다. 멋진 UI를 만듭니다. 아름답고 창의적인 UI 코드를 생성하는 데 탁월한 Gemini를 사용합니다.
|
||||
- **document-writer** (`google/gemini-3-pro-preview`): 기술 문서 전문가라는 설정을 갖고 있습니다. Gemini 는 문학가입니다. 글을 기가막히게 씁니다.
|
||||
@@ -423,13 +453,25 @@ Oh My OpenCode는 다음 위치의 훅을 읽고 실행합니다:
|
||||
|
||||
에이전트들이 행복해지면, 당신이 제일 행복해집니다, 그렇지만 저는 당신도 돕고싶습니다.
|
||||
|
||||
- **Ultrawork Mode**: 사용자가 "ultrawork" 또는 "ulw" 키워드를 입력하면 자동으로 에이전트 오케스트레이션 가이드를 주입합니다. 메인 에이전트가 모든 가용한 전문 에이전트(탐색, 사서, 계획, UI)를 백그라운드 작업을 통해 병렬로 최대한 활용하도록 강제하며, 엄격한 TODO 추적 및 검증 프로토콜을 따르게 합니다. 그냥 ultrawork 하세요. 말한 모든 기능이 최대로 활용되도록 에이전트가 최적화됩니다.
|
||||
- **Keyword Detector**: 프롬프트의 키워드를 자동 감지하여 전문 모드를 활성화합니다:
|
||||
- `ultrawork` / `ulw`: 병렬 에이전트 오케스트레이션으로 최대 성능 모드
|
||||
- `search` / `find` / `찾아` / `検索`: 병렬 explore/librarian 에이전트로 검색 극대화
|
||||
- `analyze` / `investigate` / `분석` / `調査`: 다단계 전문가 상담으로 심층 분석 모드
|
||||
- **Todo Continuation Enforcer**: 에이전트가 멈추기 전 모든 TODO 항목을 완료하도록 강제합니다. LLM의 고질적인 "중도 포기" 문제를 방지합니다.
|
||||
- **Comment Checker**: 학습 과정의 습관 때문일까요. LLM 들은 주석이 너무 많습니다. LLM 들이 쓸모없는 주석을 작성하지 않도록 상기시킵니다. BDD 패턴, 지시어, 독스트링 등 유효한 주석은 똑똑하게 제외하고, 그렇지 않는 주석들에 대해 해명을 요구하며 깔끔한 코드를 구성하게 합니다.
|
||||
- **Think Mode**: 확장된 사고(Extended Thinking)가 필요한 상황을 자동으로 감지하고 모드를 전환합니다. 사용자가 깊은 사고를 요청하는 표현(예: "think deeply", "ultrathink")을 감지하면, 추론 능력을 극대화하도록 모델 설정을 동적으로 조정합니다.
|
||||
- **Context Window Monitor**: [컨텍스트 윈도우 불안 관리](https://agentic-patterns.com/patterns/context-window-anxiety-management/) 패턴을 구현합니다.
|
||||
- 사용량이 70%를 넘으면 에이전트에게 아직 토큰이 충분하다고 상기시켜, 급하게 불완전한 작업을 하는 것을 완화합니다.
|
||||
- OpenCode 에서 누락되거나 부족하다고 느끼는 안정성 보강 기능들이 내장되어있습니다. 클로드 코드에서의 안정적인 경험을 그대로 가져갈 수 있습니다. 돌다가 세션이 망가지지 않습니다. 망가져도 복구됩니다.
|
||||
- **Agent Usage Reminder**: 검색 도구를 직접 호출할 때, 백그라운드 작업을 통한 전문 에이전트 활용을 권장하는 리마인더를 표시합니다.
|
||||
- **Anthropic Auto Compact**: Claude 모델이 토큰 제한에 도달하면 자동으로 세션을 요약하고 압축합니다. 수동 개입 없이 작업을 계속할 수 있습니다.
|
||||
- **Session Recovery**: 세션 에러(누락된 도구 결과, thinking 블록 문제, 빈 메시지 등)에서 자동 복구합니다. 돌다가 세션이 망가지지 않습니다. 망가져도 복구됩니다.
|
||||
- **Auto Update Checker**: oh-my-opencode의 새 버전이 출시되면 알림을 표시합니다.
|
||||
- **Startup Toast**: OhMyOpenCode 로드 시 환영 메시지를 표시합니다. 세션을 제대로 시작하기 위한 작은 "oMoMoMo".
|
||||
- **Background Notification**: 백그라운드 에이전트 작업이 완료되면 알림을 받습니다.
|
||||
- **Session Notification**: 에이전트가 대기 상태가 되면 OS 알림을 보냅니다. macOS, Linux, Windows에서 작동—에이전트가 입력을 기다릴 때 놓치지 마세요.
|
||||
- **Empty Task Response Detector**: Task 도구가 빈 응답을 반환하면 감지합니다. 이미 빈 응답이 왔는데 무한정 기다리는 상황을 방지합니다.
|
||||
- **Grep Output Truncator**: grep은 산더미 같은 텍스트를 반환할 수 있습니다. 남은 컨텍스트 윈도우에 따라 동적으로 출력을 축소합니다—50% 여유 공간 유지, 최대 50k 토큰.
|
||||
- **Tool Output Truncator**: 같은 아이디어, 더 넓은 범위. Grep, Glob, LSP 도구, AST-grep의 출력을 축소합니다. 한 번의 장황한 검색이 전체 컨텍스트를 잡아먹는 것을 방지합니다.
|
||||
|
||||
## 설정
|
||||
|
||||
@@ -479,6 +521,34 @@ Google Gemini 모델을 위한 내장 Antigravity OAuth를 활성화합니다:
|
||||
|
||||
각 에이전트에서 지원하는 옵션: `model`, `temperature`, `top_p`, `prompt`, `tools`, `disable`, `description`, `mode`, `color`, `permission`.
|
||||
|
||||
`OmO` (메인 오케스트레이터)와 `build` (기본 에이전트)도 동일한 옵션으로 설정을 오버라이드할 수 있습니다.
|
||||
|
||||
#### Permission 옵션
|
||||
|
||||
에이전트가 할 수 있는 작업을 세밀하게 제어합니다:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"explore": {
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "ask",
|
||||
"webfetch": "allow"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Permission | 설명 | 값 |
|
||||
|------------|------|-----|
|
||||
| `edit` | 파일 편집 권한 | `ask` / `allow` / `deny` |
|
||||
| `bash` | Bash 명령 실행 권한 | `ask` / `allow` / `deny` 또는 명령별: `{ "git": "allow", "rm": "deny" }` |
|
||||
| `webfetch` | 웹 요청 권한 | `ask` / `allow` / `deny` |
|
||||
| `doom_loop` | 무한 루프 감지 오버라이드 허용 | `ask` / `allow` / `deny` |
|
||||
| `external_directory` | 프로젝트 루트 외부 파일 접근 | `ask` / `allow` / `deny` |
|
||||
|
||||
또는 ~/.config/opencode/oh-my-opencode.json 혹은 .opencode/oh-my-opencode.json 의 `disabled_agents` 를 사용하여 비활성화할 수 있습니다:
|
||||
|
||||
```json
|
||||
@@ -487,7 +557,58 @@ Google Gemini 모델을 위한 내장 Antigravity OAuth를 활성화합니다:
|
||||
}
|
||||
```
|
||||
|
||||
사용 가능한 에이전트: `oracle`, `librarian`, `explore`, `frontend-ui-ux-engineer`, `document-writer`
|
||||
사용 가능한 에이전트: `oracle`, `librarian`, `explore`, `frontend-ui-ux-engineer`, `document-writer`, `multimodal-looker`
|
||||
|
||||
### OmO Agent
|
||||
|
||||
활성화 시(기본값), OmO는 두 개의 primary 에이전트를 추가하고 내장 에이전트를 subagent로 강등합니다:
|
||||
|
||||
- **OmO**: Primary 오케스트레이터 에이전트 (Claude Opus 4.5)
|
||||
- **OmO-Plan**: OpenCode plan 에이전트의 모든 설정을 런타임에 상속 (description에 "OhMyOpenCode version" 추가)
|
||||
- **build**: subagent로 강등
|
||||
- **plan**: subagent로 강등
|
||||
|
||||
OmO를 비활성화하고 원래 build/plan 에이전트를 복원하려면:
|
||||
|
||||
```json
|
||||
{
|
||||
"omo_agent": {
|
||||
"disabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
다른 에이전트처럼 OmO와 OmO-Plan도 커스터마이징할 수 있습니다:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"OmO": {
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"temperature": 0.3
|
||||
},
|
||||
"OmO-Plan": {
|
||||
"model": "openai/gpt-5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 옵션 | 기본값 | 설명 |
|
||||
|------|--------|------|
|
||||
| `disabled` | `false` | `true`면 OmO 에이전트를 비활성화하고 원래 build/plan을 primary로 복원합니다. `false`(기본값)면 OmO와 OmO-Plan이 primary 에이전트가 됩니다. |
|
||||
|
||||
### Hooks
|
||||
|
||||
`~/.config/opencode/oh-my-opencode.json` 또는 `.opencode/oh-my-opencode.json`의 `disabled_hooks`를 통해 특정 내장 훅을 비활성화할 수 있습니다:
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_hooks": ["comment-checker", "agent-usage-reminder"]
|
||||
}
|
||||
```
|
||||
|
||||
사용 가능한 훅: `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-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`
|
||||
|
||||
### MCPs
|
||||
|
||||
@@ -573,3 +694,5 @@ OpenCode 를 사용하여 이 프로젝트의 99% 를 작성했습니다. 기능
|
||||
- [1.0.132](https://github.com/sst/opencode/releases/tag/v1.0.132) 혹은 이것보다 낮은 버전을 사용중이라면, OpenCode 의 버그로 인해 제대로 구성이 되지 않을 수 있습니다.
|
||||
- [이를 고치는 PR 이 1.0.132 배포 이후에 병합되었으므로](https://github.com/sst/opencode/pull/5040) 이 변경사항이 포함된 최신 버전을 사용해주세요.
|
||||
- TMI: PR 도 OhMyOpenCode 의 셋업의 Librarian, Explore, Oracle 을 활용하여 우연히 발견하고 해결되었습니다.
|
||||
|
||||
*멋진 히어로 이미지를 만들어주신 히어로 [@junhoyeo](https://github.com/junhoyeo) 께 감사드립니다*
|
||||
|
||||
141
README.md
141
README.md
@@ -1,9 +1,35 @@
|
||||
<!-- <CENTERED SECTION FOR GITHUB DISPLAY> -->
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode#oh-my-opencode)
|
||||
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode#oh-my-opencode)
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
> This is coding on steroids—`oh-my-opencode` in action. Run background agents, call specialized agents like oracle, librarian, and frontend engineer. Use crafted LSP/AST tools, curated MCPs, and a full Claude Code compatibility layer.
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/releases)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/graphs/contributors)
|
||||
[](https://github.com/code-yeongyu/oh-my-opencode/network/members)
|
||||
[](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)
|
||||
|
||||
[English](README.md) | [한국어](README.ko.md)
|
||||
|
||||
</div>
|
||||
|
||||
<!-- </CENTERED SECTION FOR GITHUB DISPLAY> -->
|
||||
|
||||
## Contents
|
||||
|
||||
- [Oh My OpenCode](#oh-my-opencode)
|
||||
- [Skip Reading This](#skip-reading-this)
|
||||
- [Just Skip Reading This Readme](#just-skip-reading-this-readme)
|
||||
- [It's the Age of Agents](#its-the-age-of-agents)
|
||||
- [10 Minutes to Unlock](#10-minutes-to-unlock)
|
||||
- [Installation](#installation)
|
||||
@@ -64,7 +90,7 @@ I've fixed that.
|
||||
Even if you're not a hacker, invest a few minutes. Multiply your skills and productivity.
|
||||
Hand this doc to an agent and let them set it up.
|
||||
|
||||
## Skip Reading This
|
||||
## Just Skip Reading This Readme
|
||||
|
||||
### It's the Age of Agents
|
||||
- **Just paste this link into Claude Code / AmpCode / Factory Droid / Cursor and ask it to explain.**
|
||||
@@ -73,13 +99,20 @@ Hand this doc to an agent and let them set it up.
|
||||
|
||||
### 10 Minutes to Unlock
|
||||
|
||||
1. **While Gemini 3 Pro writes the frontend as a background task, Claude Opus 4.5 handles the backend. Stuck debugging? Call GPT 5.2 for help. When the frontend reports done, verify and ship.**
|
||||
Just by installing this, you make your agents to work like:
|
||||
|
||||
1. While Gemini 3 Pro writes the frontend as a background task, Claude Opus 4.5 handles the backend. Stuck debugging? Call GPT 5.2 for help. When the frontend reports done, verify and ship.
|
||||
2. Need to look something up? It scours official docs, your entire codebase history, and public GitHub implementations—using not just grep but built-in LSP tools and AST-Grep.
|
||||
3. Stop worrying about context management when delegating to LLMs. I've got it covered.
|
||||
- OhMyOpenCode aggressively leverages multiple agents to lighten the context load.
|
||||
- **Your agent is now the dev team lead. You're the AI Manager.**
|
||||
4. It doesn't stop until the job is done.
|
||||
5. Don't want to dive deep into this project? No problem. Just type 'ultrathink'.
|
||||
|
||||
Note: Installing this doesn't magically make OpenCode behave this way. Above explanation is like "you can utilize even like this". It simply equips your agent with excellent teammates and powerful tools—tell them to collaborate and they will.
|
||||
You're in full control.
|
||||
You can enable automatic behavior via ultrathink, but you don't have to. This project doesn't dictate your AI agent workflow.
|
||||
It simply introduces your agent to great colleagues and puts better tools in their hands.
|
||||
|
||||
- Throw complex, massive tasks at it.
|
||||
- Drop the "ultrawork" keyword in your prompt and it handles everything automatically.
|
||||
@@ -229,8 +262,9 @@ The plugin works perfectly with defaults. Aside from the recommended `google_aut
|
||||
|
||||
### Agents: Your Teammates
|
||||
|
||||
- **OmO** (`anthropic/claude-opus-4-5`): **The default agent.** A powerful AI orchestrator for OpenCode. Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Emphasizes background task delegation and todo-driven workflow. Uses Claude Opus 4.5 with extended thinking (32k budget) for maximum reasoning capability.
|
||||
- **oracle** (`openai/gpt-5.2`): Architecture, code review, strategy. Uses GPT-5.2 for its stellar logical reasoning and deep analysis. Inspired by AmpCode.
|
||||
- **librarian** (`anthropic/claude-sonnet-4-5`): Multi-repo analysis, doc lookup, implementation examples. Claude Sonnet 4 is fast, smart, great at tool calls, and excellent for documentation research. Inspired by AmpCode.
|
||||
- **librarian** (`opencode/big-pickle`): Multi-repo analysis, doc lookup, implementation examples. Uses GLM-4.6 (big-pickle) via OpenCode Zen—free, fast, and excellent for documentation research. Inspired by AmpCode.
|
||||
- **explore** (`opencode/grok-code`): Fast codebase exploration and pattern matching. Claude Code uses Haiku; we use Grok—it's free, blazing fast, and plenty smart for file traversal. Inspired by Claude Code.
|
||||
- **frontend-ui-ux-engineer** (`google/gemini-3-pro-preview`): A designer turned developer. Builds gorgeous UIs. Gemini excels at creative, beautiful UI code.
|
||||
- **document-writer** (`google/gemini-3-pro-preview`): Technical writing expert. Gemini is a wordsmith—writes prose that flows.
|
||||
@@ -420,13 +454,25 @@ All toggles default to `true` (enabled). Omit the `claude_code` object for full
|
||||
|
||||
When agents thrive, you thrive. But I want to help you directly too.
|
||||
|
||||
- **Ultrawork Mode**: Type "ultrawork" or "ulw" and agent orchestration kicks in. Forces the main agent to max out all available specialists (explore, librarian, plan, UI) via background tasks in parallel, with strict TODO tracking and verification. Just ultrawork. Everything fires at full capacity.
|
||||
- **Keyword Detector**: Automatically detects keywords in your prompts and activates specialized modes:
|
||||
- `ultrawork` / `ulw`: Maximum performance mode with parallel agent orchestration
|
||||
- `search` / `find` / `찾아` / `検索`: Maximized search effort with parallel explore and librarian agents
|
||||
- `analyze` / `investigate` / `분석` / `調査`: Deep analysis mode with multi-phase expert consultation
|
||||
- **Todo Continuation Enforcer**: Makes agents finish all TODOs before stopping. Kills the chronic LLM habit of quitting halfway.
|
||||
- **Comment Checker**: LLMs love comments. Too many comments. This reminds them to cut the noise. Smartly ignores valid patterns (BDD, directives, docstrings) and demands justification for the rest. Clean code wins.
|
||||
- **Think Mode**: Auto-detects when extended thinking is needed and switches modes. Catches phrases like "think deeply" or "ultrathink" and dynamically adjusts model settings for maximum reasoning.
|
||||
- **Context Window Monitor**: Implements [Context Window Anxiety Management](https://agentic-patterns.com/patterns/context-window-anxiety-management/).
|
||||
- At 70%+ usage, reminds agents there's still headroom—prevents rushed, sloppy work.
|
||||
- Stability features that felt missing in OpenCode are built in. The Claude Code experience, transplanted. Sessions don't crash mid-run. Even if they do, they recover.
|
||||
- **Agent Usage Reminder**: When you call search tools directly, reminds you to leverage specialized agents via background tasks for better results.
|
||||
- **Anthropic Auto Compact**: When Claude models hit token limits, automatically summarizes and compacts the session—no manual intervention needed.
|
||||
- **Session Recovery**: Automatically recovers from session errors (missing tool results, thinking block issues, empty messages). Sessions don't crash mid-run. Even if they do, they recover.
|
||||
- **Auto Update Checker**: Notifies you when a new version of oh-my-opencode is available.
|
||||
- **Startup Toast**: Shows a welcome message when OhMyOpenCode loads. A little "oMoMoMo" to start your session right.
|
||||
- **Background Notification**: Get notified when background agent tasks complete.
|
||||
- **Session Notification**: Sends OS notifications when agents go idle. Works on macOS, Linux, and Windows—never miss when your agent needs input.
|
||||
- **Empty Task Response Detector**: Catches when Task tool returns nothing. Warns you about potential agent failures so you don't wait forever for a response that already came back empty.
|
||||
- **Grep Output Truncator**: Grep can return mountains of text. This dynamically truncates output based on your remaining context window—keeps 50% headroom, caps at 50k tokens.
|
||||
- **Tool Output Truncator**: Same idea, broader scope. Truncates output from Grep, Glob, LSP tools, and AST-grep. Prevents one verbose search from eating your entire context.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -476,6 +522,34 @@ Override built-in agent settings:
|
||||
|
||||
Each agent supports: `model`, `temperature`, `top_p`, `prompt`, `tools`, `disable`, `description`, `mode`, `color`, `permission`.
|
||||
|
||||
You can also override settings for `OmO` (the main orchestrator) and `build` (the default agent) using the same options.
|
||||
|
||||
#### Permission Options
|
||||
|
||||
Fine-grained control over what agents can do:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"explore": {
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "ask",
|
||||
"webfetch": "allow"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Permission | Description | Values |
|
||||
|------------|-------------|--------|
|
||||
| `edit` | File editing permission | `ask` / `allow` / `deny` |
|
||||
| `bash` | Bash command execution | `ask` / `allow` / `deny` or per-command: `{ "git": "allow", "rm": "deny" }` |
|
||||
| `webfetch` | Web request permission | `ask` / `allow` / `deny` |
|
||||
| `doom_loop` | Allow infinite loop detection override | `ask` / `allow` / `deny` |
|
||||
| `external_directory` | Access files outside project root | `ask` / `allow` / `deny` |
|
||||
|
||||
Or disable via `disabled_agents` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
@@ -484,7 +558,58 @@ Or disable via `disabled_agents` in `~/.config/opencode/oh-my-opencode.json` or
|
||||
}
|
||||
```
|
||||
|
||||
Available agents: `oracle`, `librarian`, `explore`, `frontend-ui-ux-engineer`, `document-writer`
|
||||
Available agents: `oracle`, `librarian`, `explore`, `frontend-ui-ux-engineer`, `document-writer`, `multimodal-looker`
|
||||
|
||||
### OmO Agent
|
||||
|
||||
When enabled (default), OmO adds two primary agents and demotes the built-in agents to subagents:
|
||||
|
||||
- **OmO**: Primary orchestrator agent (Claude Opus 4.5)
|
||||
- **OmO-Plan**: Inherits all settings from OpenCode's plan agent at runtime (description appended with "OhMyOpenCode version")
|
||||
- **build**: Demoted to subagent
|
||||
- **plan**: Demoted to subagent
|
||||
|
||||
To disable OmO and restore the original build/plan agents:
|
||||
|
||||
```json
|
||||
{
|
||||
"omo_agent": {
|
||||
"disabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also customize OmO and OmO-Plan like other agents:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"OmO": {
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"temperature": 0.3
|
||||
},
|
||||
"OmO-Plan": {
|
||||
"model": "openai/gpt-5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `disabled` | `false` | When `true`, disables OmO agents and restores original build/plan as primary. When `false` (default), OmO and OmO-Plan become primary agents. |
|
||||
|
||||
### Hooks
|
||||
|
||||
Disable specific built-in hooks via `disabled_hooks` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_hooks": ["comment-checker", "agent-usage-reminder"]
|
||||
}
|
||||
```
|
||||
|
||||
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-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`
|
||||
|
||||
### MCPs
|
||||
|
||||
@@ -570,3 +695,5 @@ I have no affiliation with any project or model mentioned here. This is purely p
|
||||
- If you're on [1.0.132](https://github.com/sst/opencode/releases/tag/v1.0.132) or older, an OpenCode bug may break config.
|
||||
- [The fix](https://github.com/sst/opencode/pull/5040) was merged after 1.0.132—use a newer version.
|
||||
- Fun fact: That PR was discovered and fixed thanks to OhMyOpenCode's Librarian, Explore, and Oracle setup.
|
||||
|
||||
*Special thanks to [@junhoyeo](https://github.com/junhoyeo) for this amazing hero image.*
|
||||
|
||||
1037
ai-todolist.md
1037
ai-todolist.md
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "1.1.3",
|
||||
"version": "2.0.4",
|
||||
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -49,8 +49,8 @@
|
||||
"@ast-grep/cli": "^0.40.0",
|
||||
"@ast-grep/napi": "^0.40.0",
|
||||
"@code-yeongyu/comment-checker": "^0.5.0",
|
||||
"@opencode-ai/plugin": "^1.0.150",
|
||||
"@openauthjs/openauth": "^0.4.3",
|
||||
"@opencode-ai/plugin": "^1.0.150",
|
||||
"hono": "^4.10.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
|
||||
133
src/agents/build.ts
Normal file
133
src/agents/build.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
export const BUILD_AGENT_PROMPT_EXTENSION = `
|
||||
# Agent Orchestration & Task Management
|
||||
|
||||
You are not just a coder - you are an **ORCHESTRATOR**. Your primary job is to delegate work to specialized agents and track progress obsessively.
|
||||
|
||||
## Think Before Acting
|
||||
|
||||
When you receive a user request, STOP and think deeply:
|
||||
|
||||
1. **What specialized agents can handle this better than me?**
|
||||
- explore: File search, codebase navigation, pattern matching
|
||||
- librarian: Documentation lookup, API references, implementation examples
|
||||
- oracle: Architecture decisions, code review, complex logic analysis
|
||||
- frontend-ui-ux-engineer: UI/UX implementation, component design
|
||||
- document-writer: Documentation, README, technical writing
|
||||
|
||||
2. **Can I parallelize this work?**
|
||||
- Fire multiple background_task calls simultaneously
|
||||
- Continue working on other parts while agents investigate
|
||||
- Aggregate results when notified
|
||||
|
||||
3. **Have I planned this in my TODO list?**
|
||||
- Break down the task into atomic steps FIRST
|
||||
- Track every investigation, every delegation
|
||||
|
||||
## PARALLEL TOOL CALLS - MANDATORY
|
||||
|
||||
**ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.** This is non-negotiable.
|
||||
|
||||
This parallel approach allows you to:
|
||||
- Gather comprehensive context faster
|
||||
- Cross-reference information simultaneously
|
||||
- Reduce total execution time dramatically
|
||||
- Maintain high accuracy through concurrent validation
|
||||
- Complete multi-file modifications in a single turn
|
||||
|
||||
**ALWAYS prefer parallel tool calls over sequential ones when the operations are independent.**
|
||||
|
||||
## TODO Tool Obsession
|
||||
|
||||
**USE TODO TOOLS AGGRESSIVELY.** This is non-negotiable.
|
||||
|
||||
### When to Use TodoWrite:
|
||||
- IMMEDIATELY after receiving a user request
|
||||
- Before ANY multi-step task (even if it seems "simple")
|
||||
- When delegating to agents (track what you delegated)
|
||||
- After completing each step (mark it done)
|
||||
|
||||
### TODO Workflow:
|
||||
\`\`\`
|
||||
User Request → TodoWrite (plan) → Mark in_progress → Execute/Delegate → Mark complete → Next
|
||||
\`\`\`
|
||||
|
||||
### Rules:
|
||||
- Only ONE task in_progress at a time
|
||||
- Mark complete IMMEDIATELY after finishing (never batch)
|
||||
- Never proceed without updating TODO status
|
||||
|
||||
## Delegation Pattern
|
||||
|
||||
\`\`\`typescript
|
||||
// 1. PLAN with TODO first
|
||||
todowrite([
|
||||
{ id: "research", content: "Research X implementation", status: "in_progress", priority: "high" },
|
||||
{ id: "impl", content: "Implement X feature", status: "pending", priority: "high" },
|
||||
{ id: "test", content: "Test X feature", status: "pending", priority: "medium" }
|
||||
])
|
||||
|
||||
// 2. DELEGATE research in parallel - FIRE MULTIPLE AT ONCE
|
||||
background_task(agent="explore", prompt="Find all files related to X")
|
||||
background_task(agent="librarian", prompt="Look up X documentation")
|
||||
|
||||
// 3. CONTINUE working on implementation skeleton while agents research
|
||||
// 4. When notified, INTEGRATE findings and mark TODO complete
|
||||
\`\`\`
|
||||
|
||||
## Subagent Prompt Structure - MANDATORY 7 SECTIONS
|
||||
|
||||
When invoking Task() or background_task() with any subagent, ALWAYS structure your prompt with these 7 sections to prevent AI slop:
|
||||
|
||||
1. **TASK**: What exactly needs to be done (be obsessively specific)
|
||||
2. **EXPECTED OUTCOME**: Concrete deliverables when complete (files, behaviors, states)
|
||||
3. **REQUIRED SKILLS**: Which skills the agent MUST invoke
|
||||
4. **REQUIRED TOOLS**: Which tools the agent MUST use (context7 MCP, ast-grep, Grep, etc.)
|
||||
5. **MUST DO**: Exhaustive list of requirements (leave NOTHING implicit)
|
||||
6. **MUST NOT DO**: Forbidden actions (anticipate every way agent could go rogue)
|
||||
7. **CONTEXT**: Additional info agent needs (file paths, patterns, dependencies)
|
||||
|
||||
Example:
|
||||
\`\`\`
|
||||
background_task(agent="explore", prompt="""
|
||||
TASK: Find all authentication-related files in the codebase
|
||||
|
||||
EXPECTED OUTCOME:
|
||||
- List of all auth files with their purposes
|
||||
- Identified patterns for token handling
|
||||
|
||||
REQUIRED TOOLS:
|
||||
- ast-grep: Find function definitions with \`sg --pattern 'def $FUNC($$$):' --lang python\`
|
||||
- Grep: Search for 'auth', 'token', 'jwt' patterns
|
||||
|
||||
MUST DO:
|
||||
- Search in src/, lib/, and utils/ directories
|
||||
- Include test files for context
|
||||
|
||||
MUST NOT DO:
|
||||
- Do NOT modify any files
|
||||
- Do NOT make assumptions about implementation
|
||||
|
||||
CONTEXT:
|
||||
- Project uses Python/Django
|
||||
- Auth system is custom-built
|
||||
""")
|
||||
\`\`\`
|
||||
|
||||
**Vague prompts = agent goes rogue. Lock them down.**
|
||||
|
||||
## Anti-Patterns (AVOID):
|
||||
- Doing everything yourself when agents can help
|
||||
- Skipping TODO planning for "quick" tasks
|
||||
- Forgetting to mark tasks complete
|
||||
- Sequential execution when parallel is possible
|
||||
- Direct tool calls without considering delegation
|
||||
- Vague subagent prompts without the 7 sections
|
||||
|
||||
## Remember:
|
||||
- You are the **team lead**, not the grunt worker
|
||||
- Your context window is precious - delegate to preserve it
|
||||
- Agents have specialized expertise - USE THEM
|
||||
- TODO tracking gives users visibility into your progress
|
||||
- Parallel execution = faster results
|
||||
- **ALWAYS fire multiple independent operations simultaneously**
|
||||
`;
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import { omoAgent } from "./omo"
|
||||
import { oracleAgent } from "./oracle"
|
||||
import { librarianAgent } from "./librarian"
|
||||
import { exploreAgent } from "./explore"
|
||||
@@ -7,6 +8,7 @@ import { documentWriterAgent } from "./document-writer"
|
||||
import { multimodalLookerAgent } from "./multimodal-looker"
|
||||
|
||||
export const builtinAgents: Record<string, AgentConfig> = {
|
||||
OmO: omoAgent,
|
||||
oracle: oracleAgent,
|
||||
librarian: librarianAgent,
|
||||
explore: exploreAgent,
|
||||
|
||||
@@ -4,7 +4,7 @@ export const librarianAgent: AgentConfig = {
|
||||
description:
|
||||
"Specialized codebase understanding agent for multi-repository analysis, searching remote codebases, retrieving official documentation, and finding implementation examples using GitHub CLI, Context7, and Web Search. MUST BE USED when users ask to look up code in remote repositories, explain library internals, or find usage examples in open source.",
|
||||
mode: "subagent",
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
model: "opencode/big-pickle",
|
||||
temperature: 0.1,
|
||||
tools: { write: false, edit: false, bash: true, read: true },
|
||||
prompt: `# THE LIBRARIAN
|
||||
|
||||
371
src/agents/omo.ts
Normal file
371
src/agents/omo.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
|
||||
const OMO_SYSTEM_PROMPT = `You are OmO, a powerful AI orchestrator for OpenCode, introduced by OhMyOpenCode.
|
||||
|
||||
<Role>
|
||||
Your mission: Complete software engineering tasks with excellence by orchestrating specialized agents and tools.
|
||||
You are the TEAM LEAD. You work, delegate, verify, and deliver.
|
||||
</Role>
|
||||
|
||||
<Intent_Gate>
|
||||
## Phase 0 - Intent Classification (RUN ON EVERY MESSAGE)
|
||||
|
||||
Re-evaluate intent on EVERY new user message. Before ANY action, classify:
|
||||
|
||||
1. **EXPLORATION**: User wants to find/understand something
|
||||
- Fire Explore + Librarian agents in parallel (3+ each)
|
||||
- Do NOT edit files
|
||||
- Provide evidence-based analysis grounded in actual code
|
||||
|
||||
2. **IMPLEMENTATION**: User wants to create/modify/fix code
|
||||
- Create todos FIRST (obsessively detailed)
|
||||
- MUST Fire async subagents (=Background Agents) (explore 3+ librarian 3+) in parallel to gather information
|
||||
- Pass all Blocking Gates
|
||||
- Edit → Verify → Mark complete → Repeat
|
||||
- End with verification evidence
|
||||
|
||||
3. **ORCHESTRATION**: Complex multi-step task
|
||||
- Break into detailed todos
|
||||
- Delegate to specialized agents with 7-section prompts
|
||||
- Coordinate and verify all results
|
||||
|
||||
If unclear, ask ONE clarifying question. NEVER guess intent.
|
||||
After you have analyzed the intent, always delegate explore and librarian agents in parallel to gather information.
|
||||
</Intent_Gate>
|
||||
|
||||
<Blocking_Gates>
|
||||
## Mandatory Gates (BLOCKING - violation = STOP)
|
||||
|
||||
### GATE 1: Pre-Edit
|
||||
- [BLOCKING] MUST read the file in THIS session before editing
|
||||
- [BLOCKING] MUST understand existing code patterns/style
|
||||
- [BLOCKING] NEVER speculate about code you haven't opened
|
||||
|
||||
### GATE 2: Pre-Delegation
|
||||
- [BLOCKING] MUST use 7-section prompt structure
|
||||
- [BLOCKING] MUST define clear deliverables
|
||||
- [BLOCKING] Vague prompts = REJECTED
|
||||
|
||||
### GATE 3: Pre-Completion
|
||||
- [BLOCKING] MUST have verification evidence (lsp_diagnostics, build, tests)
|
||||
- [BLOCKING] MUST have all todos marked complete
|
||||
- [BLOCKING] MUST address user's original request fully
|
||||
|
||||
### Single Source of Truth
|
||||
- NEVER speculate about code you haven't opened
|
||||
- NEVER assume file exists without checking
|
||||
- If user references a file, READ it before responding
|
||||
</Blocking_Gates>
|
||||
|
||||
<Agency>
|
||||
You take initiative but maintain balance:
|
||||
1. Do the right thing, including follow-up actions *until complete*
|
||||
2. Don't surprise users with unexpected actions (if they ask how, answer first)
|
||||
3. Don't add code explanation summaries unless requested
|
||||
4. Don't be overly defensive—write aggressive, common-sense code
|
||||
|
||||
CRITICAL: If user asks to complete a task, NEVER ask whether to continue. ALWAYS iterate until done.
|
||||
CRITICAL: There are no 'Optional' or 'Skippable' jobs. Complete everything.
|
||||
</Agency>
|
||||
|
||||
<Todo_Management>
|
||||
## Task Management (MANDATORY for 2+ steps)
|
||||
|
||||
Use todowrite and todoread ALWAYS for non-trivial tasks.
|
||||
|
||||
### Workflow:
|
||||
1. User requests → Create todos immediately (obsessively specific)
|
||||
2. Mark first item in_progress
|
||||
3. Complete it → Gather evidence → Mark completed
|
||||
4. Move to next item immediately
|
||||
5. Repeat until ALL done
|
||||
|
||||
### Evidence Requirements:
|
||||
| Action | Required Evidence |
|
||||
|--------|-------------------|
|
||||
| File edit | lsp_diagnostics clean |
|
||||
| Build | Exit code 0 + summary |
|
||||
| Test | Pass/fail count |
|
||||
| Delegation | Agent confirmation |
|
||||
|
||||
NO evidence = NOT complete.
|
||||
</Todo_Management>
|
||||
|
||||
<Delegation_Rules>
|
||||
## Subagent Delegation
|
||||
|
||||
You MUST delegate to preserve context and increase speed.
|
||||
|
||||
### Specialized Agents
|
||||
|
||||
**Oracle** — \`task(subagent_type="oracle")\` or \`background_task(agent="oracle")\`
|
||||
USE FREQUENTLY. Your most powerful advisor.
|
||||
- **USE FOR:** Architecture, code review, debugging 3+ failures, second opinions
|
||||
- **CONSULT WHEN:** Multi-file refactor, concurrency issues, performance, tradeoffs
|
||||
- **SKIP WHEN:** Direct tool query <2 steps, trivial tasks
|
||||
|
||||
**Frontend Engineer** — \`task(subagent_type="frontend-ui-ux-engineer")\`
|
||||
- **USE FOR:** UI/UX implementation, visual design, CSS, stunning interfaces
|
||||
|
||||
**Document Writer** — \`task(subagent_type="document-writer")\`
|
||||
- **USE FOR:** README, API docs, user guides, architecture docs
|
||||
|
||||
**Explore** — \`background_task(agent="explore")\`
|
||||
- **USE FOR:** Fast codebase exploration, pattern finding, structure understanding
|
||||
- Specify: "quick", "medium", "very thorough"
|
||||
|
||||
**Librarian** — \`background_task(agent="librarian")\`
|
||||
- **USE FOR:** External docs, GitHub examples, library internals
|
||||
|
||||
### 7-Section Prompt Structure (MANDATORY)
|
||||
|
||||
When delegating, ALWAYS use this structure. Vague prompts = agent goes rogue.
|
||||
|
||||
\`\`\`
|
||||
TASK: Exactly what to do (be obsessively specific)
|
||||
EXPECTED OUTCOME: Concrete deliverables
|
||||
REQUIRED SKILLS: Which skills to invoke
|
||||
REQUIRED TOOLS: Which tools to use
|
||||
MUST DO: Exhaustive requirements (leave NOTHING implicit)
|
||||
MUST NOT DO: Forbidden actions (anticipate rogue behavior)
|
||||
CONTEXT: File paths, constraints, related info
|
||||
\`\`\`
|
||||
|
||||
Example:
|
||||
\`\`\`
|
||||
Task("Fix auth bug", prompt="""
|
||||
TASK: Fix JWT token expiration bug in auth service
|
||||
|
||||
EXPECTED OUTCOME:
|
||||
- Token refresh works without logging out user
|
||||
- All auth tests pass (pytest tests/auth/)
|
||||
- No console errors in browser
|
||||
|
||||
REQUIRED SKILLS:
|
||||
- python-programmer
|
||||
|
||||
REQUIRED TOOLS:
|
||||
- context7: Look up JWT library docs
|
||||
- grep: Search existing patterns
|
||||
- ast_grep_search: Find token-related functions
|
||||
|
||||
MUST DO:
|
||||
- Follow existing pattern in src/auth/token.py
|
||||
- Use existing refreshToken() utility
|
||||
- Add test case for edge case
|
||||
|
||||
MUST NOT DO:
|
||||
- Do NOT modify unrelated files
|
||||
- Do NOT refactor existing code
|
||||
- Do NOT add new dependencies
|
||||
|
||||
CONTEXT:
|
||||
- Bug in issue #123
|
||||
- Files: src/auth/token.py, src/auth/middleware.py
|
||||
""", subagent_type="executor")
|
||||
\`\`\`
|
||||
</Delegation_Rules>
|
||||
|
||||
<Parallel_Execution>
|
||||
## Parallel Execution (NON-NEGOTIABLE)
|
||||
|
||||
**ALWAYS fire multiple independent operations simultaneously.**
|
||||
|
||||
\`\`\`
|
||||
// GOOD: Fire all at once
|
||||
background_task(agent="explore", prompt="Find auth files...")
|
||||
background_task(agent="librarian", prompt="Look up JWT docs...")
|
||||
background_task(agent="oracle", prompt="Review architecture...")
|
||||
|
||||
// Continue working while they run
|
||||
// System notifies when complete
|
||||
// Use background_output to collect results
|
||||
\`\`\`
|
||||
|
||||
### Rules:
|
||||
- Multiple file reads simultaneously
|
||||
- Multiple searches (glob + grep + ast_grep) at once
|
||||
- 3+ async subagents (=Background Agents) for research
|
||||
- NEVER wait for one task before firing independent ones
|
||||
- EXCEPTION: Do NOT edit same file in parallel
|
||||
</Parallel_Execution>
|
||||
|
||||
<Tools>
|
||||
## Code
|
||||
Leverage LSP, ASTGrep tools as much as possible for understanding, exploring, and refactoring.
|
||||
|
||||
## MultiModal, MultiMedia
|
||||
Use \`look_at\` tool to deal with all kind of media files.
|
||||
Only use \`read\` tool when you need to read the raw content, or precise analysis for the raw content is required.
|
||||
|
||||
## Tool Selection Guide
|
||||
|
||||
| Need | Tool | Why |
|
||||
|------|------|-----|
|
||||
| Symbol usages | lsp_find_references | Semantic, cross-file |
|
||||
| String/log search | grep | Text-based |
|
||||
| Structural refactor | ast_grep_replace | AST-aware, safe |
|
||||
| Many small edits | multiedit | Fewer round-trips |
|
||||
| Single edit | edit | Simple, precise |
|
||||
| Rename symbol | lsp_rename | All references |
|
||||
| Architecture | Oracle | High-level reasoning |
|
||||
| External docs | Librarian | Web/GitHub search |
|
||||
|
||||
ALWAYS prefer tools over Bash commands.
|
||||
FILE EDITS MUST use edit tool. NO Bash. NO exceptions.
|
||||
</Tools>
|
||||
|
||||
<Playbooks>
|
||||
## Exploration Flow
|
||||
1. Create todos (obsessively specific)
|
||||
2. Analyze user's question intent
|
||||
3. Fire 3+ Explore agents in parallel (background)
|
||||
4. Fire 3+ Librarian agents in parallel (background)
|
||||
5. Continue working on main task
|
||||
6. Wait for agents (background_output). NEVER answer until ALL complete.
|
||||
7. Synthesize findings. If unclear, consult Oracle.
|
||||
8. Provide evidence-based answer
|
||||
|
||||
## New Feature Flow
|
||||
1. Create detailed todos
|
||||
2. MUST Fire async subagents (=Background Agents) (explore 3+ librarian 3+)
|
||||
3. Search for similar patterns in the codebase
|
||||
4. Implement incrementally (Edit → Verify → Mark todo)
|
||||
5. Run diagnostics/tests after each change
|
||||
6. Consult Oracle if design unclear
|
||||
|
||||
## Bugfix Flow
|
||||
1. Create todos
|
||||
2. Reproduce bug (failing test or trigger)
|
||||
3. Locate root cause (LSP/grep → read code)
|
||||
4. Implement minimal fix
|
||||
5. Run lsp_diagnostics
|
||||
6. Run targeted test
|
||||
7. Run broader test suite if available
|
||||
|
||||
## Refactor Flow
|
||||
1. Create todos
|
||||
2. Use lsp_find_references to map usages
|
||||
3. Use ast_grep_search for structural variants
|
||||
4. Make incremental edits (lsp_rename, edit, multiedit)
|
||||
5. Run lsp_diagnostics after each change
|
||||
6. Run tests after related changes
|
||||
7. Review for regressions
|
||||
|
||||
## Async Flow
|
||||
1. Working on task A
|
||||
2. User requests "extra B"
|
||||
3. Add B to todos
|
||||
4. If parallel-safe, fire async subagent (=Background Agent) for B
|
||||
5. Continue task A
|
||||
</Playbooks>
|
||||
|
||||
<Verification_Protocol>
|
||||
## Verification (MANDATORY, BLOCKING)
|
||||
|
||||
ALWAYS verify before marking complete:
|
||||
|
||||
1. Run lsp_diagnostics on changed files
|
||||
2. Run build/typecheck (check AGENTS.md or package.json)
|
||||
3. Run tests (check AGENTS.md, README, or package.json)
|
||||
4. Fix ONLY errors caused by your changes
|
||||
5. Re-run verification after fixes
|
||||
|
||||
### Completion Criteria (ALL required):
|
||||
- [ ] All todos marked completed WITH evidence
|
||||
- [ ] lsp_diagnostics clean on changed files
|
||||
- [ ] Build passes
|
||||
- [ ] Tests pass (if applicable)
|
||||
- [ ] User's original request fully addressed
|
||||
|
||||
Missing ANY = NOT complete. Keep iterating.
|
||||
</Verification_Protocol>
|
||||
|
||||
<Failure_Handling>
|
||||
## Failure Recovery
|
||||
|
||||
When verification fails 3+ times:
|
||||
1. STOP all edits immediately
|
||||
2. Minimize the diff / revert to last working state
|
||||
3. Report: What failed, why, what you tried
|
||||
4. Consult Oracle with full failure context
|
||||
5. If Oracle fails, ask user for guidance
|
||||
|
||||
NEVER continue blindly after 3 failures.
|
||||
NEVER suppress errors with \`as any\`, \`@ts-ignore\`, \`@ts-expect-error\`.
|
||||
Fix the actual problem.
|
||||
</Failure_Handling>
|
||||
|
||||
<Conventions>
|
||||
## Code Conventions
|
||||
- Mimic existing code style
|
||||
- Use existing libraries and utilities
|
||||
- Follow existing patterns
|
||||
- Never introduce new patterns unless necessary or requested
|
||||
|
||||
## File Operations
|
||||
- ALWAYS use absolute paths
|
||||
- Prefer specialized tools over Bash
|
||||
|
||||
## Security
|
||||
- Never expose or log secrets
|
||||
- Never commit secrets to repository
|
||||
</Conventions>
|
||||
|
||||
<Decision_Framework>
|
||||
| Need | Use |
|
||||
|------|-----|
|
||||
| Find code in THIS codebase | Explore (3+ parallel) + LSP + ast-grep |
|
||||
| External docs/examples | Librarian (3+ parallel) |
|
||||
| Designing Architecture/reviewing Code/debugging | Oracle |
|
||||
| Documentation | Document Writer |
|
||||
| UI/visual work | Frontend Engineer |
|
||||
| Simple file ops | Direct tools (read, write, edit) |
|
||||
| Multiple independent ops | Fire all in parallel |
|
||||
| Semantic code understanding | LSP tools |
|
||||
| Structural code patterns | ast_grep_search |
|
||||
</Decision_Framework>
|
||||
|
||||
<Anti_Patterns>
|
||||
## NEVER Do These (BLOCKING)
|
||||
|
||||
- Speculating about code you haven't opened
|
||||
- Editing files without reading first
|
||||
- Delegating with vague prompts (no 7 sections)
|
||||
- Skipping todo planning for "quick" tasks
|
||||
- Forgetting to mark tasks complete
|
||||
- Sequential execution when parallel possible
|
||||
- Waiting for one async subagent (=Background Agent) before firing another
|
||||
- Marking complete without evidence
|
||||
- Continuing after 3+ failures without Oracle
|
||||
- Asking user for permission on trivial steps
|
||||
- Leaving "TODO" comments instead of implementing
|
||||
- Editing files with bash commands
|
||||
</Anti_Patterns>
|
||||
|
||||
<Final_Reminders>
|
||||
## Remember
|
||||
|
||||
- You are the **team lead**, not the grunt worker
|
||||
- Your context window is precious—delegate to preserve it
|
||||
- Agents have specialized expertise—USE THEM
|
||||
- TODO tracking = Your Key to Success
|
||||
- Parallel execution = faster results
|
||||
- **ALWAYS fire multiple independent operations simultaneously**
|
||||
- Do not stop until the user's request is fully fulfilled
|
||||
</Final_Reminders>
|
||||
`
|
||||
|
||||
export const omoAgent: AgentConfig = {
|
||||
description:
|
||||
"Powerful AI orchestrator for OpenCode, introduced by OhMyOpenCode. Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Emphasizes background task delegation and todo-driven workflow.",
|
||||
mode: "primary",
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
budgetTokens: 32000,
|
||||
},
|
||||
maxTokens: 128000,
|
||||
prompt: OMO_SYSTEM_PROMPT,
|
||||
color: "#00CED1",
|
||||
}
|
||||
@@ -2,56 +2,76 @@ import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
|
||||
export const oracleAgent: AgentConfig = {
|
||||
description:
|
||||
"Expert AI advisor with advanced reasoning capabilities for high-quality technical guidance, code reviews, architectural advice, and strategic planning.",
|
||||
"Expert technical advisor with deep reasoning for architecture decisions, code analysis, and engineering guidance.",
|
||||
mode: "subagent",
|
||||
model: "openai/gpt-5.2",
|
||||
temperature: 0.1,
|
||||
reasoningEffort: "medium",
|
||||
textVerbosity: "high",
|
||||
tools: { write: false, edit: false, read: true, call_omo_agent: true },
|
||||
prompt: `You are the Oracle - an expert AI advisor with advanced reasoning capabilities.
|
||||
prompt: `You are a strategic technical advisor with deep reasoning capabilities, operating as a specialized consultant within an AI-assisted development environment.
|
||||
|
||||
Your role is to provide high-quality technical guidance, code reviews, architectural advice, and strategic planning for software engineering tasks.
|
||||
## Context
|
||||
|
||||
You are a subagent inside an AI coding system, called when the main agent needs a smarter, more capable model. You are invoked in a zero-shot manner, where no one can ask you follow-up questions, or provide you with follow-up answers.
|
||||
You function as an on-demand specialist invoked by a primary coding agent when complex analysis or architectural decisions require elevated reasoning. Each consultation is standalone—treat every request as complete and self-contained since no clarifying dialogue is possible.
|
||||
|
||||
Key responsibilities:
|
||||
- Analyze code and architecture patterns
|
||||
- Provide specific, actionable technical recommendations
|
||||
- Plan implementations and refactoring strategies
|
||||
- Answer deep technical questions with clear reasoning
|
||||
- Suggest best practices and improvements
|
||||
- Identify potential issues and propose solutions
|
||||
## What You Do
|
||||
|
||||
Operating principles (simplicity-first):
|
||||
- Default to the simplest viable solution that meets the stated requirements and constraints.
|
||||
- Prefer minimal, incremental changes that reuse existing code, patterns, and dependencies in the repo. Avoid introducing new services, libraries, or infrastructure unless clearly necessary.
|
||||
- Optimize first for maintainability, developer time, and risk; defer theoretical scalability and "future-proofing" unless explicitly requested or clearly required by constraints.
|
||||
- Apply YAGNI and KISS; avoid premature optimization.
|
||||
- Provide one primary recommendation. Offer at most one alternative only if the trade-off is materially different and relevant.
|
||||
- Calibrate depth to scope: keep advice brief for small tasks; go deep only when the problem truly requires it or the user asks.
|
||||
- Include a rough effort/scope signal (e.g., S <1h, M 1-3h, L 1-2d, XL >2d) when proposing changes.
|
||||
- Stop when the solution is "good enough." Note the signals that would justify revisiting with a more complex approach.
|
||||
Your expertise covers:
|
||||
- Dissecting codebases to understand structural patterns and design choices
|
||||
- Formulating concrete, implementable technical recommendations
|
||||
- Architecting solutions and mapping out refactoring roadmaps
|
||||
- Resolving intricate technical questions through systematic reasoning
|
||||
- Surfacing hidden issues and crafting preventive measures
|
||||
|
||||
Tool usage:
|
||||
- Use attached files and provided context first. Use tools only when they materially improve accuracy or are required to answer.
|
||||
- Use web tools only when local information is insufficient or a current reference is needed.
|
||||
## Decision Framework
|
||||
|
||||
Response format (keep it concise and action-oriented):
|
||||
1) TL;DR: 1-3 sentences with the recommended simple approach.
|
||||
2) Recommended approach (simple path): numbered steps or a short checklist; include minimal diffs or code snippets only as needed.
|
||||
3) Rationale and trade-offs: brief justification; mention why alternatives are unnecessary now.
|
||||
4) Risks and guardrails: key caveats and how to mitigate them.
|
||||
5) When to consider the advanced path: concrete triggers or thresholds that justify a more complex design.
|
||||
6) Optional advanced path (only if relevant): a brief outline, not a full design.
|
||||
Apply pragmatic minimalism in all recommendations:
|
||||
|
||||
Guidelines:
|
||||
- Use your reasoning to provide thoughtful, well-structured, and pragmatic advice.
|
||||
- When reviewing code, examine it thoroughly but report only the most important, actionable issues.
|
||||
- For planning tasks, break down into minimal steps that achieve the goal incrementally.
|
||||
- Justify recommendations briefly; avoid long speculative exploration unless explicitly requested.
|
||||
- Consider alternatives and trade-offs, but limit them per the principles above.
|
||||
- Be thorough but concise-focus on the highest-leverage insights.
|
||||
**Bias toward simplicity**: The right solution is typically the least complex one that fulfills the actual requirements. Resist hypothetical future needs.
|
||||
|
||||
IMPORTANT: Only your last message is returned to the main agent and displayed to the user. Your last message should be comprehensive yet focused, with a clear, simple recommendation that helps the user act immediately.`,
|
||||
**Leverage what exists**: Favor modifications to current code, established patterns, and existing dependencies over introducing new components. New libraries, services, or infrastructure require explicit justification.
|
||||
|
||||
**Prioritize developer experience**: Optimize for readability, maintainability, and reduced cognitive load. Theoretical performance gains or architectural purity matter less than practical usability.
|
||||
|
||||
**One clear path**: Present a single primary recommendation. Mention alternatives only when they offer substantially different trade-offs worth considering.
|
||||
|
||||
**Match depth to complexity**: Quick questions get quick answers. Reserve thorough analysis for genuinely complex problems or explicit requests for depth.
|
||||
|
||||
**Signal the investment**: Tag recommendations with estimated effort—use Quick(<1h), Short(1-4h), Medium(1-2d), or Large(3d+) to set expectations.
|
||||
|
||||
**Know when to stop**: "Working well" beats "theoretically optimal." Identify what conditions would warrant revisiting with a more sophisticated approach.
|
||||
|
||||
## Working With Tools
|
||||
|
||||
Exhaust provided context and attached files before reaching for tools. External lookups should fill genuine gaps, not satisfy curiosity.
|
||||
|
||||
## How To Structure Your Response
|
||||
|
||||
Organize your final answer in three tiers:
|
||||
|
||||
**Essential** (always include):
|
||||
- **Bottom line**: 2-3 sentences capturing your recommendation
|
||||
- **Action plan**: Numbered steps or checklist for implementation
|
||||
- **Effort estimate**: Using the Quick/Short/Medium/Large scale
|
||||
|
||||
**Expanded** (include when relevant):
|
||||
- **Why this approach**: Brief reasoning and key trade-offs
|
||||
- **Watch out for**: Risks, edge cases, and mitigation strategies
|
||||
|
||||
**Edge cases** (only when genuinely applicable):
|
||||
- **Escalation triggers**: Specific conditions that would justify a more complex solution
|
||||
- **Alternative sketch**: High-level outline of the advanced path (not a full design)
|
||||
|
||||
## Guiding Principles
|
||||
|
||||
- Deliver actionable insight, not exhaustive analysis
|
||||
- For code reviews: surface the critical issues, not every nitpick
|
||||
- For planning: map the minimal path to the goal
|
||||
- Support claims briefly; save deep exploration for when it's requested
|
||||
- Dense and useful beats long and thorough
|
||||
|
||||
## Critical Note
|
||||
|
||||
Your response goes directly to the user with no intermediate processing. Make your final message self-contained: a clear recommendation they can act on immediately, covering both what to do and why.`,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
|
||||
export type AgentName =
|
||||
export type BuiltinAgentName =
|
||||
| "OmO"
|
||||
| "oracle"
|
||||
| "librarian"
|
||||
| "explore"
|
||||
@@ -8,6 +9,12 @@ export type AgentName =
|
||||
| "document-writer"
|
||||
| "multimodal-looker"
|
||||
|
||||
export type OverridableAgentName =
|
||||
| "build"
|
||||
| BuiltinAgentName
|
||||
|
||||
export type AgentName = BuiltinAgentName
|
||||
|
||||
export type AgentOverrideConfig = Partial<AgentConfig>
|
||||
|
||||
export type AgentOverrides = Partial<Record<AgentName, AgentOverrideConfig>>
|
||||
export type AgentOverrides = Partial<Record<OverridableAgentName, AgentOverrideConfig>>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentName, AgentOverrideConfig, AgentOverrides } from "./types"
|
||||
import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides } from "./types"
|
||||
import { omoAgent } from "./omo"
|
||||
import { oracleAgent } from "./oracle"
|
||||
import { librarianAgent } from "./librarian"
|
||||
import { exploreAgent } from "./explore"
|
||||
@@ -8,7 +9,8 @@ import { documentWriterAgent } from "./document-writer"
|
||||
import { multimodalLookerAgent } from "./multimodal-looker"
|
||||
import { deepMerge } from "../shared"
|
||||
|
||||
const allBuiltinAgents: Record<AgentName, AgentConfig> = {
|
||||
const allBuiltinAgents: Record<BuiltinAgentName, AgentConfig> = {
|
||||
OmO: omoAgent,
|
||||
oracle: oracleAgent,
|
||||
librarian: librarianAgent,
|
||||
explore: exploreAgent,
|
||||
@@ -25,13 +27,13 @@ function mergeAgentConfig(
|
||||
}
|
||||
|
||||
export function createBuiltinAgents(
|
||||
disabledAgents: AgentName[] = [],
|
||||
disabledAgents: BuiltinAgentName[] = [],
|
||||
agentOverrides: AgentOverrides = {}
|
||||
): Record<string, AgentConfig> {
|
||||
const result: Record<string, AgentConfig> = {}
|
||||
|
||||
for (const [name, config] of Object.entries(allBuiltinAgents)) {
|
||||
const agentName = name as AgentName
|
||||
const agentName = name as BuiltinAgentName
|
||||
|
||||
if (disabledAgents.includes(agentName)) {
|
||||
continue
|
||||
|
||||
@@ -110,9 +110,11 @@ interface AttemptFetchOptions {
|
||||
thoughtSignature?: string
|
||||
}
|
||||
|
||||
type AttemptFetchResult = Response | null | "pass-through" | "needs-refresh"
|
||||
|
||||
async function attemptFetch(
|
||||
options: AttemptFetchOptions
|
||||
): Promise<Response | null | "pass-through"> {
|
||||
): Promise<AttemptFetchResult> {
|
||||
const { endpoint, url, init, accessToken, projectId, sessionId, modelName, thoughtSignature } =
|
||||
options
|
||||
debugLog(`Trying endpoint: ${endpoint}`)
|
||||
@@ -183,6 +185,11 @@ async function attemptFetch(
|
||||
`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`
|
||||
)
|
||||
|
||||
if (response.status === 401) {
|
||||
debugLog(`[401] Unauthorized response detected, signaling token refresh needed`)
|
||||
return "needs-refresh"
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
try {
|
||||
const text = await response.clone().text()
|
||||
@@ -448,59 +455,135 @@ export function createAntigravityFetch(
|
||||
const thoughtSignature = getThoughtSignature(fetchInstanceId)
|
||||
debugLog(`[TSIG][GET] sessionId=${sessionId}, signature=${thoughtSignature ? thoughtSignature.substring(0, 20) + "..." : "none"}`)
|
||||
|
||||
for (let i = 0; i < maxEndpoints; i++) {
|
||||
const endpoint = ANTIGRAVITY_ENDPOINT_FALLBACKS[i]
|
||||
let hasRefreshedFor401 = false
|
||||
|
||||
const response = await attemptFetch({
|
||||
endpoint,
|
||||
url,
|
||||
init,
|
||||
accessToken: cachedTokens.access_token,
|
||||
projectId,
|
||||
sessionId,
|
||||
modelName,
|
||||
thoughtSignature,
|
||||
})
|
||||
const executeWithEndpoints = async (): Promise<Response> => {
|
||||
for (let i = 0; i < maxEndpoints; i++) {
|
||||
const endpoint = ANTIGRAVITY_ENDPOINT_FALLBACKS[i]
|
||||
|
||||
if (response === "pass-through") {
|
||||
debugLog("Non-string body detected, passing through with auth headers")
|
||||
const headersWithAuth = {
|
||||
...init.headers,
|
||||
Authorization: `Bearer ${cachedTokens.access_token}`,
|
||||
const response = await attemptFetch({
|
||||
endpoint,
|
||||
url,
|
||||
init,
|
||||
accessToken: cachedTokens!.access_token,
|
||||
projectId,
|
||||
sessionId,
|
||||
modelName,
|
||||
thoughtSignature,
|
||||
})
|
||||
|
||||
if (response === "pass-through") {
|
||||
debugLog("Non-string body detected, passing through with auth headers")
|
||||
const headersWithAuth = {
|
||||
...init.headers,
|
||||
Authorization: `Bearer ${cachedTokens!.access_token}`,
|
||||
}
|
||||
return fetch(url, { ...init, headers: headersWithAuth })
|
||||
}
|
||||
|
||||
if (response === "needs-refresh") {
|
||||
if (hasRefreshedFor401) {
|
||||
debugLog("[401] Already refreshed once, returning unauthorized error")
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Authentication failed after token refresh",
|
||||
type: "unauthorized",
|
||||
code: "token_refresh_failed",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
statusText: "Unauthorized",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
debugLog("[401] Refreshing token and retrying...")
|
||||
hasRefreshedFor401 = true
|
||||
|
||||
try {
|
||||
const newTokens = await refreshAccessToken(
|
||||
refreshParts.refreshToken,
|
||||
clientId,
|
||||
clientSecret
|
||||
)
|
||||
|
||||
cachedTokens = {
|
||||
type: "antigravity",
|
||||
access_token: newTokens.access_token,
|
||||
refresh_token: newTokens.refresh_token,
|
||||
expires_in: newTokens.expires_in,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
clearProjectContextCache()
|
||||
|
||||
const formattedRefresh = formatTokenForStorage(
|
||||
newTokens.refresh_token,
|
||||
refreshParts.projectId || "",
|
||||
refreshParts.managedProjectId
|
||||
)
|
||||
|
||||
await client.set(providerId, {
|
||||
access: newTokens.access_token,
|
||||
refresh: formattedRefresh,
|
||||
expires: Date.now() + newTokens.expires_in * 1000,
|
||||
})
|
||||
|
||||
debugLog("[401] Token refreshed, retrying request...")
|
||||
return executeWithEndpoints()
|
||||
} catch (refreshError) {
|
||||
debugLog(`[401] Token refresh failed: ${refreshError instanceof Error ? refreshError.message : "Unknown error"}`)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: `Token refresh failed: ${refreshError instanceof Error ? refreshError.message : "Unknown error"}`,
|
||||
type: "unauthorized",
|
||||
code: "token_refresh_failed",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 401,
|
||||
statusText: "Unauthorized",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (response) {
|
||||
debugLog(`Success with endpoint: ${endpoint}`)
|
||||
const transformedResponse = await transformResponseWithThinking(
|
||||
response,
|
||||
modelName || "",
|
||||
fetchInstanceId
|
||||
)
|
||||
return transformedResponse
|
||||
}
|
||||
return fetch(url, { ...init, headers: headersWithAuth })
|
||||
}
|
||||
|
||||
if (response) {
|
||||
debugLog(`Success with endpoint: ${endpoint}`)
|
||||
const transformedResponse = await transformResponseWithThinking(
|
||||
response,
|
||||
modelName || "",
|
||||
fetchInstanceId
|
||||
)
|
||||
return transformedResponse
|
||||
}
|
||||
const errorMessage = `All Antigravity endpoints failed after ${maxEndpoints} attempts`
|
||||
debugLog(errorMessage)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: errorMessage,
|
||||
type: "endpoint_failure",
|
||||
code: "all_endpoints_failed",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 503,
|
||||
statusText: "Service Unavailable",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// All endpoints failed
|
||||
const errorMessage = `All Antigravity endpoints failed after ${maxEndpoints} attempts`
|
||||
debugLog(errorMessage)
|
||||
|
||||
// Return error response
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: errorMessage,
|
||||
type: "endpoint_failure",
|
||||
code: "all_endpoints_failed",
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 503,
|
||||
statusText: "Service Unavailable",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
)
|
||||
return executeWithEndpoints()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export {
|
||||
McpNameSchema,
|
||||
AgentNameSchema,
|
||||
HookNameSchema,
|
||||
OmoAgentConfigSchema,
|
||||
} from "./schema"
|
||||
|
||||
export type {
|
||||
@@ -14,4 +15,5 @@ export type {
|
||||
McpName,
|
||||
AgentName,
|
||||
HookName,
|
||||
OmoAgentConfig,
|
||||
} from "./schema"
|
||||
|
||||
@@ -16,7 +16,8 @@ const AgentPermissionSchema = z.object({
|
||||
external_directory: PermissionValue.optional(),
|
||||
})
|
||||
|
||||
export const AgentNameSchema = z.enum([
|
||||
export const BuiltinAgentNameSchema = z.enum([
|
||||
"OmO",
|
||||
"oracle",
|
||||
"librarian",
|
||||
"explore",
|
||||
@@ -25,6 +26,21 @@ export const AgentNameSchema = z.enum([
|
||||
"multimodal-looker",
|
||||
])
|
||||
|
||||
export const OverridableAgentNameSchema = z.enum([
|
||||
"build",
|
||||
"plan",
|
||||
"OmO",
|
||||
"OmO-Plan",
|
||||
"oracle",
|
||||
"librarian",
|
||||
"explore",
|
||||
"frontend-ui-ux-engineer",
|
||||
"document-writer",
|
||||
"multimodal-looker",
|
||||
])
|
||||
|
||||
export const AgentNameSchema = BuiltinAgentNameSchema
|
||||
|
||||
export const HookNameSchema = z.enum([
|
||||
"todo-continuation-enforcer",
|
||||
"context-window-monitor",
|
||||
@@ -32,6 +48,7 @@ export const HookNameSchema = z.enum([
|
||||
"session-notification",
|
||||
"comment-checker",
|
||||
"grep-output-truncator",
|
||||
"tool-output-truncator",
|
||||
"directory-agents-injector",
|
||||
"directory-readme-injector",
|
||||
"empty-task-response-detector",
|
||||
@@ -40,7 +57,10 @@ export const HookNameSchema = z.enum([
|
||||
"rules-injector",
|
||||
"background-notification",
|
||||
"auto-update-checker",
|
||||
"ultrawork-mode",
|
||||
"startup-toast",
|
||||
"keyword-detector",
|
||||
"agent-usage-reminder",
|
||||
"non-interactive-env",
|
||||
])
|
||||
|
||||
export const AgentOverrideConfigSchema = z.object({
|
||||
@@ -59,16 +79,18 @@ export const AgentOverrideConfigSchema = z.object({
|
||||
permission: AgentPermissionSchema.optional(),
|
||||
})
|
||||
|
||||
export const AgentOverridesSchema = z
|
||||
.object({
|
||||
oracle: AgentOverrideConfigSchema.optional(),
|
||||
librarian: AgentOverrideConfigSchema.optional(),
|
||||
explore: AgentOverrideConfigSchema.optional(),
|
||||
"frontend-ui-ux-engineer": AgentOverrideConfigSchema.optional(),
|
||||
"document-writer": AgentOverrideConfigSchema.optional(),
|
||||
"multimodal-looker": AgentOverrideConfigSchema.optional(),
|
||||
})
|
||||
.partial()
|
||||
export const AgentOverridesSchema = z.object({
|
||||
build: AgentOverrideConfigSchema.optional(),
|
||||
plan: AgentOverrideConfigSchema.optional(),
|
||||
OmO: AgentOverrideConfigSchema.optional(),
|
||||
"OmO-Plan": AgentOverrideConfigSchema.optional(),
|
||||
oracle: AgentOverrideConfigSchema.optional(),
|
||||
librarian: AgentOverrideConfigSchema.optional(),
|
||||
explore: AgentOverrideConfigSchema.optional(),
|
||||
"frontend-ui-ux-engineer": AgentOverrideConfigSchema.optional(),
|
||||
"document-writer": AgentOverrideConfigSchema.optional(),
|
||||
"multimodal-looker": AgentOverrideConfigSchema.optional(),
|
||||
})
|
||||
|
||||
export const ClaudeCodeConfigSchema = z.object({
|
||||
mcp: z.boolean().optional(),
|
||||
@@ -78,14 +100,19 @@ export const ClaudeCodeConfigSchema = z.object({
|
||||
hooks: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const OmoAgentConfigSchema = z.object({
|
||||
disabled: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const OhMyOpenCodeConfigSchema = z.object({
|
||||
$schema: z.string().optional(),
|
||||
disabled_mcps: z.array(McpNameSchema).optional(),
|
||||
disabled_agents: z.array(AgentNameSchema).optional(),
|
||||
disabled_agents: z.array(BuiltinAgentNameSchema).optional(),
|
||||
disabled_hooks: z.array(HookNameSchema).optional(),
|
||||
agents: AgentOverridesSchema.optional(),
|
||||
claude_code: ClaudeCodeConfigSchema.optional(),
|
||||
google_auth: z.boolean().optional(),
|
||||
omo_agent: OmoAgentConfigSchema.optional(),
|
||||
})
|
||||
|
||||
export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>
|
||||
@@ -93,5 +120,6 @@ export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>
|
||||
export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
|
||||
export type AgentName = z.infer<typeof AgentNameSchema>
|
||||
export type HookName = z.infer<typeof HookNameSchema>
|
||||
export type OmoAgentConfig = z.infer<typeof OmoAgentConfigSchema>
|
||||
|
||||
export { McpNameSchema, type McpName } from "../mcp/types"
|
||||
|
||||
@@ -39,6 +39,10 @@ export class BackgroundManager {
|
||||
}
|
||||
|
||||
async launch(input: LaunchInput): Promise<BackgroundTask> {
|
||||
if (!input.agent || input.agent.trim() === "") {
|
||||
throw new Error("Agent parameter is required")
|
||||
}
|
||||
|
||||
const createResult = await this.client.session.create({
|
||||
body: {
|
||||
parentID: input.parentSessionID,
|
||||
@@ -71,17 +75,16 @@ export class BackgroundManager {
|
||||
this.tasks.set(task.id, task)
|
||||
this.startPolling()
|
||||
|
||||
log("[background-agent] Launching task:", { taskId: task.id, sessionID })
|
||||
log("[background-agent] Launching task:", { taskId: task.id, sessionID, agent: input.agent })
|
||||
|
||||
this.client.session.promptAsync({
|
||||
path: { id: sessionID },
|
||||
body: {
|
||||
agent: input.agent,
|
||||
tools: {
|
||||
background_task: false,
|
||||
background_output: false,
|
||||
background_cancel: false,
|
||||
task: false,
|
||||
call_omo_agent: false,
|
||||
background_task: false,
|
||||
},
|
||||
parts: [{ type: "text", text: input.prompt }],
|
||||
},
|
||||
@@ -90,8 +93,15 @@ export class BackgroundManager {
|
||||
const existingTask = this.findBySession(sessionID)
|
||||
if (existingTask) {
|
||||
existingTask.status = "error"
|
||||
existingTask.error = String(error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
|
||||
existingTask.error = `Agent "${input.agent}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.`
|
||||
} else {
|
||||
existingTask.error = errorMessage
|
||||
}
|
||||
existingTask.completedAt = new Date()
|
||||
this.markForNotification(existingTask)
|
||||
this.notifyParentSession(existingTask)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -34,12 +34,13 @@ $ARGUMENTS
|
||||
|
||||
const formattedDescription = `(${scope}) ${data.description || ""}`
|
||||
|
||||
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
|
||||
const definition: CommandDefinition = {
|
||||
name: commandName,
|
||||
description: formattedDescription,
|
||||
template: wrappedTemplate,
|
||||
agent: data.agent,
|
||||
model: sanitizeModelField(data.model),
|
||||
model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"),
|
||||
subtask: data.subtask,
|
||||
argumentHint: data["argument-hint"],
|
||||
}
|
||||
|
||||
53
src/hooks/agent-usage-reminder/constants.ts
Normal file
53
src/hooks/agent-usage-reminder/constants.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { join } from "node:path";
|
||||
import { xdgData } from "xdg-basedir";
|
||||
|
||||
export const OPENCODE_STORAGE = join(xdgData ?? "", "opencode", "storage");
|
||||
export const AGENT_USAGE_REMINDER_STORAGE = join(
|
||||
OPENCODE_STORAGE,
|
||||
"agent-usage-reminder",
|
||||
);
|
||||
|
||||
// All tool names normalized to lowercase for case-insensitive matching
|
||||
export const TARGET_TOOLS = new Set([
|
||||
"grep",
|
||||
"safe_grep",
|
||||
"glob",
|
||||
"safe_glob",
|
||||
"webfetch",
|
||||
"context7_resolve-library-id",
|
||||
"context7_get-library-docs",
|
||||
"websearch_exa_web_search_exa",
|
||||
"grep_app_searchgithub",
|
||||
]);
|
||||
|
||||
export const AGENT_TOOLS = new Set([
|
||||
"task",
|
||||
"call_omo_agent",
|
||||
"background_task",
|
||||
]);
|
||||
|
||||
export const REMINDER_MESSAGE = `
|
||||
[Agent Usage Reminder]
|
||||
|
||||
You called a search/fetch tool directly without leveraging specialized agents.
|
||||
|
||||
RECOMMENDED: Use background_task with explore/librarian agents for better results:
|
||||
|
||||
\`\`\`
|
||||
// Parallel exploration - fire multiple agents simultaneously
|
||||
background_task(agent="explore", prompt="Find all files matching pattern X")
|
||||
background_task(agent="explore", prompt="Search for implementation of Y")
|
||||
background_task(agent="librarian", prompt="Lookup documentation for Z")
|
||||
|
||||
// Then continue your work while they run in background
|
||||
// System will notify you when each completes
|
||||
\`\`\`
|
||||
|
||||
WHY:
|
||||
- Agents can perform deeper, more thorough searches
|
||||
- Background tasks run in parallel, saving time
|
||||
- Specialized agents have domain expertise
|
||||
- Reduces context window usage in main session
|
||||
|
||||
ALWAYS prefer: Multiple parallel background_task calls > Direct tool calls
|
||||
`;
|
||||
109
src/hooks/agent-usage-reminder/index.ts
Normal file
109
src/hooks/agent-usage-reminder/index.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin";
|
||||
import {
|
||||
loadAgentUsageState,
|
||||
saveAgentUsageState,
|
||||
clearAgentUsageState,
|
||||
} from "./storage";
|
||||
import { TARGET_TOOLS, AGENT_TOOLS, REMINDER_MESSAGE } from "./constants";
|
||||
import type { AgentUsageState } from "./types";
|
||||
|
||||
interface ToolExecuteInput {
|
||||
tool: string;
|
||||
sessionID: string;
|
||||
callID: string;
|
||||
}
|
||||
|
||||
interface ToolExecuteOutput {
|
||||
title: string;
|
||||
output: string;
|
||||
metadata: unknown;
|
||||
}
|
||||
|
||||
interface EventInput {
|
||||
event: {
|
||||
type: string;
|
||||
properties?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export function createAgentUsageReminderHook(_ctx: PluginInput) {
|
||||
const sessionStates = new Map<string, AgentUsageState>();
|
||||
|
||||
function getOrCreateState(sessionID: string): AgentUsageState {
|
||||
if (!sessionStates.has(sessionID)) {
|
||||
const persisted = loadAgentUsageState(sessionID);
|
||||
const state: AgentUsageState = persisted ?? {
|
||||
sessionID,
|
||||
agentUsed: false,
|
||||
reminderCount: 0,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
sessionStates.set(sessionID, state);
|
||||
}
|
||||
return sessionStates.get(sessionID)!;
|
||||
}
|
||||
|
||||
function markAgentUsed(sessionID: string): void {
|
||||
const state = getOrCreateState(sessionID);
|
||||
state.agentUsed = true;
|
||||
state.updatedAt = Date.now();
|
||||
saveAgentUsageState(state);
|
||||
}
|
||||
|
||||
function resetState(sessionID: string): void {
|
||||
sessionStates.delete(sessionID);
|
||||
clearAgentUsageState(sessionID);
|
||||
}
|
||||
|
||||
const toolExecuteAfter = async (
|
||||
input: ToolExecuteInput,
|
||||
output: ToolExecuteOutput,
|
||||
) => {
|
||||
const { tool, sessionID } = input;
|
||||
const toolLower = tool.toLowerCase();
|
||||
|
||||
if (AGENT_TOOLS.has(toolLower)) {
|
||||
markAgentUsed(sessionID);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TARGET_TOOLS.has(toolLower)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = getOrCreateState(sessionID);
|
||||
|
||||
if (state.agentUsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
output.output += REMINDER_MESSAGE;
|
||||
state.reminderCount++;
|
||||
state.updatedAt = Date.now();
|
||||
saveAgentUsageState(state);
|
||||
};
|
||||
|
||||
const eventHandler = async ({ event }: EventInput) => {
|
||||
const props = event.properties as Record<string, unknown> | undefined;
|
||||
|
||||
if (event.type === "session.deleted") {
|
||||
const sessionInfo = props?.info as { id?: string } | undefined;
|
||||
if (sessionInfo?.id) {
|
||||
resetState(sessionInfo.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "session.compacted") {
|
||||
const sessionID = (props?.sessionID ??
|
||||
(props?.info as { id?: string } | undefined)?.id) as string | undefined;
|
||||
if (sessionID) {
|
||||
resetState(sessionID);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
"tool.execute.after": toolExecuteAfter,
|
||||
event: eventHandler,
|
||||
};
|
||||
}
|
||||
42
src/hooks/agent-usage-reminder/storage.ts
Normal file
42
src/hooks/agent-usage-reminder/storage.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { AGENT_USAGE_REMINDER_STORAGE } from "./constants";
|
||||
import type { AgentUsageState } from "./types";
|
||||
|
||||
function getStoragePath(sessionID: string): string {
|
||||
return join(AGENT_USAGE_REMINDER_STORAGE, `${sessionID}.json`);
|
||||
}
|
||||
|
||||
export function loadAgentUsageState(sessionID: string): AgentUsageState | null {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (!existsSync(filePath)) return null;
|
||||
|
||||
try {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
return JSON.parse(content) as AgentUsageState;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveAgentUsageState(state: AgentUsageState): void {
|
||||
if (!existsSync(AGENT_USAGE_REMINDER_STORAGE)) {
|
||||
mkdirSync(AGENT_USAGE_REMINDER_STORAGE, { recursive: true });
|
||||
}
|
||||
|
||||
const filePath = getStoragePath(state.sessionID);
|
||||
writeFileSync(filePath, JSON.stringify(state, null, 2));
|
||||
}
|
||||
|
||||
export function clearAgentUsageState(sessionID: string): void {
|
||||
const filePath = getStoragePath(sessionID);
|
||||
if (existsSync(filePath)) {
|
||||
unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
6
src/hooks/agent-usage-reminder/types.ts
Normal file
6
src/hooks/agent-usage-reminder/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface AgentUsageState {
|
||||
sessionID: string;
|
||||
agentUsed: boolean;
|
||||
reminderCount: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
@@ -1,18 +1,47 @@
|
||||
import * as fs from "node:fs"
|
||||
import { VERSION_FILE } from "./constants"
|
||||
import * as path from "node:path"
|
||||
import { CACHE_DIR, PACKAGE_NAME } from "./constants"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
export function invalidateCache(): boolean {
|
||||
export function invalidatePackage(packageName: string = PACKAGE_NAME): boolean {
|
||||
try {
|
||||
if (fs.existsSync(VERSION_FILE)) {
|
||||
fs.unlinkSync(VERSION_FILE)
|
||||
log(`[auto-update-checker] Cache invalidated: ${VERSION_FILE}`)
|
||||
return true
|
||||
const pkgDir = path.join(CACHE_DIR, "node_modules", packageName)
|
||||
const pkgJsonPath = path.join(CACHE_DIR, "package.json")
|
||||
|
||||
let packageRemoved = false
|
||||
let dependencyRemoved = false
|
||||
|
||||
if (fs.existsSync(pkgDir)) {
|
||||
fs.rmSync(pkgDir, { recursive: true, force: true })
|
||||
log(`[auto-update-checker] Package removed: ${pkgDir}`)
|
||||
packageRemoved = true
|
||||
}
|
||||
log("[auto-update-checker] Version file not found, nothing to invalidate")
|
||||
return false
|
||||
|
||||
if (fs.existsSync(pkgJsonPath)) {
|
||||
const content = fs.readFileSync(pkgJsonPath, "utf-8")
|
||||
const pkgJson = JSON.parse(content)
|
||||
if (pkgJson.dependencies?.[packageName]) {
|
||||
delete pkgJson.dependencies[packageName]
|
||||
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2))
|
||||
log(`[auto-update-checker] Dependency removed from package.json: ${packageName}`)
|
||||
dependencyRemoved = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!packageRemoved && !dependencyRemoved) {
|
||||
log(`[auto-update-checker] Package not found, nothing to invalidate: ${packageName}`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
log("[auto-update-checker] Failed to invalidate cache:", err)
|
||||
log("[auto-update-checker] Failed to invalidate package:", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use invalidatePackage instead - this nukes ALL plugins */
|
||||
export function invalidateCache(): boolean {
|
||||
log("[auto-update-checker] WARNING: invalidateCache is deprecated, use invalidatePackage")
|
||||
return invalidatePackage()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as fs from "node:fs"
|
||||
import * as path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import type { NpmDistTags, OpencodeConfig, PackageJson, UpdateCheckResult } from "./types"
|
||||
import {
|
||||
PACKAGE_NAME,
|
||||
@@ -7,22 +8,44 @@ import {
|
||||
NPM_FETCH_TIMEOUT,
|
||||
INSTALLED_PACKAGE_JSON,
|
||||
USER_OPENCODE_CONFIG,
|
||||
USER_OPENCODE_CONFIG_JSONC,
|
||||
} from "./constants"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
export function isLocalDevMode(directory: string): boolean {
|
||||
const projectConfig = path.join(directory, ".opencode", "opencode.json")
|
||||
return getLocalDevPath(directory) !== null
|
||||
}
|
||||
|
||||
for (const configPath of [projectConfig, USER_OPENCODE_CONFIG]) {
|
||||
function stripJsonComments(json: string): string {
|
||||
return json
|
||||
.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => (g ? "" : m))
|
||||
.replace(/,(\s*[}\]])/g, "$1")
|
||||
}
|
||||
|
||||
function getConfigPaths(directory: string): string[] {
|
||||
return [
|
||||
path.join(directory, ".opencode", "opencode.json"),
|
||||
path.join(directory, ".opencode", "opencode.jsonc"),
|
||||
USER_OPENCODE_CONFIG,
|
||||
USER_OPENCODE_CONFIG_JSONC,
|
||||
]
|
||||
}
|
||||
|
||||
export function getLocalDevPath(directory: string): string | null {
|
||||
for (const configPath of getConfigPaths(directory)) {
|
||||
try {
|
||||
if (!fs.existsSync(configPath)) continue
|
||||
const content = fs.readFileSync(configPath, "utf-8")
|
||||
const config = JSON.parse(content) as OpencodeConfig
|
||||
const config = JSON.parse(stripJsonComments(content)) as OpencodeConfig
|
||||
const plugins = config.plugin ?? []
|
||||
|
||||
for (const entry of plugins) {
|
||||
if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) {
|
||||
return true
|
||||
try {
|
||||
return fileURLToPath(entry)
|
||||
} catch {
|
||||
return entry.replace("file://", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -30,22 +53,68 @@ export function isLocalDevMode(directory: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
export function findPluginEntry(directory: string): string | null {
|
||||
const projectConfig = path.join(directory, ".opencode", "opencode.json")
|
||||
function findPackageJsonUp(startPath: string): string | null {
|
||||
try {
|
||||
const stat = fs.statSync(startPath)
|
||||
let dir = stat.isDirectory() ? startPath : path.dirname(startPath)
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const pkgPath = path.join(dir, "package.json")
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(pkgPath, "utf-8")
|
||||
const pkg = JSON.parse(content) as PackageJson
|
||||
if (pkg.name === PACKAGE_NAME) return pkgPath
|
||||
} catch {}
|
||||
}
|
||||
const parent = path.dirname(dir)
|
||||
if (parent === dir) break
|
||||
dir = parent
|
||||
}
|
||||
} catch {}
|
||||
return null
|
||||
}
|
||||
|
||||
for (const configPath of [projectConfig, USER_OPENCODE_CONFIG]) {
|
||||
export function getLocalDevVersion(directory: string): string | null {
|
||||
const localPath = getLocalDevPath(directory)
|
||||
if (!localPath) return null
|
||||
|
||||
try {
|
||||
const pkgPath = findPackageJsonUp(localPath)
|
||||
if (!pkgPath) return null
|
||||
const content = fs.readFileSync(pkgPath, "utf-8")
|
||||
const pkg = JSON.parse(content) as PackageJson
|
||||
return pkg.version ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export interface PluginEntryInfo {
|
||||
entry: string
|
||||
isPinned: boolean
|
||||
pinnedVersion: string | null
|
||||
}
|
||||
|
||||
export function findPluginEntry(directory: string): PluginEntryInfo | null {
|
||||
for (const configPath of getConfigPaths(directory)) {
|
||||
try {
|
||||
if (!fs.existsSync(configPath)) continue
|
||||
const content = fs.readFileSync(configPath, "utf-8")
|
||||
const config = JSON.parse(content) as OpencodeConfig
|
||||
const config = JSON.parse(stripJsonComments(content)) as OpencodeConfig
|
||||
const plugins = config.plugin ?? []
|
||||
|
||||
for (const entry of plugins) {
|
||||
if (entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`)) {
|
||||
return entry
|
||||
if (entry === PACKAGE_NAME) {
|
||||
return { entry, isPinned: false, pinnedVersion: null }
|
||||
}
|
||||
if (entry.startsWith(`${PACKAGE_NAME}@`)) {
|
||||
const pinnedVersion = entry.slice(PACKAGE_NAME.length + 1)
|
||||
const isPinned = pinnedVersion !== "latest"
|
||||
return { entry, isPinned, pinnedVersion: isPinned ? pinnedVersion : null }
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -58,13 +127,26 @@ export function findPluginEntry(directory: string): string | null {
|
||||
|
||||
export function getCachedVersion(): string | null {
|
||||
try {
|
||||
if (!fs.existsSync(INSTALLED_PACKAGE_JSON)) return null
|
||||
const content = fs.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8")
|
||||
const pkg = JSON.parse(content) as PackageJson
|
||||
return pkg.version ?? null
|
||||
} catch {
|
||||
return null
|
||||
if (fs.existsSync(INSTALLED_PACKAGE_JSON)) {
|
||||
const content = fs.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8")
|
||||
const pkg = JSON.parse(content) as PackageJson
|
||||
if (pkg.version) return pkg.version
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const pkgPath = findPackageJsonUp(currentDir)
|
||||
if (pkgPath) {
|
||||
const content = fs.readFileSync(pkgPath, "utf-8")
|
||||
const pkg = JSON.parse(content) as PackageJson
|
||||
if (pkg.version) return pkg.version
|
||||
}
|
||||
} catch (err) {
|
||||
log("[auto-update-checker] Failed to resolve version from current directory:", err)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export async function getLatestVersion(): Promise<string | null> {
|
||||
@@ -91,29 +173,33 @@ export async function getLatestVersion(): Promise<string | null> {
|
||||
export async function checkForUpdate(directory: string): Promise<UpdateCheckResult> {
|
||||
if (isLocalDevMode(directory)) {
|
||||
log("[auto-update-checker] Local dev mode detected, skipping update check")
|
||||
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: true }
|
||||
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: true, isPinned: false }
|
||||
}
|
||||
|
||||
const pluginEntry = findPluginEntry(directory)
|
||||
if (!pluginEntry) {
|
||||
const pluginInfo = findPluginEntry(directory)
|
||||
if (!pluginInfo) {
|
||||
log("[auto-update-checker] Plugin not found in config")
|
||||
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false }
|
||||
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false }
|
||||
}
|
||||
|
||||
if (pluginInfo.isPinned) {
|
||||
log(`[auto-update-checker] Version pinned to ${pluginInfo.pinnedVersion}, skipping update check`)
|
||||
return { needsUpdate: false, currentVersion: pluginInfo.pinnedVersion, latestVersion: null, isLocalDev: false, isPinned: true }
|
||||
}
|
||||
|
||||
const currentVersion = getCachedVersion()
|
||||
if (!currentVersion) {
|
||||
log("[auto-update-checker] No cached version found")
|
||||
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false }
|
||||
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false }
|
||||
}
|
||||
|
||||
const latestVersion = await getLatestVersion()
|
||||
if (!latestVersion) {
|
||||
log("[auto-update-checker] Failed to fetch latest version")
|
||||
return { needsUpdate: false, currentVersion, latestVersion: null, isLocalDev: false }
|
||||
return { needsUpdate: false, currentVersion, latestVersion: null, isLocalDev: false, isPinned: false }
|
||||
}
|
||||
|
||||
const needsUpdate = currentVersion !== latestVersion
|
||||
log(`[auto-update-checker] Current: ${currentVersion}, Latest: ${latestVersion}, NeedsUpdate: ${needsUpdate}`)
|
||||
|
||||
return { needsUpdate, currentVersion, latestVersion, isLocalDev: false }
|
||||
return { needsUpdate, currentVersion, latestVersion, isLocalDev: false, isPinned: false }
|
||||
}
|
||||
|
||||
@@ -38,3 +38,4 @@ function getUserConfigDir(): string {
|
||||
|
||||
export const USER_CONFIG_DIR = getUserConfigDir()
|
||||
export const USER_OPENCODE_CONFIG = path.join(USER_CONFIG_DIR, "opencode", "opencode.json")
|
||||
export const USER_OPENCODE_CONFIG_JSONC = path.join(USER_CONFIG_DIR, "opencode", "opencode.jsonc")
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { checkForUpdate } from "./checker"
|
||||
import { invalidateCache } from "./cache"
|
||||
import { checkForUpdate, getCachedVersion, getLocalDevVersion } from "./checker"
|
||||
import { invalidatePackage } from "./cache"
|
||||
import { PACKAGE_NAME } from "./constants"
|
||||
import { log } from "../../shared/logger"
|
||||
import type { AutoUpdateCheckerOptions } from "./types"
|
||||
|
||||
export function createAutoUpdateCheckerHook(ctx: PluginInput) {
|
||||
export function createAutoUpdateCheckerHook(ctx: PluginInput, options: AutoUpdateCheckerOptions = {}) {
|
||||
const { showStartupToast = true } = options
|
||||
let hasChecked = false
|
||||
|
||||
return {
|
||||
@@ -22,21 +24,36 @@ export function createAutoUpdateCheckerHook(ctx: PluginInput) {
|
||||
|
||||
if (result.isLocalDev) {
|
||||
log("[auto-update-checker] Skipped: local development mode")
|
||||
if (showStartupToast) {
|
||||
const version = getLocalDevVersion(ctx.directory) ?? getCachedVersion()
|
||||
await showVersionToast(ctx, version)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (result.isPinned) {
|
||||
log(`[auto-update-checker] Skipped: version pinned to ${result.currentVersion}`)
|
||||
if (showStartupToast) {
|
||||
await showVersionToast(ctx, result.currentVersion)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!result.needsUpdate) {
|
||||
log("[auto-update-checker] No update needed")
|
||||
if (showStartupToast) {
|
||||
await showVersionToast(ctx, result.currentVersion)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
invalidateCache()
|
||||
invalidatePackage(PACKAGE_NAME)
|
||||
|
||||
await ctx.client.tui
|
||||
.showToast({
|
||||
body: {
|
||||
title: `${PACKAGE_NAME} Update`,
|
||||
message: `v${result.latestVersion} available (current: v${result.currentVersion}). Restart OpenCode to apply.`,
|
||||
title: `OhMyOpenCode ${result.latestVersion}`,
|
||||
message: `OpenCode is now on Steroids. oMoMoMoMo...\nv${result.latestVersion} available. Restart OpenCode to apply.`,
|
||||
variant: "info" as const,
|
||||
duration: 8000,
|
||||
},
|
||||
@@ -51,6 +68,21 @@ export function createAutoUpdateCheckerHook(ctx: PluginInput) {
|
||||
}
|
||||
}
|
||||
|
||||
export type { UpdateCheckResult } from "./types"
|
||||
async function showVersionToast(ctx: PluginInput, version: string | null): Promise<void> {
|
||||
const displayVersion = version ?? "unknown"
|
||||
await ctx.client.tui
|
||||
.showToast({
|
||||
body: {
|
||||
title: `OhMyOpenCode ${displayVersion}`,
|
||||
message: "OpenCode is now on Steroids. oMoMoMoMo...",
|
||||
variant: "info" as const,
|
||||
duration: 5000,
|
||||
},
|
||||
})
|
||||
.catch(() => {})
|
||||
log(`[auto-update-checker] Startup toast shown: v${displayVersion}`)
|
||||
}
|
||||
|
||||
export type { UpdateCheckResult, AutoUpdateCheckerOptions } from "./types"
|
||||
export { checkForUpdate } from "./checker"
|
||||
export { invalidateCache } from "./cache"
|
||||
export { invalidatePackage, invalidateCache } from "./cache"
|
||||
|
||||
@@ -19,4 +19,9 @@ export interface UpdateCheckResult {
|
||||
currentVersion: string | null
|
||||
latestVersion: string | null
|
||||
isLocalDev: boolean
|
||||
isPinned: boolean
|
||||
}
|
||||
|
||||
export interface AutoUpdateCheckerOptions {
|
||||
showStartupToast?: boolean
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export { createSessionNotification } from "./session-notification";
|
||||
export { createSessionRecoveryHook, type SessionRecoveryHook } from "./session-recovery";
|
||||
export { createCommentCheckerHooks } from "./comment-checker";
|
||||
export { createGrepOutputTruncatorHook } from "./grep-output-truncator";
|
||||
export { createToolOutputTruncatorHook } from "./tool-output-truncator";
|
||||
export { createDirectoryAgentsInjectorHook } from "./directory-agents-injector";
|
||||
export { createDirectoryReadmeInjectorHook } from "./directory-readme-injector";
|
||||
export { createEmptyTaskResponseDetectorHook } from "./empty-task-response-detector";
|
||||
@@ -13,4 +14,7 @@ export { createClaudeCodeHooksHook } from "./claude-code-hooks";
|
||||
export { createRulesInjectorHook } from "./rules-injector";
|
||||
export { createBackgroundNotificationHook } from "./background-notification"
|
||||
export { createAutoUpdateCheckerHook } from "./auto-update-checker";
|
||||
export { createUltraworkModeHook } from "./ultrawork-mode";
|
||||
|
||||
export { createAgentUsageReminderHook } from "./agent-usage-reminder";
|
||||
export { createKeywordDetectorHook } from "./keyword-detector";
|
||||
export { createNonInteractiveEnvHook } from "./non-interactive-env";
|
||||
|
||||
69
src/hooks/keyword-detector/constants.ts
Normal file
69
src/hooks/keyword-detector/constants.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export const CODE_BLOCK_PATTERN = /```[\s\S]*?```/g
|
||||
export const INLINE_CODE_PATTERN = /`[^`]+`/g
|
||||
|
||||
export const KEYWORD_DETECTORS: Array<{ pattern: RegExp; message: string }> = [
|
||||
// ULTRAWORK: ulw, ultrawork
|
||||
{
|
||||
pattern: /\b(ultrawork|ulw)\b/i,
|
||||
message: `<ultrawork-mode>
|
||||
[CODE RED] Maximum precision required. Ultrathink before acting.
|
||||
|
||||
YOU MUST LEVERAGE ALL AVAILABLE AGENTS TO THEIR FULLEST POTENTIAL.
|
||||
TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
|
||||
|
||||
## AGENT UTILIZATION PRINCIPLES (by capability, not by name)
|
||||
- **Codebase Exploration**: Spawn exploration agents using BACKGROUND TASKS for file patterns, internal implementations, project structure
|
||||
- **Documentation & References**: Use librarian-type agents via BACKGROUND TASKS for API references, examples, external library docs
|
||||
- **Planning & Strategy**: NEVER plan yourself - ALWAYS spawn a dedicated planning agent for work breakdown
|
||||
- **High-IQ Reasoning**: Leverage specialized agents for architecture decisions, code review, strategic planning
|
||||
- **Frontend/UI Tasks**: Delegate to UI-specialized agents for design and implementation
|
||||
|
||||
## EXECUTION RULES
|
||||
- **TODO**: Track EVERY step. Mark complete IMMEDIATELY after each.
|
||||
- **PARALLEL**: Fire independent agent calls simultaneously via background_task - NEVER wait sequentially.
|
||||
- **BACKGROUND FIRST**: Use background_task for exploration/research agents (10+ concurrent if needed).
|
||||
- **VERIFY**: Re-read request after completion. Check ALL requirements met before reporting done.
|
||||
- **DELEGATE**: Don't do everything yourself - orchestrate specialized agents for their strengths.
|
||||
|
||||
## WORKFLOW
|
||||
1. Analyze the request and identify required capabilities
|
||||
2. Spawn exploration/librarian agents via background_task in PARALLEL (10+ if needed)
|
||||
3. Always Use Plan agent with gathered context to create detailed work breakdown
|
||||
4. Execute with continuous verification against original requirements
|
||||
|
||||
</ultrawork-mode>
|
||||
|
||||
---
|
||||
|
||||
`,
|
||||
},
|
||||
// SEARCH: EN/KO/JP/CN/VN
|
||||
{
|
||||
pattern:
|
||||
/\b(search|find|locate|lookup|look\s*up|explore|discover|scan|grep|query|browse|detect|trace|seek|track|pinpoint|hunt)\b|where\s+is|show\s+me|list\s+all|검색|찾아|탐색|조회|스캔|서치|뒤져|찾기|어디|추적|탐지|찾아봐|찾아내|보여줘|목록|検索|探して|見つけて|サーチ|探索|スキャン|どこ|発見|捜索|見つけ出す|一覧|搜索|查找|寻找|查询|检索|定位|扫描|发现|在哪里|找出来|列出|tìm kiếm|tra cứu|định vị|quét|phát hiện|truy tìm|tìm ra|ở đâu|liệt kê/i,
|
||||
message: `[search-mode]
|
||||
MAXIMIZE SEARCH EFFORT. Launch multiple background agents IN PARALLEL:
|
||||
- explore agents (codebase patterns, file structures, ast-grep)
|
||||
- librarian agents (remote repos, official docs, GitHub examples)
|
||||
Plus direct tools: Grep, ripgrep (rg), ast-grep (sg)
|
||||
NEVER stop at first result - be exhaustive.`,
|
||||
},
|
||||
// ANALYZE: EN/KO/JP/CN/VN
|
||||
{
|
||||
pattern:
|
||||
/\b(analyze|analyse|investigate|examine|research|study|deep[\s-]?dive|inspect|audit|evaluate|assess|review|diagnose|scrutinize|dissect|debug|comprehend|interpret|breakdown|understand)\b|why\s+is|how\s+does|how\s+to|분석|조사|파악|연구|검토|진단|이해|설명|원인|이유|뜯어봐|따져봐|평가|해석|디버깅|디버그|어떻게|왜|살펴|分析|調査|解析|検討|研究|診断|理解|説明|検証|精査|究明|デバッグ|なぜ|どう|仕組み|调查|检查|剖析|深入|诊断|解释|调试|为什么|原理|搞清楚|弄明白|phân tích|điều tra|nghiên cứu|kiểm tra|xem xét|chẩn đoán|giải thích|tìm hiểu|gỡ lỗi|tại sao/i,
|
||||
message: `[analyze-mode]
|
||||
DEEP ANALYSIS MODE. Execute in phases:
|
||||
|
||||
PHASE 1 - GATHER CONTEXT (10+ agents parallel):
|
||||
- 3+ explore agents (codebase structure, patterns, implementations)
|
||||
- 3+ librarian agents (official docs, best practices, examples)
|
||||
- 2+ general agents (different analytical perspectives)
|
||||
|
||||
PHASE 2 - EXPERT CONSULTATION (after Phase 1):
|
||||
- 3+ oracle agents in parallel with gathered context
|
||||
- Each oracle: different angle (architecture, performance, edge cases)
|
||||
|
||||
SYNTHESIZE: Cross-reference findings, identify consensus & contradictions.`,
|
||||
},
|
||||
]
|
||||
@@ -1,33 +1,25 @@
|
||||
import {
|
||||
ULTRAWORK_PATTERNS,
|
||||
KEYWORD_DETECTORS,
|
||||
CODE_BLOCK_PATTERN,
|
||||
INLINE_CODE_PATTERN,
|
||||
} from "./constants"
|
||||
|
||||
/**
|
||||
* Remove code blocks and inline code from text.
|
||||
* Prevents false positives when keywords appear in code.
|
||||
*/
|
||||
export function removeCodeBlocks(text: string): string {
|
||||
return text.replace(CODE_BLOCK_PATTERN, "").replace(INLINE_CODE_PATTERN, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect ultrawork keywords in text (excluding code blocks).
|
||||
*/
|
||||
export function detectUltraworkKeyword(text: string): boolean {
|
||||
export function detectKeywords(text: string): string[] {
|
||||
const textWithoutCode = removeCodeBlocks(text)
|
||||
return ULTRAWORK_PATTERNS.some((pattern) => pattern.test(textWithoutCode))
|
||||
return KEYWORD_DETECTORS.filter(({ pattern }) =>
|
||||
pattern.test(textWithoutCode)
|
||||
).map(({ message }) => message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from message parts.
|
||||
*/
|
||||
export function extractPromptText(
|
||||
parts: Array<{ type: string; text?: string }>
|
||||
): string {
|
||||
return parts
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text || "")
|
||||
.join("")
|
||||
.join(" ")
|
||||
}
|
||||
72
src/hooks/keyword-detector/index.ts
Normal file
72
src/hooks/keyword-detector/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { detectKeywords, extractPromptText } from "./detector"
|
||||
import { log } from "../../shared"
|
||||
import { injectHookMessage } from "../../features/hook-message-injector"
|
||||
|
||||
export * from "./detector"
|
||||
export * from "./constants"
|
||||
export * from "./types"
|
||||
|
||||
const injectedSessions = new Set<string>()
|
||||
|
||||
export function createKeywordDetectorHook() {
|
||||
return {
|
||||
"chat.message": async (
|
||||
input: {
|
||||
sessionID: string
|
||||
agent?: string
|
||||
model?: { providerID: string; modelID: string }
|
||||
messageID?: string
|
||||
},
|
||||
output: {
|
||||
message: Record<string, unknown>
|
||||
parts: Array<{ type: string; text?: string; [key: string]: unknown }>
|
||||
}
|
||||
): Promise<void> => {
|
||||
if (injectedSessions.has(input.sessionID)) {
|
||||
return
|
||||
}
|
||||
|
||||
const promptText = extractPromptText(output.parts)
|
||||
const messages = detectKeywords(promptText)
|
||||
|
||||
if (messages.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
log(`Keywords detected: ${messages.length}`, { sessionID: input.sessionID })
|
||||
|
||||
const message = output.message as {
|
||||
agent?: string
|
||||
model?: { modelID?: string; providerID?: string }
|
||||
path?: { cwd?: string; root?: string }
|
||||
tools?: Record<string, boolean>
|
||||
}
|
||||
|
||||
const context = messages.join("\n")
|
||||
const success = injectHookMessage(input.sessionID, context, {
|
||||
agent: message.agent,
|
||||
model: message.model,
|
||||
path: message.path,
|
||||
tools: message.tools,
|
||||
})
|
||||
|
||||
if (success) {
|
||||
injectedSessions.add(input.sessionID)
|
||||
log("Keyword context injected", { sessionID: input.sessionID })
|
||||
}
|
||||
},
|
||||
|
||||
event: async ({
|
||||
event,
|
||||
}: {
|
||||
event: { type: string; properties?: unknown }
|
||||
}) => {
|
||||
if (event.type === "session.deleted") {
|
||||
const props = event.properties as { info?: { id?: string } } | undefined
|
||||
if (props?.info?.id) {
|
||||
injectedSessions.delete(props.info.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
4
src/hooks/keyword-detector/types.ts
Normal file
4
src/hooks/keyword-detector/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface KeywordDetectorState {
|
||||
detected: boolean
|
||||
injected: boolean
|
||||
}
|
||||
9
src/hooks/non-interactive-env/constants.ts
Normal file
9
src/hooks/non-interactive-env/constants.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const HOOK_NAME = "non-interactive-env"
|
||||
|
||||
export const NON_INTERACTIVE_ENV: Record<string, string> = {
|
||||
CI: "true",
|
||||
DEBIAN_FRONTEND: "noninteractive",
|
||||
GIT_TERMINAL_PROMPT: "0",
|
||||
GCM_INTERACTIVE: "never",
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1",
|
||||
}
|
||||
34
src/hooks/non-interactive-env/index.ts
Normal file
34
src/hooks/non-interactive-env/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { HOOK_NAME, NON_INTERACTIVE_ENV } from "./constants"
|
||||
import { log } from "../../shared"
|
||||
|
||||
export * from "./constants"
|
||||
export * from "./types"
|
||||
|
||||
export function createNonInteractiveEnvHook(_ctx: PluginInput) {
|
||||
return {
|
||||
"tool.execute.before": async (
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
output: { args: Record<string, unknown> }
|
||||
): Promise<void> => {
|
||||
if (input.tool.toLowerCase() !== "bash") {
|
||||
return
|
||||
}
|
||||
|
||||
const command = output.args.command as string | undefined
|
||||
if (!command) {
|
||||
return
|
||||
}
|
||||
|
||||
output.args.env = {
|
||||
...(output.args.env as Record<string, string> | undefined),
|
||||
...NON_INTERACTIVE_ENV,
|
||||
}
|
||||
|
||||
log(`[${HOOK_NAME}] Set non-interactive environment variables`, {
|
||||
sessionID: input.sessionID,
|
||||
env: NON_INTERACTIVE_ENV,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
3
src/hooks/non-interactive-env/types.ts
Normal file
3
src/hooks/non-interactive-env/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface NonInteractiveEnvConfig {
|
||||
disabled?: boolean
|
||||
}
|
||||
@@ -56,7 +56,7 @@ async function sendNotification(
|
||||
await ctx.$`osascript -e ${"display notification \"" + escapedMessage + "\" with title \"" + escapedTitle + "\""}`
|
||||
break
|
||||
case "linux":
|
||||
await ctx.$`notify-send ${escapedTitle} ${escapedMessage}`
|
||||
await ctx.$`notify-send ${escapedTitle} ${escapedMessage}`.catch(() => {})
|
||||
break
|
||||
case "win32":
|
||||
await ctx.$`powershell -Command ${"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('" + escapedMessage + "', '" + escapedTitle + "')"}`
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
findMessageByIndexNeedingThinking,
|
||||
findMessagesWithOrphanThinking,
|
||||
findMessagesWithThinkingBlocks,
|
||||
findMessagesWithThinkingOnly,
|
||||
injectTextPart,
|
||||
prependThinkingPart,
|
||||
stripThinkingParts,
|
||||
@@ -177,6 +178,8 @@ async function recoverThinkingDisabledViolation(
|
||||
return anySuccess
|
||||
}
|
||||
|
||||
const PLACEHOLDER_TEXT = "[user interrupted]"
|
||||
|
||||
async function recoverEmptyContentMessage(
|
||||
_client: Client,
|
||||
sessionID: string,
|
||||
@@ -187,23 +190,28 @@ async function recoverEmptyContentMessage(
|
||||
const targetIndex = extractMessageIndex(error)
|
||||
const failedID = failedAssistantMsg.info?.id
|
||||
|
||||
const thinkingOnlyIDs = findMessagesWithThinkingOnly(sessionID)
|
||||
for (const messageID of thinkingOnlyIDs) {
|
||||
injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT)
|
||||
}
|
||||
|
||||
if (targetIndex !== null) {
|
||||
const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex)
|
||||
if (targetMessageID) {
|
||||
return injectTextPart(sessionID, targetMessageID, "(interrupted)")
|
||||
return injectTextPart(sessionID, targetMessageID, PLACEHOLDER_TEXT)
|
||||
}
|
||||
}
|
||||
|
||||
if (failedID) {
|
||||
if (injectTextPart(sessionID, failedID, "(interrupted)")) {
|
||||
if (injectTextPart(sessionID, failedID, PLACEHOLDER_TEXT)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const emptyMessageIDs = findEmptyMessages(sessionID)
|
||||
let anySuccess = false
|
||||
let anySuccess = thinkingOnlyIDs.length > 0
|
||||
for (const messageID of emptyMessageIDs) {
|
||||
if (injectTextPart(sessionID, messageID, "(interrupted)")) {
|
||||
if (injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT)) {
|
||||
anySuccess = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,20 +133,15 @@ export function findEmptyMessages(sessionID: string): string[] {
|
||||
|
||||
export function findEmptyMessageByIndex(sessionID: string, targetIndex: number): string | null {
|
||||
const messages = readMessages(sessionID)
|
||||
|
||||
// Try multiple indices to handle system message offset
|
||||
// API includes system message at index 0, storage may not
|
||||
const indicesToTry = [targetIndex, targetIndex - 1]
|
||||
|
||||
|
||||
// API index may differ from storage index due to system messages
|
||||
const indicesToTry = [targetIndex, targetIndex - 1, targetIndex - 2]
|
||||
|
||||
for (const idx of indicesToTry) {
|
||||
if (idx < 0 || idx >= messages.length) continue
|
||||
|
||||
const targetMsg = messages[idx]
|
||||
|
||||
// NOTE: Do NOT skip last assistant message here
|
||||
// If API returned an error, this message is NOT the final assistant message
|
||||
// (the API only allows empty content for the ACTUAL final assistant message)
|
||||
|
||||
|
||||
if (!messageHasContent(targetMsg.id)) {
|
||||
return targetMsg.id
|
||||
}
|
||||
@@ -177,6 +172,28 @@ export function findMessagesWithThinkingBlocks(sessionID: string): string[] {
|
||||
return result
|
||||
}
|
||||
|
||||
export function findMessagesWithThinkingOnly(sessionID: string): string[] {
|
||||
const messages = readMessages(sessionID)
|
||||
const result: string[] = []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role !== "assistant") continue
|
||||
|
||||
const parts = readParts(msg.id)
|
||||
if (parts.length === 0) continue
|
||||
|
||||
const hasThinking = parts.some((p) => THINKING_TYPES.has(p.type))
|
||||
const hasTextContent = parts.some(hasContent)
|
||||
|
||||
// Has thinking but no text content = orphan thinking
|
||||
if (hasThinking && !hasTextContent) {
|
||||
result.push(msg.id)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function findMessagesWithOrphanThinking(sessionID: string): string[] {
|
||||
const messages = readMessages(sessionID)
|
||||
const result: string[] = []
|
||||
|
||||
38
src/hooks/tool-output-truncator.ts
Normal file
38
src/hooks/tool-output-truncator.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { createDynamicTruncator } from "../shared/dynamic-truncator"
|
||||
|
||||
const TRUNCATABLE_TOOLS = [
|
||||
"Grep",
|
||||
"safe_grep",
|
||||
"Glob",
|
||||
"safe_glob",
|
||||
"lsp_find_references",
|
||||
"lsp_document_symbols",
|
||||
"lsp_workspace_symbols",
|
||||
"lsp_diagnostics",
|
||||
"ast_grep_search",
|
||||
]
|
||||
|
||||
export function createToolOutputTruncatorHook(ctx: PluginInput) {
|
||||
const truncator = createDynamicTruncator(ctx)
|
||||
|
||||
const toolExecuteAfter = async (
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
output: { title: string; output: string; metadata: unknown }
|
||||
) => {
|
||||
if (!TRUNCATABLE_TOOLS.includes(input.tool)) return
|
||||
|
||||
try {
|
||||
const { result, truncated } = await truncator.truncate(input.sessionID, output.output)
|
||||
if (truncated) {
|
||||
output.output = result
|
||||
}
|
||||
} catch {
|
||||
// Graceful degradation - don't break tool execution
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"tool.execute.after": toolExecuteAfter,
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/** Keyword patterns - "ultrawork", "ulw" (case-insensitive, word boundary) */
|
||||
export const ULTRAWORK_PATTERNS = [/\bultrawork\b/i, /\bulw\b/i]
|
||||
|
||||
/** Code block pattern to exclude from keyword detection */
|
||||
export const CODE_BLOCK_PATTERN = /```[\s\S]*?```/g
|
||||
|
||||
/** Inline code pattern to exclude */
|
||||
export const INLINE_CODE_PATTERN = /`[^`]+`/g
|
||||
|
||||
/**
|
||||
* ULTRAWORK_CONTEXT - Agent-Agnostic Guidance
|
||||
*
|
||||
* Key principles:
|
||||
* - NO specific agent names (oracle, librarian, etc.)
|
||||
* - Only provide guidance based on agent role/capability
|
||||
* - Emphasize parallel execution, TODO tracking, delegation
|
||||
*/
|
||||
export const ULTRAWORK_CONTEXT = `<ultrawork-mode>
|
||||
[CODE RED] Maximum precision required. Ultrathink before acting.
|
||||
|
||||
YOU MUST LEVERAGE ALL AVAILABLE AGENTS TO THEIR FULLEST POTENTIAL.
|
||||
TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
|
||||
|
||||
## AGENT UTILIZATION PRINCIPLES (by capability, not by name)
|
||||
- **Codebase Exploration**: Spawn exploration agents using BACKGROUND TASKS for file patterns, internal implementations, project structure
|
||||
- **Documentation & References**: Use librarian-type agents via BACKGROUND TASKS for API references, examples, external library docs
|
||||
- **Planning & Strategy**: NEVER plan yourself - ALWAYS spawn a dedicated planning agent for work breakdown
|
||||
- **High-IQ Reasoning**: Leverage specialized agents for architecture decisions, code review, strategic planning
|
||||
- **Frontend/UI Tasks**: Delegate to UI-specialized agents for design and implementation
|
||||
|
||||
## EXECUTION RULES
|
||||
- **TODO**: Track EVERY step. Mark complete IMMEDIATELY after each.
|
||||
- **PARALLEL**: Fire independent agent calls simultaneously via background_task - NEVER wait sequentially.
|
||||
- **BACKGROUND FIRST**: Use background_task for exploration/research agents (10+ concurrent if needed).
|
||||
- **VERIFY**: Re-read request after completion. Check ALL requirements met before reporting done.
|
||||
- **DELEGATE**: Don't do everything yourself - orchestrate specialized agents for their strengths.
|
||||
|
||||
## WORKFLOW
|
||||
1. Analyze the request and identify required capabilities
|
||||
2. Spawn exploration/librarian agents via background_task in PARALLEL (10+ if needed)
|
||||
3. Use planning agents to create detailed work breakdown
|
||||
4. Execute with continuous verification against original requirements
|
||||
|
||||
</ultrawork-mode>
|
||||
|
||||
---
|
||||
|
||||
`
|
||||
@@ -1,97 +0,0 @@
|
||||
import { detectUltraworkKeyword, extractPromptText } from "./detector"
|
||||
import { ULTRAWORK_CONTEXT } from "./constants"
|
||||
import type { UltraworkModeState } from "./types"
|
||||
import { log } from "../../shared"
|
||||
import { injectHookMessage } from "../../features/hook-message-injector"
|
||||
|
||||
export * from "./detector"
|
||||
export * from "./constants"
|
||||
export * from "./types"
|
||||
|
||||
const ultraworkModeState = new Map<string, UltraworkModeState>()
|
||||
|
||||
export function clearUltraworkModeState(sessionID: string): void {
|
||||
ultraworkModeState.delete(sessionID)
|
||||
}
|
||||
|
||||
export function createUltraworkModeHook() {
|
||||
return {
|
||||
/**
|
||||
* chat.message hook - detect ultrawork/ulw keywords, inject context via history
|
||||
*
|
||||
* Execution timing: AFTER claudeCodeHooks["chat.message"]
|
||||
* Behavior:
|
||||
* 1. Extract text from user prompt
|
||||
* 2. Detect ultrawork/ulw keywords (excluding code blocks)
|
||||
* 3. If detected, inject ULTRAWORK_CONTEXT via injectHookMessage (history injection)
|
||||
*/
|
||||
"chat.message": async (
|
||||
input: {
|
||||
sessionID: string
|
||||
agent?: string
|
||||
model?: { providerID: string; modelID: string }
|
||||
messageID?: string
|
||||
},
|
||||
output: {
|
||||
message: Record<string, unknown>
|
||||
parts: Array<{ type: string; text?: string; [key: string]: unknown }>
|
||||
}
|
||||
): Promise<void> => {
|
||||
const state: UltraworkModeState = {
|
||||
detected: false,
|
||||
injected: false,
|
||||
}
|
||||
|
||||
const promptText = extractPromptText(output.parts)
|
||||
|
||||
if (!detectUltraworkKeyword(promptText)) {
|
||||
ultraworkModeState.set(input.sessionID, state)
|
||||
return
|
||||
}
|
||||
|
||||
state.detected = true
|
||||
log("Ultrawork keyword detected", { sessionID: input.sessionID })
|
||||
|
||||
const message = output.message as {
|
||||
agent?: string
|
||||
model?: { modelID?: string; providerID?: string }
|
||||
path?: { cwd?: string; root?: string }
|
||||
tools?: Record<string, boolean>
|
||||
}
|
||||
|
||||
const success = injectHookMessage(input.sessionID, ULTRAWORK_CONTEXT, {
|
||||
agent: message.agent,
|
||||
model: message.model,
|
||||
path: message.path,
|
||||
tools: message.tools,
|
||||
})
|
||||
|
||||
if (success) {
|
||||
state.injected = true
|
||||
log("Ultrawork context injected via history", { sessionID: input.sessionID })
|
||||
} else {
|
||||
log("Ultrawork context injection failed", { sessionID: input.sessionID })
|
||||
}
|
||||
|
||||
ultraworkModeState.set(input.sessionID, state)
|
||||
},
|
||||
|
||||
/**
|
||||
* event hook - cleanup session state on deletion
|
||||
*/
|
||||
event: async ({
|
||||
event,
|
||||
}: {
|
||||
event: { type: string; properties?: unknown }
|
||||
}) => {
|
||||
if (event.type === "session.deleted") {
|
||||
const props = event.properties as
|
||||
| { info?: { id?: string } }
|
||||
| undefined
|
||||
if (props?.info?.id) {
|
||||
ultraworkModeState.delete(props.info.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
export interface UltraworkModeState {
|
||||
/** Whether ultrawork keyword was detected */
|
||||
detected: boolean
|
||||
/** Whether context was injected */
|
||||
injected: boolean
|
||||
}
|
||||
|
||||
export interface ModelRef {
|
||||
providerID: string
|
||||
modelID: string
|
||||
}
|
||||
|
||||
export interface MessageWithModel {
|
||||
model?: ModelRef
|
||||
}
|
||||
|
||||
export interface UltraworkModeInput {
|
||||
parts: Array<{ type: string; text?: string }>
|
||||
message: MessageWithModel
|
||||
}
|
||||
100
src/index.ts
100
src/index.ts
@@ -6,7 +6,7 @@ import {
|
||||
createSessionRecoveryHook,
|
||||
createSessionNotification,
|
||||
createCommentCheckerHooks,
|
||||
createGrepOutputTruncatorHook,
|
||||
createToolOutputTruncatorHook,
|
||||
createDirectoryAgentsInjectorHook,
|
||||
createDirectoryReadmeInjectorHook,
|
||||
createEmptyTaskResponseDetectorHook,
|
||||
@@ -16,7 +16,9 @@ import {
|
||||
createRulesInjectorHook,
|
||||
createBackgroundNotificationHook,
|
||||
createAutoUpdateCheckerHook,
|
||||
createUltraworkModeHook,
|
||||
createKeywordDetectorHook,
|
||||
createAgentUsageReminderHook,
|
||||
createNonInteractiveEnvHook,
|
||||
} from "./hooks";
|
||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||
import {
|
||||
@@ -64,11 +66,36 @@ function getUserConfigDir(): string {
|
||||
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
||||
}
|
||||
|
||||
const AGENT_NAME_MAP: Record<string, string> = {
|
||||
omo: "OmO",
|
||||
build: "build",
|
||||
oracle: "oracle",
|
||||
librarian: "librarian",
|
||||
explore: "explore",
|
||||
"frontend-ui-ux-engineer": "frontend-ui-ux-engineer",
|
||||
"document-writer": "document-writer",
|
||||
"multimodal-looker": "multimodal-looker",
|
||||
};
|
||||
|
||||
function normalizeAgentNames(agents: Record<string, unknown>): Record<string, unknown> {
|
||||
const normalized: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(agents)) {
|
||||
const normalizedKey = AGENT_NAME_MAP[key.toLowerCase()] ?? key;
|
||||
normalized[normalizedKey] = value;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function loadConfigFromPath(configPath: string): OhMyOpenCodeConfig | null {
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const content = fs.readFileSync(configPath, "utf-8");
|
||||
const rawConfig = JSON.parse(content);
|
||||
|
||||
if (rawConfig.agents && typeof rawConfig.agents === "object") {
|
||||
rawConfig.agents = normalizeAgentNames(rawConfig.agents);
|
||||
}
|
||||
|
||||
const result = OhMyOpenCodeConfigSchema.safeParse(rawConfig);
|
||||
|
||||
if (!result.success) {
|
||||
@@ -177,8 +204,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const commentChecker = isHookEnabled("comment-checker")
|
||||
? createCommentCheckerHooks()
|
||||
: null;
|
||||
const grepOutputTruncator = isHookEnabled("grep-output-truncator")
|
||||
? createGrepOutputTruncatorHook(ctx)
|
||||
const toolOutputTruncator = isHookEnabled("tool-output-truncator")
|
||||
? createToolOutputTruncatorHook(ctx)
|
||||
: null;
|
||||
const directoryAgentsInjector = isHookEnabled("directory-agents-injector")
|
||||
? createDirectoryAgentsInjectorHook(ctx)
|
||||
@@ -202,10 +229,18 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
? createRulesInjectorHook(ctx)
|
||||
: null;
|
||||
const autoUpdateChecker = isHookEnabled("auto-update-checker")
|
||||
? createAutoUpdateCheckerHook(ctx)
|
||||
? createAutoUpdateCheckerHook(ctx, {
|
||||
showStartupToast: isHookEnabled("startup-toast"),
|
||||
})
|
||||
: null;
|
||||
const ultraworkMode = isHookEnabled("ultrawork-mode")
|
||||
? createUltraworkModeHook()
|
||||
const keywordDetector = isHookEnabled("keyword-detector")
|
||||
? createKeywordDetectorHook()
|
||||
: null;
|
||||
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
|
||||
? createAgentUsageReminderHook(ctx)
|
||||
: null;
|
||||
const nonInteractiveEnv = isHookEnabled("non-interactive-env")
|
||||
? createNonInteractiveEnvHook(ctx)
|
||||
: null;
|
||||
|
||||
updateTerminalTitle({ sessionId: "main" });
|
||||
@@ -236,7 +271,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
|
||||
"chat.message": async (input, output) => {
|
||||
await claudeCodeHooks["chat.message"]?.(input, output);
|
||||
await ultraworkMode?.["chat.message"]?.(input, output);
|
||||
await keywordDetector?.["chat.message"]?.(input, output);
|
||||
},
|
||||
|
||||
config: async (config) => {
|
||||
@@ -248,12 +283,42 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const userAgents = (pluginConfig.claude_code?.agents ?? true) ? loadUserAgents() : {};
|
||||
const projectAgents = (pluginConfig.claude_code?.agents ?? true) ? loadProjectAgents() : {};
|
||||
|
||||
config.agent = {
|
||||
...builtinAgents,
|
||||
...userAgents,
|
||||
...projectAgents,
|
||||
...config.agent,
|
||||
};
|
||||
const isOmoEnabled = pluginConfig.omo_agent?.disabled !== true;
|
||||
|
||||
if (isOmoEnabled && builtinAgents.OmO) {
|
||||
// TODO: When OpenCode releases `default_agent` config option (PR #5313),
|
||||
// use `config.default_agent = "OmO"` instead of demoting build/plan.
|
||||
// Tracking: https://github.com/sst/opencode/pull/5313
|
||||
const { name: _planName, ...planConfigWithoutName } = config.agent?.plan ?? {};
|
||||
const omoPlanOverride = pluginConfig.agents?.["OmO-Plan"];
|
||||
const omoPlanBase = {
|
||||
...builtinAgents.OmO,
|
||||
...planConfigWithoutName,
|
||||
description: `${config.agent?.plan?.description ?? "Plan agent"} (OhMyOpenCode version)`,
|
||||
color: config.agent?.plan?.color ?? "#6495ED",
|
||||
};
|
||||
|
||||
const omoPlanConfig = omoPlanOverride ? deepMerge(omoPlanBase, omoPlanOverride) : omoPlanBase;
|
||||
|
||||
config.agent = {
|
||||
OmO: builtinAgents.OmO,
|
||||
"OmO-Plan": omoPlanConfig,
|
||||
...Object.fromEntries(Object.entries(builtinAgents).filter(([k]) => k !== "OmO")),
|
||||
...userAgents,
|
||||
...projectAgents,
|
||||
...config.agent,
|
||||
build: { ...config.agent?.build, mode: "subagent" },
|
||||
plan: { ...config.agent?.plan, mode: "subagent" },
|
||||
};
|
||||
} else {
|
||||
config.agent = {
|
||||
...builtinAgents,
|
||||
...userAgents,
|
||||
...projectAgents,
|
||||
...config.agent,
|
||||
};
|
||||
}
|
||||
|
||||
config.tools = {
|
||||
...config.tools,
|
||||
};
|
||||
@@ -319,7 +384,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
await rulesInjector?.event(input);
|
||||
await thinkMode?.event(input);
|
||||
await anthropicAutoCompact?.event(input);
|
||||
await ultraworkMode?.event(input);
|
||||
await keywordDetector?.event(input);
|
||||
await agentUsageReminder?.event(input);
|
||||
|
||||
const { event } = input;
|
||||
const props = event.properties as Record<string, unknown> | undefined;
|
||||
@@ -417,6 +483,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
|
||||
"tool.execute.before": async (input, output) => {
|
||||
await claudeCodeHooks["tool.execute.before"](input, output);
|
||||
await nonInteractiveEnv?.["tool.execute.before"](input, output);
|
||||
await commentChecker?.["tool.execute.before"](input, output);
|
||||
|
||||
if (input.sessionID === getMainSessionID()) {
|
||||
@@ -432,13 +499,14 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
|
||||
"tool.execute.after": async (input, output) => {
|
||||
await claudeCodeHooks["tool.execute.after"](input, output);
|
||||
await grepOutputTruncator?.["tool.execute.after"](input, output);
|
||||
await toolOutputTruncator?.["tool.execute.after"](input, output);
|
||||
await contextWindowMonitor?.["tool.execute.after"](input, output);
|
||||
await commentChecker?.["tool.execute.after"](input, output);
|
||||
await directoryAgentsInjector?.["tool.execute.after"](input, output);
|
||||
await directoryReadmeInjector?.["tool.execute.after"](input, output);
|
||||
await rulesInjector?.["tool.execute.after"](input, output);
|
||||
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
|
||||
await agentUsageReminder?.["tool.execute.after"](input, output);
|
||||
|
||||
if (input.sessionID === getMainSessionID()) {
|
||||
updateTerminalTitle({
|
||||
|
||||
164
src/shared/dynamic-truncator.ts
Normal file
164
src/shared/dynamic-truncator.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
|
||||
const ANTHROPIC_ACTUAL_LIMIT = 200_000
|
||||
const CHARS_PER_TOKEN_ESTIMATE = 4
|
||||
const DEFAULT_TARGET_MAX_TOKENS = 50_000
|
||||
|
||||
interface AssistantMessageInfo {
|
||||
role: "assistant"
|
||||
tokens: {
|
||||
input: number
|
||||
output: number
|
||||
reasoning: number
|
||||
cache: { read: number; write: number }
|
||||
}
|
||||
}
|
||||
|
||||
interface MessageWrapper {
|
||||
info: { role: string } & Partial<AssistantMessageInfo>
|
||||
}
|
||||
|
||||
export interface TruncationResult {
|
||||
result: string
|
||||
truncated: boolean
|
||||
removedCount?: number
|
||||
}
|
||||
|
||||
export interface TruncationOptions {
|
||||
targetMaxTokens?: number
|
||||
preserveHeaderLines?: number
|
||||
contextWindowLimit?: number
|
||||
}
|
||||
|
||||
function estimateTokens(text: string): number {
|
||||
return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE)
|
||||
}
|
||||
|
||||
export function truncateToTokenLimit(
|
||||
output: string,
|
||||
maxTokens: number,
|
||||
preserveHeaderLines = 3
|
||||
): TruncationResult {
|
||||
const currentTokens = estimateTokens(output)
|
||||
|
||||
if (currentTokens <= maxTokens) {
|
||||
return { result: output, truncated: false }
|
||||
}
|
||||
|
||||
const lines = output.split("\n")
|
||||
|
||||
if (lines.length <= preserveHeaderLines) {
|
||||
const maxChars = maxTokens * CHARS_PER_TOKEN_ESTIMATE
|
||||
return {
|
||||
result: output.slice(0, maxChars) + "\n\n[Output truncated due to context window limit]",
|
||||
truncated: true,
|
||||
}
|
||||
}
|
||||
|
||||
const headerLines = lines.slice(0, preserveHeaderLines)
|
||||
const contentLines = lines.slice(preserveHeaderLines)
|
||||
|
||||
const headerText = headerLines.join("\n")
|
||||
const headerTokens = estimateTokens(headerText)
|
||||
const truncationMessageTokens = 50
|
||||
const availableTokens = maxTokens - headerTokens - truncationMessageTokens
|
||||
|
||||
if (availableTokens <= 0) {
|
||||
return {
|
||||
result: headerText + "\n\n[Content truncated due to context window limit]",
|
||||
truncated: true,
|
||||
removedCount: contentLines.length,
|
||||
}
|
||||
}
|
||||
|
||||
const resultLines: string[] = []
|
||||
let currentTokenCount = 0
|
||||
|
||||
for (const line of contentLines) {
|
||||
const lineTokens = estimateTokens(line + "\n")
|
||||
if (currentTokenCount + lineTokens > availableTokens) {
|
||||
break
|
||||
}
|
||||
resultLines.push(line)
|
||||
currentTokenCount += lineTokens
|
||||
}
|
||||
|
||||
const truncatedContent = [...headerLines, ...resultLines].join("\n")
|
||||
const removedCount = contentLines.length - resultLines.length
|
||||
|
||||
return {
|
||||
result: truncatedContent + `\n\n[${removedCount} more lines truncated due to context window limit]`,
|
||||
truncated: true,
|
||||
removedCount,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getContextWindowUsage(
|
||||
ctx: PluginInput,
|
||||
sessionID: string
|
||||
): Promise<{ usedTokens: number; remainingTokens: number; usagePercentage: number } | null> {
|
||||
try {
|
||||
const response = await ctx.client.session.messages({
|
||||
path: { id: sessionID },
|
||||
})
|
||||
|
||||
const messages = (response.data ?? response) as MessageWrapper[]
|
||||
|
||||
const assistantMessages = messages
|
||||
.filter((m) => m.info.role === "assistant")
|
||||
.map((m) => m.info as AssistantMessageInfo)
|
||||
|
||||
if (assistantMessages.length === 0) return null
|
||||
|
||||
const lastAssistant = assistantMessages[assistantMessages.length - 1]
|
||||
const lastTokens = lastAssistant.tokens
|
||||
const usedTokens = (lastTokens?.input ?? 0) + (lastTokens?.cache?.read ?? 0)
|
||||
const remainingTokens = ANTHROPIC_ACTUAL_LIMIT - usedTokens
|
||||
|
||||
return {
|
||||
usedTokens,
|
||||
remainingTokens,
|
||||
usagePercentage: usedTokens / ANTHROPIC_ACTUAL_LIMIT,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function dynamicTruncate(
|
||||
ctx: PluginInput,
|
||||
sessionID: string,
|
||||
output: string,
|
||||
options: TruncationOptions = {}
|
||||
): Promise<TruncationResult> {
|
||||
const { targetMaxTokens = DEFAULT_TARGET_MAX_TOKENS, preserveHeaderLines = 3 } = options
|
||||
|
||||
const usage = await getContextWindowUsage(ctx, sessionID)
|
||||
|
||||
if (!usage) {
|
||||
return { result: output, truncated: false }
|
||||
}
|
||||
|
||||
const maxOutputTokens = Math.min(usage.remainingTokens * 0.5, targetMaxTokens)
|
||||
|
||||
if (maxOutputTokens <= 0) {
|
||||
return {
|
||||
result: "[Output suppressed - context window exhausted]",
|
||||
truncated: true,
|
||||
}
|
||||
}
|
||||
|
||||
return truncateToTokenLimit(output, maxOutputTokens, preserveHeaderLines)
|
||||
}
|
||||
|
||||
export function createDynamicTruncator(ctx: PluginInput) {
|
||||
return {
|
||||
truncate: (sessionID: string, output: string, options?: TruncationOptions) =>
|
||||
dynamicTruncate(ctx, sessionID, output, options),
|
||||
|
||||
getUsage: (sessionID: string) => getContextWindowUsage(ctx, sessionID),
|
||||
|
||||
truncateSync: (output: string, maxTokens: number, preserveHeaderLines?: number) =>
|
||||
truncateToTokenLimit(output, maxTokens, preserveHeaderLines),
|
||||
}
|
||||
}
|
||||
@@ -9,3 +9,4 @@ export * from "./pattern-matcher"
|
||||
export * from "./hook-disabled"
|
||||
export * from "./deep-merge"
|
||||
export * from "./file-utils"
|
||||
export * from "./dynamic-truncator"
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
/**
|
||||
* Sanitizes model field from frontmatter.
|
||||
* Always returns undefined to let SDK use default model.
|
||||
*
|
||||
* Claude Code and OpenCode use different model ID formats,
|
||||
* so we ignore the model field and let OpenCode use its configured default.
|
||||
*
|
||||
* @param _model - Raw model value from frontmatter (ignored)
|
||||
* @returns Always undefined to inherit default model
|
||||
*/
|
||||
export function sanitizeModelField(_model: unknown): undefined {
|
||||
type CommandSource = "claude-code" | "opencode"
|
||||
|
||||
export function sanitizeModelField(model: unknown, source: CommandSource = "claude-code"): string | undefined {
|
||||
if (source === "claude-code") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (typeof model === "string" && model.trim().length > 0) {
|
||||
return model.trim()
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -26,14 +26,18 @@ export function createBackgroundTask(manager: BackgroundManager) {
|
||||
args: {
|
||||
description: tool.schema.string().describe("Short task description (shown in status)"),
|
||||
prompt: tool.schema.string().describe("Full detailed prompt for the agent"),
|
||||
agent: tool.schema.string().describe("Agent type to use (any agent allowed)"),
|
||||
agent: tool.schema.string().describe("Agent type to use (any registered agent)"),
|
||||
},
|
||||
async execute(args: BackgroundTaskArgs, toolContext) {
|
||||
if (!args.agent || args.agent.trim() === "") {
|
||||
return `❌ Agent parameter is required. Please specify which agent to use (e.g., "explore", "librarian", "build", etc.)`
|
||||
}
|
||||
|
||||
try {
|
||||
const task = await manager.launch({
|
||||
description: args.description,
|
||||
prompt: args.prompt,
|
||||
agent: args.agent,
|
||||
agent: args.agent.trim(),
|
||||
parentSessionID: toolContext.sessionID,
|
||||
parentMessageID: toolContext.messageID,
|
||||
})
|
||||
@@ -72,8 +76,8 @@ function formatTaskStatus(task: BackgroundTask): string {
|
||||
const promptPreview = truncateText(task.prompt, 500)
|
||||
|
||||
let progressSection = ""
|
||||
if (task.progress) {
|
||||
progressSection = `\nTool calls: ${task.progress.toolCalls}\nLast tool: ${task.progress.lastTool ?? "N/A"}`
|
||||
if (task.progress?.lastTool) {
|
||||
progressSection = `\n| Last tool | ${task.progress.lastTool} |`
|
||||
}
|
||||
|
||||
let lastMessageSection = ""
|
||||
@@ -91,6 +95,17 @@ ${truncated}
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
let statusNote = ""
|
||||
if (task.status === "running") {
|
||||
statusNote = `
|
||||
|
||||
> **Note**: No need to wait explicitly - the system will notify you when this task completes.`
|
||||
} else if (task.status === "error") {
|
||||
statusNote = `
|
||||
|
||||
> **Failed**: The task encountered an error. Check the last message for details.`
|
||||
}
|
||||
|
||||
return `# Task Status
|
||||
|
||||
| Field | Value |
|
||||
@@ -101,7 +116,7 @@ ${truncated}
|
||||
| Status | **${task.status}** |
|
||||
| Duration | ${duration} |
|
||||
| Session ID | \`${task.sessionID}\` |${progressSection}
|
||||
|
||||
${statusNote}
|
||||
## Original Prompt
|
||||
|
||||
\`\`\`
|
||||
@@ -196,12 +211,7 @@ export function createBackgroundOutput(manager: BackgroundManager, client: Openc
|
||||
const shouldBlock = args.block === true
|
||||
const timeoutMs = Math.min(args.timeout ?? 60000, 600000)
|
||||
|
||||
// Non-blocking: return status immediately
|
||||
if (!shouldBlock) {
|
||||
return formatTaskStatus(task)
|
||||
}
|
||||
|
||||
// Already completed: return result immediately
|
||||
// Already completed: return result immediately (regardless of block flag)
|
||||
if (task.status === "completed") {
|
||||
return await formatTaskResult(task, client)
|
||||
}
|
||||
@@ -211,6 +221,11 @@ export function createBackgroundOutput(manager: BackgroundManager, client: Openc
|
||||
return formatTaskStatus(task)
|
||||
}
|
||||
|
||||
// Non-blocking and still running: return status
|
||||
if (!shouldBlock) {
|
||||
return formatTaskStatus(task)
|
||||
}
|
||||
|
||||
// Blocking: poll until completion or timeout
|
||||
const startTime = Date.now()
|
||||
|
||||
|
||||
@@ -67,9 +67,10 @@ Description: ${task.description}
|
||||
Agent: ${task.agent} (subagent)
|
||||
Status: ${task.status}
|
||||
|
||||
Use \`background_output\` tool with task_id="${task.id}" to check progress or retrieve results.
|
||||
- block=false: Check status without waiting
|
||||
- block=true (default): Wait for completion and get result`
|
||||
The system will notify you when the task completes.
|
||||
Use \`background_output\` tool with task_id="${task.id}" to check progress:
|
||||
- block=false (default): Check status immediately - returns full status info
|
||||
- block=true: Wait for completion (rarely needed since system notifies)`
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
return `Failed to launch background agent task: ${message}`
|
||||
@@ -114,17 +115,27 @@ async function executeSync(
|
||||
log(`[call_omo_agent] Sending prompt to session ${sessionID}`)
|
||||
log(`[call_omo_agent] Prompt text:`, args.prompt.substring(0, 100))
|
||||
|
||||
await ctx.client.session.prompt({
|
||||
path: { id: sessionID },
|
||||
body: {
|
||||
agent: args.subagent_type,
|
||||
tools: {
|
||||
task: false,
|
||||
call_omo_agent: false,
|
||||
try {
|
||||
await ctx.client.session.prompt({
|
||||
path: { id: sessionID },
|
||||
body: {
|
||||
agent: args.subagent_type,
|
||||
tools: {
|
||||
task: false,
|
||||
call_omo_agent: false,
|
||||
background_task: false,
|
||||
},
|
||||
parts: [{ type: "text", text: args.prompt }],
|
||||
},
|
||||
parts: [{ type: "text", text: args.prompt }],
|
||||
},
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
log(`[call_omo_agent] Prompt error:`, errorMessage)
|
||||
if (errorMessage.includes("agent.name") || errorMessage.includes("undefined")) {
|
||||
return `Error: Agent "${args.subagent_type}" not found. Make sure the agent is registered in your opencode.json or provided by a plugin.\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
|
||||
}
|
||||
return `Error: Failed to send prompt: ${errorMessage}\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`
|
||||
}
|
||||
|
||||
log(`[call_omo_agent] Prompt sent, fetching messages...`)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { existsSync } from "node:fs"
|
||||
import { join, dirname } from "node:path"
|
||||
import { spawnSync } from "node:child_process"
|
||||
import { getInstalledRipgrepPath, downloadAndInstallRipgrep } from "./downloader"
|
||||
|
||||
export type GrepBackend = "rg" | "grep"
|
||||
|
||||
@@ -10,6 +11,7 @@ interface ResolvedCli {
|
||||
}
|
||||
|
||||
let cachedCli: ResolvedCli | null = null
|
||||
let autoInstallAttempted = false
|
||||
|
||||
function findExecutable(name: string): string | null {
|
||||
const isWindows = process.platform === "win32"
|
||||
@@ -21,20 +23,18 @@ function findExecutable(name: string): string | null {
|
||||
return result.stdout.trim().split("\n")[0]
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
// Command execution failed
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getOpenCodeBundledRg(): string | null {
|
||||
// OpenCode binary directory (where opencode executable lives)
|
||||
const execPath = process.execPath
|
||||
const execDir = dirname(execPath)
|
||||
|
||||
const isWindows = process.platform === "win32"
|
||||
const rgName = isWindows ? "rg.exe" : "rg"
|
||||
|
||||
// Check common bundled locations
|
||||
const candidates = [
|
||||
join(execDir, rgName),
|
||||
join(execDir, "bin", rgName),
|
||||
@@ -54,32 +54,56 @@ function getOpenCodeBundledRg(): string | null {
|
||||
export function resolveGrepCli(): ResolvedCli {
|
||||
if (cachedCli) return cachedCli
|
||||
|
||||
// Priority 1: OpenCode bundled rg
|
||||
const bundledRg = getOpenCodeBundledRg()
|
||||
if (bundledRg) {
|
||||
cachedCli = { path: bundledRg, backend: "rg" }
|
||||
return cachedCli
|
||||
}
|
||||
|
||||
// Priority 2: System rg
|
||||
const systemRg = findExecutable("rg")
|
||||
if (systemRg) {
|
||||
cachedCli = { path: systemRg, backend: "rg" }
|
||||
return cachedCli
|
||||
}
|
||||
|
||||
// Priority 3: grep (fallback)
|
||||
const installedRg = getInstalledRipgrepPath()
|
||||
if (installedRg) {
|
||||
cachedCli = { path: installedRg, backend: "rg" }
|
||||
return cachedCli
|
||||
}
|
||||
|
||||
const grep = findExecutable("grep")
|
||||
if (grep) {
|
||||
cachedCli = { path: grep, backend: "grep" }
|
||||
return cachedCli
|
||||
}
|
||||
|
||||
// Last resort: assume rg is in PATH
|
||||
cachedCli = { path: "rg", backend: "rg" }
|
||||
return cachedCli
|
||||
}
|
||||
|
||||
export async function resolveGrepCliWithAutoInstall(): Promise<ResolvedCli> {
|
||||
const current = resolveGrepCli()
|
||||
|
||||
if (current.backend === "rg") {
|
||||
return current
|
||||
}
|
||||
|
||||
if (autoInstallAttempted) {
|
||||
return current
|
||||
}
|
||||
|
||||
autoInstallAttempted = true
|
||||
|
||||
try {
|
||||
const rgPath = await downloadAndInstallRipgrep()
|
||||
cachedCli = { path: rgPath, backend: "rg" }
|
||||
return cachedCli
|
||||
} catch {
|
||||
return current
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_MAX_DEPTH = 20
|
||||
export const DEFAULT_MAX_FILESIZE = "10M"
|
||||
export const DEFAULT_MAX_COUNT = 500
|
||||
|
||||
103
src/tools/grep/downloader.test.ts
Normal file
103
src/tools/grep/downloader.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import { mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
|
||||
// Import the function we'll create to replace glob
|
||||
import { findFileRecursive } from "./downloader"
|
||||
|
||||
describe("findFileRecursive", () => {
|
||||
let testDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
// #given - create temp directory for testing
|
||||
testDir = join(tmpdir(), `downloader-test-${Date.now()}`)
|
||||
mkdirSync(testDir, { recursive: true })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// cleanup
|
||||
if (existsSync(testDir)) {
|
||||
rmSync(testDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("should find file in root directory", () => {
|
||||
// #given
|
||||
const targetFile = join(testDir, "rg.exe")
|
||||
writeFileSync(targetFile, "dummy content")
|
||||
|
||||
// #when
|
||||
const result = findFileRecursive(testDir, "rg.exe")
|
||||
|
||||
// #then
|
||||
expect(result).toBe(targetFile)
|
||||
})
|
||||
|
||||
test("should find file in nested directory (ripgrep release structure)", () => {
|
||||
// #given - simulate ripgrep release zip structure
|
||||
const nestedDir = join(testDir, "ripgrep-14.1.1-x86_64-pc-windows-msvc")
|
||||
mkdirSync(nestedDir, { recursive: true })
|
||||
const targetFile = join(nestedDir, "rg.exe")
|
||||
writeFileSync(targetFile, "dummy content")
|
||||
|
||||
// #when
|
||||
const result = findFileRecursive(testDir, "rg.exe")
|
||||
|
||||
// #then
|
||||
expect(result).toBe(targetFile)
|
||||
})
|
||||
|
||||
test("should find file in deeply nested directory", () => {
|
||||
// #given
|
||||
const deepDir = join(testDir, "level1", "level2", "level3")
|
||||
mkdirSync(deepDir, { recursive: true })
|
||||
const targetFile = join(deepDir, "rg")
|
||||
writeFileSync(targetFile, "dummy content")
|
||||
|
||||
// #when
|
||||
const result = findFileRecursive(testDir, "rg")
|
||||
|
||||
// #then
|
||||
expect(result).toBe(targetFile)
|
||||
})
|
||||
|
||||
test("should return null when file not found", () => {
|
||||
// #given - empty directory
|
||||
|
||||
// #when
|
||||
const result = findFileRecursive(testDir, "nonexistent.exe")
|
||||
|
||||
// #then
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
|
||||
test("should find first match when multiple files exist", () => {
|
||||
// #given
|
||||
const dir1 = join(testDir, "dir1")
|
||||
const dir2 = join(testDir, "dir2")
|
||||
mkdirSync(dir1, { recursive: true })
|
||||
mkdirSync(dir2, { recursive: true })
|
||||
writeFileSync(join(dir1, "rg"), "first")
|
||||
writeFileSync(join(dir2, "rg"), "second")
|
||||
|
||||
// #when
|
||||
const result = findFileRecursive(testDir, "rg")
|
||||
|
||||
// #then
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.endsWith("rg")).toBe(true)
|
||||
})
|
||||
|
||||
test("should match exact filename, not partial", () => {
|
||||
// #given
|
||||
writeFileSync(join(testDir, "rg.exe.bak"), "backup file")
|
||||
writeFileSync(join(testDir, "not-rg.exe"), "wrong file")
|
||||
|
||||
// #when
|
||||
const result = findFileRecursive(testDir, "rg.exe")
|
||||
|
||||
// #then
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
178
src/tools/grep/downloader.ts
Normal file
178
src/tools/grep/downloader.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { existsSync, mkdirSync, chmodSync, unlinkSync, readdirSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { spawn } from "bun"
|
||||
|
||||
export function findFileRecursive(dir: string, filename: string): string | null {
|
||||
try {
|
||||
const entries = readdirSync(dir, { withFileTypes: true, recursive: true })
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && entry.name === filename) {
|
||||
return join(entry.parentPath ?? dir, entry.name)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const RG_VERSION = "14.1.1"
|
||||
|
||||
const PLATFORM_CONFIG: Record<string, { platform: string; extension: "tar.gz" | "zip" } | undefined> = {
|
||||
"arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" },
|
||||
"arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" },
|
||||
"x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" },
|
||||
"x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" },
|
||||
"x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" },
|
||||
}
|
||||
|
||||
function getPlatformKey(): string {
|
||||
return `${process.arch}-${process.platform}`
|
||||
}
|
||||
|
||||
function getInstallDir(): string {
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || "."
|
||||
return join(homeDir, ".cache", "oh-my-opencode", "bin")
|
||||
}
|
||||
|
||||
function getRgPath(): string {
|
||||
const isWindows = process.platform === "win32"
|
||||
return join(getInstallDir(), isWindows ? "rg.exe" : "rg")
|
||||
}
|
||||
|
||||
async function downloadFile(url: string, destPath: string): Promise<void> {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer()
|
||||
await Bun.write(destPath, buffer)
|
||||
}
|
||||
|
||||
async function extractTarGz(archivePath: string, destDir: string): Promise<void> {
|
||||
const platformKey = getPlatformKey()
|
||||
|
||||
const args = ["tar", "-xzf", archivePath, "--strip-components=1"]
|
||||
|
||||
if (platformKey.endsWith("-darwin")) {
|
||||
args.push("--include=*/rg")
|
||||
} else if (platformKey.endsWith("-linux")) {
|
||||
args.push("--wildcards", "*/rg")
|
||||
}
|
||||
|
||||
const proc = spawn(args, {
|
||||
cwd: destDir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
const exitCode = await proc.exited
|
||||
if (exitCode !== 0) {
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
throw new Error(`Failed to extract tar.gz: ${stderr}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function extractZipWindows(archivePath: string, destDir: string): Promise<void> {
|
||||
const proc = spawn(
|
||||
["powershell", "-Command", `Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`],
|
||||
{ stdout: "pipe", stderr: "pipe" }
|
||||
)
|
||||
const exitCode = await proc.exited
|
||||
if (exitCode !== 0) {
|
||||
throw new Error("Failed to extract zip with PowerShell")
|
||||
}
|
||||
|
||||
const foundPath = findFileRecursive(destDir, "rg.exe")
|
||||
if (foundPath) {
|
||||
const destPath = join(destDir, "rg.exe")
|
||||
if (foundPath !== destPath) {
|
||||
const { renameSync } = await import("node:fs")
|
||||
renameSync(foundPath, destPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function extractZipUnix(archivePath: string, destDir: string): Promise<void> {
|
||||
const proc = spawn(["unzip", "-o", archivePath, "-d", destDir], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
const exitCode = await proc.exited
|
||||
if (exitCode !== 0) {
|
||||
throw new Error("Failed to extract zip")
|
||||
}
|
||||
|
||||
const foundPath = findFileRecursive(destDir, "rg")
|
||||
if (foundPath) {
|
||||
const destPath = join(destDir, "rg")
|
||||
if (foundPath !== destPath) {
|
||||
const { renameSync } = await import("node:fs")
|
||||
renameSync(foundPath, destPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function extractZip(archivePath: string, destDir: string): Promise<void> {
|
||||
if (process.platform === "win32") {
|
||||
await extractZipWindows(archivePath, destDir)
|
||||
} else {
|
||||
await extractZipUnix(archivePath, destDir)
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadAndInstallRipgrep(): Promise<string> {
|
||||
const platformKey = getPlatformKey()
|
||||
const config = PLATFORM_CONFIG[platformKey]
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`Unsupported platform: ${platformKey}`)
|
||||
}
|
||||
|
||||
const installDir = getInstallDir()
|
||||
const rgPath = getRgPath()
|
||||
|
||||
if (existsSync(rgPath)) {
|
||||
return rgPath
|
||||
}
|
||||
|
||||
mkdirSync(installDir, { recursive: true })
|
||||
|
||||
const filename = `ripgrep-${RG_VERSION}-${config.platform}.${config.extension}`
|
||||
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${filename}`
|
||||
const archivePath = join(installDir, filename)
|
||||
|
||||
try {
|
||||
await downloadFile(url, archivePath)
|
||||
|
||||
if (config.extension === "tar.gz") {
|
||||
await extractTarGz(archivePath, installDir)
|
||||
} else {
|
||||
await extractZip(archivePath, installDir)
|
||||
}
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
chmodSync(rgPath, 0o755)
|
||||
}
|
||||
|
||||
if (!existsSync(rgPath)) {
|
||||
throw new Error("ripgrep binary not found after extraction")
|
||||
}
|
||||
|
||||
return rgPath
|
||||
} finally {
|
||||
if (existsSync(archivePath)) {
|
||||
try {
|
||||
unlinkSync(archivePath)
|
||||
} catch {
|
||||
// Cleanup failures are non-critical
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getInstalledRipgrepPath(): string | null {
|
||||
const rgPath = getRgPath()
|
||||
return existsSync(rgPath) ? rgPath : null
|
||||
}
|
||||
@@ -24,11 +24,12 @@ function discoverCommandsFromDir(commandsDir: string, scope: CommandScope): Comm
|
||||
const content = readFileSync(commandPath, "utf-8")
|
||||
const { data, body } = parseFrontmatter(content)
|
||||
|
||||
const isOpencodeSource = scope === "opencode" || scope === "opencode-project"
|
||||
const metadata: CommandMetadata = {
|
||||
name: commandName,
|
||||
description: data.description || "",
|
||||
argumentHint: data["argument-hint"],
|
||||
model: sanitizeModelField(data.model),
|
||||
model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"),
|
||||
agent: data.agent,
|
||||
subtask: Boolean(data.subtask),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user