Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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>
|
||||
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>
|
||||
41
README.ko.md
41
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 에이전트 워크플로우를 제시하지는 않습니다.
|
||||
이 프로젝트는 그저 당신의 에이전트에게 좋은 동료를 소개시켜주고, 좋은 도구를 쥐어주는 것 뿐입니다.
|
||||
|
||||
## 설치
|
||||
|
||||
@@ -573,3 +602,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) 께 감사드립니다*
|
||||
|
||||
41
README.md
41
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.
|
||||
@@ -570,3 +603,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
15
bun.lock
15
bun.lock
@@ -10,6 +10,7 @@
|
||||
"@code-yeongyu/comment-checker": "^0.5.0",
|
||||
"@openauthjs/openauth": "^0.4.3",
|
||||
"@opencode-ai/plugin": "^1.0.150",
|
||||
"glob": "^13.0.0",
|
||||
"hono": "^4.10.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
@@ -70,6 +71,10 @@
|
||||
|
||||
"@code-yeongyu/comment-checker": ["@code-yeongyu/comment-checker@0.5.0", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "comment-checker": "bin/comment-checker" } }, "sha512-rKD2qQnTVUacsVQtpu3I5Sxi09X/XpOwS9fcmbUv1yfUL6llraaPuLmmxMBMRcmm7Zu31yEPVKCeUkVODfRL1g=="],
|
||||
|
||||
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
|
||||
|
||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
||||
|
||||
"@openauthjs/openauth": ["@openauthjs/openauth@0.4.3", "", { "dependencies": { "@standard-schema/spec": "1.0.0-beta.3", "aws4fetch": "1.0.20", "jose": "5.9.6" }, "peerDependencies": { "arctic": "^2.2.2", "hono": "^4.0.0" } }, "sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw=="],
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.0.150", "", { "dependencies": { "@opencode-ai/sdk": "1.0.150", "zod": "4.1.8" } }, "sha512-XmY3yydk120GBv2KeLxSZlElFx4Zx9TYLa3bS9X1TxXot42UeoMLEi3Xa46yboYnWwp4bC9Fu+Gd1E7hypG8Jw=="],
|
||||
@@ -124,12 +129,22 @@
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="],
|
||||
|
||||
"hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="],
|
||||
|
||||
"jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
|
||||
|
||||
"minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"oh-my-opencode": ["oh-my-opencode@0.1.30", "", { "dependencies": { "@ast-grep/cli": "^0.40.0", "@ast-grep/napi": "^0.40.0", "@code-yeongyu/comment-checker": "^0.4.1", "@opencode-ai/plugin": "^1.0.7", "xdg-basedir": "^5.1.0", "zod": "^4.1.8" }, "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-pXGGgL/7Jcz3yuGJJTI72BKern2egwfRz2LQZTBq+jl+pNCybOvGvXtFmR+WGlF8O3ZjL1wIHypBbIVuHOBzxg=="],
|
||||
|
||||
"path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.8",
|
||||
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
@@ -49,8 +49,9 @@
|
||||
"@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",
|
||||
"glob": "^13.0.0",
|
||||
"hono": "^4.10.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
|
||||
@@ -23,6 +23,19 @@ When you receive a user request, STOP and think deeply:
|
||||
- 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.
|
||||
@@ -53,7 +66,7 @@ todowrite([
|
||||
{ id: "test", content: "Test X feature", status: "pending", priority: "medium" }
|
||||
])
|
||||
|
||||
// 2. DELEGATE research in parallel
|
||||
// 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")
|
||||
|
||||
@@ -61,12 +74,54 @@ background_task(agent="librarian", prompt="Look up X documentation")
|
||||
// 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
|
||||
@@ -74,4 +129,5 @@ background_task(agent="librarian", prompt="Look up X documentation")
|
||||
- Agents have specialized expertise - USE THEM
|
||||
- TODO tracking gives users visibility into your progress
|
||||
- Parallel execution = faster results
|
||||
- **ALWAYS fire multiple independent operations simultaneously**
|
||||
`;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,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",
|
||||
@@ -52,7 +53,7 @@ export const HookNameSchema = z.enum([
|
||||
"rules-injector",
|
||||
"background-notification",
|
||||
"auto-update-checker",
|
||||
"ultrawork-mode",
|
||||
"keyword-detector",
|
||||
"agent-usage-reminder",
|
||||
])
|
||||
|
||||
|
||||
@@ -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,15 @@ 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: {
|
||||
task: false,
|
||||
background_task: false,
|
||||
background_output: false,
|
||||
background_cancel: false,
|
||||
call_omo_agent: false,
|
||||
},
|
||||
parts: [{ type: "text", text: input.prompt }],
|
||||
},
|
||||
@@ -90,8 +92,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"],
|
||||
}
|
||||
|
||||
@@ -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,5 +14,6 @@ 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";
|
||||
|
||||
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. Use planning agents 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
|
||||
}
|
||||
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
|
||||
}
|
||||
36
src/index.ts
36
src/index.ts
@@ -6,7 +6,7 @@ import {
|
||||
createSessionRecoveryHook,
|
||||
createSessionNotification,
|
||||
createCommentCheckerHooks,
|
||||
createGrepOutputTruncatorHook,
|
||||
createToolOutputTruncatorHook,
|
||||
createDirectoryAgentsInjectorHook,
|
||||
createDirectoryReadmeInjectorHook,
|
||||
createEmptyTaskResponseDetectorHook,
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
createRulesInjectorHook,
|
||||
createBackgroundNotificationHook,
|
||||
createAutoUpdateCheckerHook,
|
||||
createUltraworkModeHook,
|
||||
createKeywordDetectorHook,
|
||||
createAgentUsageReminderHook,
|
||||
} from "./hooks";
|
||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||
@@ -178,8 +178,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)
|
||||
@@ -205,8 +205,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const autoUpdateChecker = isHookEnabled("auto-update-checker")
|
||||
? createAutoUpdateCheckerHook(ctx)
|
||||
: null;
|
||||
const ultraworkMode = isHookEnabled("ultrawork-mode")
|
||||
? createUltraworkModeHook()
|
||||
const keywordDetector = isHookEnabled("keyword-detector")
|
||||
? createKeywordDetectorHook()
|
||||
: null;
|
||||
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
|
||||
? createAgentUsageReminderHook(ctx)
|
||||
@@ -240,7 +240,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) => {
|
||||
@@ -259,13 +259,17 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
...config.agent,
|
||||
};
|
||||
|
||||
if (config.agent.build) {
|
||||
const existingPrompt = config.agent.build.prompt || "";
|
||||
const userOverride = pluginConfig.agents?.build?.prompt || "";
|
||||
config.agent.build = {
|
||||
...config.agent.build,
|
||||
prompt: existingPrompt + BUILD_AGENT_PROMPT_EXTENSION + userOverride,
|
||||
};
|
||||
// Inject orchestration prompt to all non-subagent agents
|
||||
// Subagents are delegated TO, so they don't need orchestration guidance
|
||||
for (const [agentName, agentConfig] of Object.entries(config.agent ?? {})) {
|
||||
if (agentConfig && agentConfig.mode !== "subagent") {
|
||||
const existingPrompt = agentConfig.prompt || "";
|
||||
const userOverride = pluginConfig.agents?.[agentName as keyof typeof pluginConfig.agents]?.prompt || "";
|
||||
config.agent[agentName] = {
|
||||
...agentConfig,
|
||||
prompt: existingPrompt + BUILD_AGENT_PROMPT_EXTENSION + userOverride,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
config.tools = {
|
||||
@@ -333,7 +337,7 @@ 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;
|
||||
@@ -447,7 +451,7 @@ 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);
|
||||
|
||||
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,
|
||||
})
|
||||
@@ -207,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)
|
||||
}
|
||||
@@ -222,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,29 @@ 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,
|
||||
background_output: false,
|
||||
background_cancel: 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
|
||||
|
||||
168
src/tools/grep/downloader.ts
Normal file
168
src/tools/grep/downloader.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { existsSync, mkdirSync, chmodSync, unlinkSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { spawn } from "bun"
|
||||
|
||||
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 { globSync } = await import("glob")
|
||||
const rgFiles = globSync("**/rg.exe", { cwd: destDir })
|
||||
if (rgFiles.length > 0) {
|
||||
const srcPath = join(destDir, rgFiles[0])
|
||||
const destPath = join(destDir, "rg.exe")
|
||||
if (srcPath !== destPath) {
|
||||
const { renameSync } = await import("node:fs")
|
||||
renameSync(srcPath, 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 { globSync } = await import("glob")
|
||||
const rgFiles = globSync("**/rg", { cwd: destDir })
|
||||
if (rgFiles.length > 0) {
|
||||
const srcPath = join(destDir, rgFiles[0])
|
||||
const destPath = join(destDir, "rg")
|
||||
if (srcPath !== destPath) {
|
||||
const { renameSync } = await import("node:fs")
|
||||
renameSync(srcPath, 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