Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
39
README.ko.md
39
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 에이전트 워크플로우를 제시하지는 않습니다.
|
||||
이 프로젝트는 그저 당신의 에이전트에게 좋은 동료를 소개시켜주고, 좋은 도구를 쥐어주는 것 뿐입니다.
|
||||
|
||||
## 설치
|
||||
|
||||
|
||||
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 for this amazing hero image.*
|
||||
|
||||
1037
ai-todolist.md
1037
ai-todolist.md
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.6",
|
||||
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
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**
|
||||
`;
|
||||
@@ -17,3 +17,4 @@ export const builtinAgents: Record<string, AgentConfig> = {
|
||||
|
||||
export * from "./types"
|
||||
export { createBuiltinAgents } from "./utils"
|
||||
export { BUILD_AGENT_PROMPT_EXTENSION } from "./build"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
|
||||
export type AgentName =
|
||||
export type BuiltinAgentName =
|
||||
| "oracle"
|
||||
| "librarian"
|
||||
| "explore"
|
||||
@@ -8,6 +8,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,5 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentName, AgentOverrideConfig, AgentOverrides } from "./types"
|
||||
import type { BuiltinAgentName, AgentOverrideConfig, AgentOverrides } from "./types"
|
||||
import { oracleAgent } from "./oracle"
|
||||
import { librarianAgent } from "./librarian"
|
||||
import { exploreAgent } from "./explore"
|
||||
@@ -8,7 +8,7 @@ import { documentWriterAgent } from "./document-writer"
|
||||
import { multimodalLookerAgent } from "./multimodal-looker"
|
||||
import { deepMerge } from "../shared"
|
||||
|
||||
const allBuiltinAgents: Record<AgentName, AgentConfig> = {
|
||||
const allBuiltinAgents: Record<BuiltinAgentName, AgentConfig> = {
|
||||
oracle: oracleAgent,
|
||||
librarian: librarianAgent,
|
||||
explore: exploreAgent,
|
||||
@@ -25,13 +25,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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const AgentPermissionSchema = z.object({
|
||||
external_directory: PermissionValue.optional(),
|
||||
})
|
||||
|
||||
export const AgentNameSchema = z.enum([
|
||||
export const BuiltinAgentNameSchema = z.enum([
|
||||
"oracle",
|
||||
"librarian",
|
||||
"explore",
|
||||
@@ -25,6 +25,18 @@ export const AgentNameSchema = z.enum([
|
||||
"multimodal-looker",
|
||||
])
|
||||
|
||||
export const OverridableAgentNameSchema = z.enum([
|
||||
"build",
|
||||
"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",
|
||||
@@ -41,6 +53,7 @@ export const HookNameSchema = z.enum([
|
||||
"background-notification",
|
||||
"auto-update-checker",
|
||||
"ultrawork-mode",
|
||||
"agent-usage-reminder",
|
||||
])
|
||||
|
||||
export const AgentOverrideConfigSchema = z.object({
|
||||
@@ -61,6 +74,7 @@ export const AgentOverrideConfigSchema = z.object({
|
||||
|
||||
export const AgentOverridesSchema = z
|
||||
.object({
|
||||
build: AgentOverrideConfigSchema.optional(),
|
||||
oracle: AgentOverrideConfigSchema.optional(),
|
||||
librarian: AgentOverrideConfigSchema.optional(),
|
||||
explore: AgentOverrideConfigSchema.optional(),
|
||||
@@ -81,7 +95,7 @@ export const ClaudeCodeConfigSchema = z.object({
|
||||
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(),
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -14,3 +14,4 @@ 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";
|
||||
|
||||
22
src/index.ts
22
src/index.ts
@@ -1,5 +1,5 @@
|
||||
import type { Plugin } from "@opencode-ai/plugin";
|
||||
import { createBuiltinAgents } from "./agents";
|
||||
import { createBuiltinAgents, BUILD_AGENT_PROMPT_EXTENSION } from "./agents";
|
||||
import {
|
||||
createTodoContinuationEnforcer,
|
||||
createContextWindowMonitorHook,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
createBackgroundNotificationHook,
|
||||
createAutoUpdateCheckerHook,
|
||||
createUltraworkModeHook,
|
||||
createAgentUsageReminderHook,
|
||||
} from "./hooks";
|
||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||
import {
|
||||
@@ -207,6 +208,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const ultraworkMode = isHookEnabled("ultrawork-mode")
|
||||
? createUltraworkModeHook()
|
||||
: null;
|
||||
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
|
||||
? createAgentUsageReminderHook(ctx)
|
||||
: null;
|
||||
|
||||
updateTerminalTitle({ sessionId: "main" });
|
||||
|
||||
@@ -254,6 +258,20 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
...projectAgents,
|
||||
...config.agent,
|
||||
};
|
||||
|
||||
// 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 = {
|
||||
...config.tools,
|
||||
};
|
||||
@@ -320,6 +338,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
await thinkMode?.event(input);
|
||||
await anthropicAutoCompact?.event(input);
|
||||
await ultraworkMode?.event(input);
|
||||
await agentUsageReminder?.event(input);
|
||||
|
||||
const { event } = input;
|
||||
const props = event.properties as Record<string, unknown> | undefined;
|
||||
@@ -439,6 +458,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
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({
|
||||
|
||||
@@ -72,8 +72,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 +91,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 +112,7 @@ ${truncated}
|
||||
| Status | **${task.status}** |
|
||||
| Duration | ${duration} |
|
||||
| Session ID | \`${task.sessionID}\` |${progressSection}
|
||||
|
||||
${statusNote}
|
||||
## Original Prompt
|
||||
|
||||
\`\`\`
|
||||
|
||||
Reference in New Issue
Block a user