Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2920d5fe65 | ||
|
|
7fd52e27ce | ||
|
|
08481c046f | ||
|
|
192e8adf18 | ||
|
|
5dd4d97c94 | ||
|
|
b1abb7999b | ||
|
|
8618d57d95 | ||
|
|
4b6b725f13 | ||
|
|
1aaa6e6ba2 | ||
|
|
7cb8210e65 | ||
|
|
7e4b633bbd | ||
|
|
f44555a021 | ||
|
|
cccc7b7443 | ||
|
|
056b144174 | ||
|
|
7fef07da2e | ||
|
|
62307d987c | ||
|
|
24f2ee0c92 | ||
|
|
e836ad18ce | ||
|
|
0c237064b5 | ||
|
|
58279897ae |
77
README.ko.md
77
README.ko.md
@@ -261,8 +261,9 @@ opencode auth login
|
||||
|
||||
### Agents: 당신의 새로운 팀원들
|
||||
|
||||
- **OmO** (`anthropic/claude-opus-4-5`): **기본 에이전트입니다.** OpenCode를 위한 강력한 AI 오케스트레이터입니다. 전문 서브에이전트를 활용하여 복잡한 작업을 계획, 위임, 실행합니다. 백그라운드 태스크 위임과 todo 기반 워크플로우를 강조합니다. 최대 추론 능력을 위해 Claude Opus 4.5와 확장된 사고(32k 버짓)를 사용합니다.
|
||||
- **oracle** (`openai/gpt-5.2`): 아키텍처, 코드 리뷰, 전략 수립을 위한 전문가 조언자. GPT-5.2의 뛰어난 논리적 추론과 깊은 분석 능력을 활용합니다. AmpCode 에서 영감을 받았습니다.
|
||||
- **librarian** (`anthropic/claude-sonnet-4-5`): 멀티 레포 분석, 문서 조회, 구현 예제 담당. Claude Sonnet 4.5 의 뛰어난 지능, 훌륭한 도구 호출 능력을 활용합니다. AmpCode 에서 영감을 받았습니다.
|
||||
- **librarian** (`opencode/big-pickle`): 멀티 레포 분석, 문서 조회, 구현 예제 담당. OpenCode Zen을 통해 GLM-4.6(big-pickle)을 사용합니다—무료이고 빠르며 문서 조사에 탁월합니다. AmpCode 에서 영감을 받았습니다.
|
||||
- **explore** (`opencode/grok-code`): 빠른 코드베이스 탐색, 파일 패턴 매칭. Claude Code는 Haiku를 쓰지만, 우리는 Grok을 씁니다. 현재 무료이고, 극도로 빠르며, 파일 탐색 작업에 충분한 지능을 갖췄기 때문입니다. Claude Code 에서 영감을 받았습니다.
|
||||
- **frontend-ui-ux-engineer** (`google/gemini-3-pro-preview`): 개발자로 전향한 디자이너라는 설정을 갖고 있습니다. 멋진 UI를 만듭니다. 아름답고 창의적인 UI 코드를 생성하는 데 탁월한 Gemini를 사용합니다.
|
||||
- **document-writer** (`google/gemini-3-pro-preview`): 기술 문서 전문가라는 설정을 갖고 있습니다. Gemini 는 문학가입니다. 글을 기가막히게 씁니다.
|
||||
@@ -465,7 +466,12 @@ Oh My OpenCode는 다음 위치의 훅을 읽고 실행합니다:
|
||||
- **Anthropic Auto Compact**: Claude 모델이 토큰 제한에 도달하면 자동으로 세션을 요약하고 압축합니다. 수동 개입 없이 작업을 계속할 수 있습니다.
|
||||
- **Session Recovery**: 세션 에러(누락된 도구 결과, thinking 블록 문제, 빈 메시지 등)에서 자동 복구합니다. 돌다가 세션이 망가지지 않습니다. 망가져도 복구됩니다.
|
||||
- **Auto Update Checker**: oh-my-opencode의 새 버전이 출시되면 알림을 표시합니다.
|
||||
- **Startup Toast**: OhMyOpenCode 로드 시 환영 메시지를 표시합니다. 세션을 제대로 시작하기 위한 작은 "oMoMoMo".
|
||||
- **Background Notification**: 백그라운드 에이전트 작업이 완료되면 알림을 받습니다.
|
||||
- **Session Notification**: 에이전트가 대기 상태가 되면 OS 알림을 보냅니다. macOS, Linux, Windows에서 작동—에이전트가 입력을 기다릴 때 놓치지 마세요.
|
||||
- **Empty Task Response Detector**: Task 도구가 빈 응답을 반환하면 감지합니다. 이미 빈 응답이 왔는데 무한정 기다리는 상황을 방지합니다.
|
||||
- **Grep Output Truncator**: grep은 산더미 같은 텍스트를 반환할 수 있습니다. 남은 컨텍스트 윈도우에 따라 동적으로 출력을 축소합니다—50% 여유 공간 유지, 최대 50k 토큰.
|
||||
- **Tool Output Truncator**: 같은 아이디어, 더 넓은 범위. Grep, Glob, LSP 도구, AST-grep의 출력을 축소합니다. 한 번의 장황한 검색이 전체 컨텍스트를 잡아먹는 것을 방지합니다.
|
||||
|
||||
## 설정
|
||||
|
||||
@@ -515,6 +521,34 @@ Google Gemini 모델을 위한 내장 Antigravity OAuth를 활성화합니다:
|
||||
|
||||
각 에이전트에서 지원하는 옵션: `model`, `temperature`, `top_p`, `prompt`, `tools`, `disable`, `description`, `mode`, `color`, `permission`.
|
||||
|
||||
`OmO` (메인 오케스트레이터)와 `build` (기본 에이전트)도 동일한 옵션으로 설정을 오버라이드할 수 있습니다.
|
||||
|
||||
#### Permission 옵션
|
||||
|
||||
에이전트가 할 수 있는 작업을 세밀하게 제어합니다:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"explore": {
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "ask",
|
||||
"webfetch": "allow"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Permission | 설명 | 값 |
|
||||
|------------|------|-----|
|
||||
| `edit` | 파일 편집 권한 | `ask` / `allow` / `deny` |
|
||||
| `bash` | Bash 명령 실행 권한 | `ask` / `allow` / `deny` 또는 명령별: `{ "git": "allow", "rm": "deny" }` |
|
||||
| `webfetch` | 웹 요청 권한 | `ask` / `allow` / `deny` |
|
||||
| `doom_loop` | 무한 루프 감지 오버라이드 허용 | `ask` / `allow` / `deny` |
|
||||
| `external_directory` | 프로젝트 루트 외부 파일 접근 | `ask` / `allow` / `deny` |
|
||||
|
||||
또는 ~/.config/opencode/oh-my-opencode.json 혹은 .opencode/oh-my-opencode.json 의 `disabled_agents` 를 사용하여 비활성화할 수 있습니다:
|
||||
|
||||
```json
|
||||
@@ -525,6 +559,45 @@ Google Gemini 모델을 위한 내장 Antigravity OAuth를 활성화합니다:
|
||||
|
||||
사용 가능한 에이전트: `oracle`, `librarian`, `explore`, `frontend-ui-ux-engineer`, `document-writer`, `multimodal-looker`
|
||||
|
||||
### OmO Agent
|
||||
|
||||
활성화 시(기본값), OmO는 두 개의 primary 에이전트를 추가하고 내장 에이전트를 subagent로 강등합니다:
|
||||
|
||||
- **OmO**: Primary 오케스트레이터 에이전트 (Claude Opus 4.5)
|
||||
- **OmO-Plan**: OpenCode plan 에이전트의 모든 설정을 런타임에 상속 (description에 "OhMyOpenCode version" 추가)
|
||||
- **build**: subagent로 강등
|
||||
- **plan**: subagent로 강등
|
||||
|
||||
OmO를 비활성화하고 원래 build/plan 에이전트를 복원하려면:
|
||||
|
||||
```json
|
||||
{
|
||||
"omo_agent": {
|
||||
"disabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
다른 에이전트처럼 OmO와 OmO-Plan도 커스터마이징할 수 있습니다:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"OmO": {
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"temperature": 0.3
|
||||
},
|
||||
"OmO-Plan": {
|
||||
"model": "openai/gpt-5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 옵션 | 기본값 | 설명 |
|
||||
|------|--------|------|
|
||||
| `disabled` | `false` | `true`면 OmO 에이전트를 비활성화하고 원래 build/plan을 primary로 복원합니다. `false`(기본값)면 OmO와 OmO-Plan이 primary 에이전트가 됩니다. |
|
||||
|
||||
### Hooks
|
||||
|
||||
`~/.config/opencode/oh-my-opencode.json` 또는 `.opencode/oh-my-opencode.json`의 `disabled_hooks`를 통해 특정 내장 훅을 비활성화할 수 있습니다:
|
||||
@@ -535,7 +608,7 @@ Google Gemini 모델을 위한 내장 Antigravity OAuth를 활성화합니다:
|
||||
}
|
||||
```
|
||||
|
||||
사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`
|
||||
사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`
|
||||
|
||||
### MCPs
|
||||
|
||||
|
||||
77
README.md
77
README.md
@@ -262,8 +262,9 @@ The plugin works perfectly with defaults. Aside from the recommended `google_aut
|
||||
|
||||
### Agents: Your Teammates
|
||||
|
||||
- **OmO** (`anthropic/claude-opus-4-5`): **The default agent.** A powerful AI orchestrator for OpenCode. Plans, delegates, and executes complex tasks using specialized subagents with aggressive parallel execution. Emphasizes background task delegation and todo-driven workflow. Uses Claude Opus 4.5 with extended thinking (32k budget) for maximum reasoning capability.
|
||||
- **oracle** (`openai/gpt-5.2`): Architecture, code review, strategy. Uses GPT-5.2 for its stellar logical reasoning and deep analysis. Inspired by AmpCode.
|
||||
- **librarian** (`anthropic/claude-sonnet-4-5`): Multi-repo analysis, doc lookup, implementation examples. Claude Sonnet 4 is fast, smart, great at tool calls, and excellent for documentation research. Inspired by AmpCode.
|
||||
- **librarian** (`opencode/big-pickle`): Multi-repo analysis, doc lookup, implementation examples. Uses GLM-4.6 (big-pickle) via OpenCode Zen—free, fast, and excellent for documentation research. Inspired by AmpCode.
|
||||
- **explore** (`opencode/grok-code`): Fast codebase exploration and pattern matching. Claude Code uses Haiku; we use Grok—it's free, blazing fast, and plenty smart for file traversal. Inspired by Claude Code.
|
||||
- **frontend-ui-ux-engineer** (`google/gemini-3-pro-preview`): A designer turned developer. Builds gorgeous UIs. Gemini excels at creative, beautiful UI code.
|
||||
- **document-writer** (`google/gemini-3-pro-preview`): Technical writing expert. Gemini is a wordsmith—writes prose that flows.
|
||||
@@ -466,7 +467,12 @@ When agents thrive, you thrive. But I want to help you directly too.
|
||||
- **Anthropic Auto Compact**: When Claude models hit token limits, automatically summarizes and compacts the session—no manual intervention needed.
|
||||
- **Session Recovery**: Automatically recovers from session errors (missing tool results, thinking block issues, empty messages). Sessions don't crash mid-run. Even if they do, they recover.
|
||||
- **Auto Update Checker**: Notifies you when a new version of oh-my-opencode is available.
|
||||
- **Startup Toast**: Shows a welcome message when OhMyOpenCode loads. A little "oMoMoMo" to start your session right.
|
||||
- **Background Notification**: Get notified when background agent tasks complete.
|
||||
- **Session Notification**: Sends OS notifications when agents go idle. Works on macOS, Linux, and Windows—never miss when your agent needs input.
|
||||
- **Empty Task Response Detector**: Catches when Task tool returns nothing. Warns you about potential agent failures so you don't wait forever for a response that already came back empty.
|
||||
- **Grep Output Truncator**: Grep can return mountains of text. This dynamically truncates output based on your remaining context window—keeps 50% headroom, caps at 50k tokens.
|
||||
- **Tool Output Truncator**: Same idea, broader scope. Truncates output from Grep, Glob, LSP tools, and AST-grep. Prevents one verbose search from eating your entire context.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -516,6 +522,34 @@ Override built-in agent settings:
|
||||
|
||||
Each agent supports: `model`, `temperature`, `top_p`, `prompt`, `tools`, `disable`, `description`, `mode`, `color`, `permission`.
|
||||
|
||||
You can also override settings for `OmO` (the main orchestrator) and `build` (the default agent) using the same options.
|
||||
|
||||
#### Permission Options
|
||||
|
||||
Fine-grained control over what agents can do:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"explore": {
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "ask",
|
||||
"webfetch": "allow"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Permission | Description | Values |
|
||||
|------------|-------------|--------|
|
||||
| `edit` | File editing permission | `ask` / `allow` / `deny` |
|
||||
| `bash` | Bash command execution | `ask` / `allow` / `deny` or per-command: `{ "git": "allow", "rm": "deny" }` |
|
||||
| `webfetch` | Web request permission | `ask` / `allow` / `deny` |
|
||||
| `doom_loop` | Allow infinite loop detection override | `ask` / `allow` / `deny` |
|
||||
| `external_directory` | Access files outside project root | `ask` / `allow` / `deny` |
|
||||
|
||||
Or disable via `disabled_agents` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
@@ -526,6 +560,45 @@ Or disable via `disabled_agents` in `~/.config/opencode/oh-my-opencode.json` or
|
||||
|
||||
Available agents: `oracle`, `librarian`, `explore`, `frontend-ui-ux-engineer`, `document-writer`, `multimodal-looker`
|
||||
|
||||
### OmO Agent
|
||||
|
||||
When enabled (default), OmO adds two primary agents and demotes the built-in agents to subagents:
|
||||
|
||||
- **OmO**: Primary orchestrator agent (Claude Opus 4.5)
|
||||
- **OmO-Plan**: Inherits all settings from OpenCode's plan agent at runtime (description appended with "OhMyOpenCode version")
|
||||
- **build**: Demoted to subagent
|
||||
- **plan**: Demoted to subagent
|
||||
|
||||
To disable OmO and restore the original build/plan agents:
|
||||
|
||||
```json
|
||||
{
|
||||
"omo_agent": {
|
||||
"disabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also customize OmO and OmO-Plan like other agents:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"OmO": {
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"temperature": 0.3
|
||||
},
|
||||
"OmO-Plan": {
|
||||
"model": "openai/gpt-5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `disabled` | `false` | When `true`, disables OmO agents and restores original build/plan as primary. When `false` (default), OmO and OmO-Plan become primary agents. |
|
||||
|
||||
### Hooks
|
||||
|
||||
Disable specific built-in hooks via `disabled_hooks` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
@@ -536,7 +609,7 @@ Disable specific built-in hooks via `disabled_hooks` in `~/.config/opencode/oh-m
|
||||
}
|
||||
```
|
||||
|
||||
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`
|
||||
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env`
|
||||
|
||||
### MCPs
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.4",
|
||||
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -366,41 +366,6 @@ export const omoAgent: AgentConfig = {
|
||||
budgetTokens: 32000,
|
||||
},
|
||||
maxTokens: 128000,
|
||||
tools: {
|
||||
read: true,
|
||||
write: true,
|
||||
edit: true,
|
||||
multiedit: true,
|
||||
patch: true,
|
||||
glob: true,
|
||||
grep: true,
|
||||
list: true,
|
||||
bash: true,
|
||||
batch: true,
|
||||
webfetch: true,
|
||||
websearch: true,
|
||||
codesearch: true,
|
||||
todowrite: true,
|
||||
todoread: true,
|
||||
task: true,
|
||||
lsp_hover: true,
|
||||
lsp_goto_definition: true,
|
||||
lsp_find_references: true,
|
||||
lsp_document_symbols: true,
|
||||
lsp_workspace_symbols: true,
|
||||
lsp_diagnostics: true,
|
||||
lsp_rename: true,
|
||||
lsp_prepare_rename: true,
|
||||
lsp_code_actions: true,
|
||||
lsp_code_action_resolve: true,
|
||||
lsp_servers: true,
|
||||
ast_grep_search: true,
|
||||
ast_grep_replace: true,
|
||||
skill: true,
|
||||
call_omo_agent: true,
|
||||
background_task: true,
|
||||
background_output: true,
|
||||
},
|
||||
prompt: OMO_SYSTEM_PROMPT,
|
||||
color: "#00CED1",
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const AgentPermissionSchema = z.object({
|
||||
})
|
||||
|
||||
export const BuiltinAgentNameSchema = z.enum([
|
||||
"OmO",
|
||||
"oracle",
|
||||
"librarian",
|
||||
"explore",
|
||||
@@ -27,6 +28,9 @@ export const BuiltinAgentNameSchema = z.enum([
|
||||
|
||||
export const OverridableAgentNameSchema = z.enum([
|
||||
"build",
|
||||
"plan",
|
||||
"OmO",
|
||||
"OmO-Plan",
|
||||
"oracle",
|
||||
"librarian",
|
||||
"explore",
|
||||
@@ -56,6 +60,7 @@ export const HookNameSchema = z.enum([
|
||||
"startup-toast",
|
||||
"keyword-detector",
|
||||
"agent-usage-reminder",
|
||||
"non-interactive-env",
|
||||
])
|
||||
|
||||
export const AgentOverrideConfigSchema = z.object({
|
||||
@@ -76,6 +81,9 @@ export const AgentOverrideConfigSchema = z.object({
|
||||
|
||||
export const AgentOverridesSchema = z.object({
|
||||
build: AgentOverrideConfigSchema.optional(),
|
||||
plan: AgentOverrideConfigSchema.optional(),
|
||||
OmO: AgentOverrideConfigSchema.optional(),
|
||||
"OmO-Plan": AgentOverrideConfigSchema.optional(),
|
||||
oracle: AgentOverrideConfigSchema.optional(),
|
||||
librarian: AgentOverrideConfigSchema.optional(),
|
||||
explore: AgentOverrideConfigSchema.optional(),
|
||||
@@ -93,7 +101,7 @@ export const ClaudeCodeConfigSchema = z.object({
|
||||
})
|
||||
|
||||
export const OmoAgentConfigSchema = z.object({
|
||||
disable_build: z.boolean().optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const OhMyOpenCodeConfigSchema = z.object({
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
NPM_FETCH_TIMEOUT,
|
||||
INSTALLED_PACKAGE_JSON,
|
||||
USER_OPENCODE_CONFIG,
|
||||
USER_OPENCODE_CONFIG_JSONC,
|
||||
} from "./constants"
|
||||
import { log } from "../../shared/logger"
|
||||
|
||||
@@ -16,13 +17,22 @@ export function isLocalDevMode(directory: string): boolean {
|
||||
}
|
||||
|
||||
function stripJsonComments(json: string): string {
|
||||
return json.replace(/^\s*\/\/.*$/gm, "").replace(/,(\s*[}\]])/g, "$1")
|
||||
return json
|
||||
.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => (g ? "" : m))
|
||||
.replace(/,(\s*[}\]])/g, "$1")
|
||||
}
|
||||
|
||||
function getConfigPaths(directory: string): string[] {
|
||||
return [
|
||||
path.join(directory, ".opencode", "opencode.json"),
|
||||
path.join(directory, ".opencode", "opencode.jsonc"),
|
||||
USER_OPENCODE_CONFIG,
|
||||
USER_OPENCODE_CONFIG_JSONC,
|
||||
]
|
||||
}
|
||||
|
||||
export function getLocalDevPath(directory: string): string | null {
|
||||
const projectConfig = path.join(directory, ".opencode", "opencode.json")
|
||||
|
||||
for (const configPath of [projectConfig, USER_OPENCODE_CONFIG]) {
|
||||
for (const configPath of getConfigPaths(directory)) {
|
||||
try {
|
||||
if (!fs.existsSync(configPath)) continue
|
||||
const content = fs.readFileSync(configPath, "utf-8")
|
||||
@@ -31,7 +41,11 @@ export function getLocalDevPath(directory: string): string | null {
|
||||
|
||||
for (const entry of plugins) {
|
||||
if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) {
|
||||
return entry.replace("file://", "")
|
||||
try {
|
||||
return fileURLToPath(entry)
|
||||
} catch {
|
||||
return entry.replace("file://", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -86,9 +100,7 @@ export interface PluginEntryInfo {
|
||||
}
|
||||
|
||||
export function findPluginEntry(directory: string): PluginEntryInfo | null {
|
||||
const projectConfig = path.join(directory, ".opencode", "opencode.json")
|
||||
|
||||
for (const configPath of [projectConfig, USER_OPENCODE_CONFIG]) {
|
||||
for (const configPath of getConfigPaths(directory)) {
|
||||
try {
|
||||
if (!fs.existsSync(configPath)) continue
|
||||
const content = fs.readFileSync(configPath, "utf-8")
|
||||
@@ -170,7 +182,6 @@ export async function checkForUpdate(directory: string): Promise<UpdateCheckResu
|
||||
return { needsUpdate: false, currentVersion: null, latestVersion: null, isLocalDev: false, isPinned: false }
|
||||
}
|
||||
|
||||
// Respect version pinning
|
||||
if (pluginInfo.isPinned) {
|
||||
log(`[auto-update-checker] Version pinned to ${pluginInfo.pinnedVersion}, skipping update check`)
|
||||
return { needsUpdate: false, currentVersion: pluginInfo.pinnedVersion, latestVersion: null, isLocalDev: false, isPinned: true }
|
||||
@@ -190,6 +201,5 @@ export async function checkForUpdate(directory: string): Promise<UpdateCheckResu
|
||||
|
||||
const needsUpdate = currentVersion !== latestVersion
|
||||
log(`[auto-update-checker] Current: ${currentVersion}, Latest: ${latestVersion}, NeedsUpdate: ${needsUpdate}`)
|
||||
|
||||
return { needsUpdate, currentVersion, latestVersion, isLocalDev: false, isPinned: false }
|
||||
}
|
||||
|
||||
@@ -38,3 +38,4 @@ function getUserConfigDir(): string {
|
||||
|
||||
export const USER_CONFIG_DIR = getUserConfigDir()
|
||||
export const USER_OPENCODE_CONFIG = path.join(USER_CONFIG_DIR, "opencode", "opencode.json")
|
||||
export const USER_OPENCODE_CONFIG_JSONC = path.join(USER_CONFIG_DIR, "opencode", "opencode.jsonc")
|
||||
|
||||
@@ -17,3 +17,4 @@ export { createAutoUpdateCheckerHook } from "./auto-update-checker";
|
||||
|
||||
export { createAgentUsageReminderHook } from "./agent-usage-reminder";
|
||||
export { createKeywordDetectorHook } from "./keyword-detector";
|
||||
export { createNonInteractiveEnvHook } from "./non-interactive-env";
|
||||
|
||||
@@ -28,7 +28,7 @@ TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
|
||||
## 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
|
||||
3. Always Use Plan agent with gathered context to create detailed work breakdown
|
||||
4. Execute with continuous verification against original requirements
|
||||
|
||||
</ultrawork-mode>
|
||||
|
||||
9
src/hooks/non-interactive-env/constants.ts
Normal file
9
src/hooks/non-interactive-env/constants.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const HOOK_NAME = "non-interactive-env"
|
||||
|
||||
export const NON_INTERACTIVE_ENV: Record<string, string> = {
|
||||
CI: "true",
|
||||
DEBIAN_FRONTEND: "noninteractive",
|
||||
GIT_TERMINAL_PROMPT: "0",
|
||||
GCM_INTERACTIVE: "never",
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1",
|
||||
}
|
||||
34
src/hooks/non-interactive-env/index.ts
Normal file
34
src/hooks/non-interactive-env/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import { HOOK_NAME, NON_INTERACTIVE_ENV } from "./constants"
|
||||
import { log } from "../../shared"
|
||||
|
||||
export * from "./constants"
|
||||
export * from "./types"
|
||||
|
||||
export function createNonInteractiveEnvHook(_ctx: PluginInput) {
|
||||
return {
|
||||
"tool.execute.before": async (
|
||||
input: { tool: string; sessionID: string; callID: string },
|
||||
output: { args: Record<string, unknown> }
|
||||
): Promise<void> => {
|
||||
if (input.tool.toLowerCase() !== "bash") {
|
||||
return
|
||||
}
|
||||
|
||||
const command = output.args.command as string | undefined
|
||||
if (!command) {
|
||||
return
|
||||
}
|
||||
|
||||
output.args.env = {
|
||||
...(output.args.env as Record<string, string> | undefined),
|
||||
...NON_INTERACTIVE_ENV,
|
||||
}
|
||||
|
||||
log(`[${HOOK_NAME}] Set non-interactive environment variables`, {
|
||||
sessionID: input.sessionID,
|
||||
env: NON_INTERACTIVE_ENV,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
3
src/hooks/non-interactive-env/types.ts
Normal file
3
src/hooks/non-interactive-env/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface NonInteractiveEnvConfig {
|
||||
disabled?: boolean
|
||||
}
|
||||
@@ -56,7 +56,7 @@ async function sendNotification(
|
||||
await ctx.$`osascript -e ${"display notification \"" + escapedMessage + "\" with title \"" + escapedTitle + "\""}`
|
||||
break
|
||||
case "linux":
|
||||
await ctx.$`notify-send ${escapedTitle} ${escapedMessage}`
|
||||
await ctx.$`notify-send ${escapedTitle} ${escapedMessage}`.catch(() => {})
|
||||
break
|
||||
case "win32":
|
||||
await ctx.$`powershell -Command ${"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('" + escapedMessage + "', '" + escapedTitle + "')"}`
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
findMessageByIndexNeedingThinking,
|
||||
findMessagesWithOrphanThinking,
|
||||
findMessagesWithThinkingBlocks,
|
||||
findMessagesWithThinkingOnly,
|
||||
injectTextPart,
|
||||
prependThinkingPart,
|
||||
stripThinkingParts,
|
||||
@@ -177,6 +178,8 @@ async function recoverThinkingDisabledViolation(
|
||||
return anySuccess
|
||||
}
|
||||
|
||||
const PLACEHOLDER_TEXT = "[user interrupted]"
|
||||
|
||||
async function recoverEmptyContentMessage(
|
||||
_client: Client,
|
||||
sessionID: string,
|
||||
@@ -187,23 +190,28 @@ async function recoverEmptyContentMessage(
|
||||
const targetIndex = extractMessageIndex(error)
|
||||
const failedID = failedAssistantMsg.info?.id
|
||||
|
||||
const thinkingOnlyIDs = findMessagesWithThinkingOnly(sessionID)
|
||||
for (const messageID of thinkingOnlyIDs) {
|
||||
injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT)
|
||||
}
|
||||
|
||||
if (targetIndex !== null) {
|
||||
const targetMessageID = findEmptyMessageByIndex(sessionID, targetIndex)
|
||||
if (targetMessageID) {
|
||||
return injectTextPart(sessionID, targetMessageID, "(interrupted)")
|
||||
return injectTextPart(sessionID, targetMessageID, PLACEHOLDER_TEXT)
|
||||
}
|
||||
}
|
||||
|
||||
if (failedID) {
|
||||
if (injectTextPart(sessionID, failedID, "(interrupted)")) {
|
||||
if (injectTextPart(sessionID, failedID, PLACEHOLDER_TEXT)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const emptyMessageIDs = findEmptyMessages(sessionID)
|
||||
let anySuccess = false
|
||||
let anySuccess = thinkingOnlyIDs.length > 0
|
||||
for (const messageID of emptyMessageIDs) {
|
||||
if (injectTextPart(sessionID, messageID, "(interrupted)")) {
|
||||
if (injectTextPart(sessionID, messageID, PLACEHOLDER_TEXT)) {
|
||||
anySuccess = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,20 +133,15 @@ export function findEmptyMessages(sessionID: string): string[] {
|
||||
|
||||
export function findEmptyMessageByIndex(sessionID: string, targetIndex: number): string | null {
|
||||
const messages = readMessages(sessionID)
|
||||
|
||||
// Try multiple indices to handle system message offset
|
||||
// API includes system message at index 0, storage may not
|
||||
const indicesToTry = [targetIndex, targetIndex - 1]
|
||||
|
||||
|
||||
// API index may differ from storage index due to system messages
|
||||
const indicesToTry = [targetIndex, targetIndex - 1, targetIndex - 2]
|
||||
|
||||
for (const idx of indicesToTry) {
|
||||
if (idx < 0 || idx >= messages.length) continue
|
||||
|
||||
const targetMsg = messages[idx]
|
||||
|
||||
// NOTE: Do NOT skip last assistant message here
|
||||
// If API returned an error, this message is NOT the final assistant message
|
||||
// (the API only allows empty content for the ACTUAL final assistant message)
|
||||
|
||||
|
||||
if (!messageHasContent(targetMsg.id)) {
|
||||
return targetMsg.id
|
||||
}
|
||||
@@ -177,6 +172,28 @@ export function findMessagesWithThinkingBlocks(sessionID: string): string[] {
|
||||
return result
|
||||
}
|
||||
|
||||
export function findMessagesWithThinkingOnly(sessionID: string): string[] {
|
||||
const messages = readMessages(sessionID)
|
||||
const result: string[] = []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role !== "assistant") continue
|
||||
|
||||
const parts = readParts(msg.id)
|
||||
if (parts.length === 0) continue
|
||||
|
||||
const hasThinking = parts.some((p) => THINKING_TYPES.has(p.type))
|
||||
const hasTextContent = parts.some(hasContent)
|
||||
|
||||
// Has thinking but no text content = orphan thinking
|
||||
if (hasThinking && !hasTextContent) {
|
||||
result.push(msg.id)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function findMessagesWithOrphanThinking(sessionID: string): string[] {
|
||||
const messages = readMessages(sessionID)
|
||||
const result: string[] = []
|
||||
|
||||
72
src/index.ts
72
src/index.ts
@@ -18,6 +18,7 @@ import {
|
||||
createAutoUpdateCheckerHook,
|
||||
createKeywordDetectorHook,
|
||||
createAgentUsageReminderHook,
|
||||
createNonInteractiveEnvHook,
|
||||
} from "./hooks";
|
||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||
import {
|
||||
@@ -65,11 +66,36 @@ function getUserConfigDir(): string {
|
||||
return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
||||
}
|
||||
|
||||
const AGENT_NAME_MAP: Record<string, string> = {
|
||||
omo: "OmO",
|
||||
build: "build",
|
||||
oracle: "oracle",
|
||||
librarian: "librarian",
|
||||
explore: "explore",
|
||||
"frontend-ui-ux-engineer": "frontend-ui-ux-engineer",
|
||||
"document-writer": "document-writer",
|
||||
"multimodal-looker": "multimodal-looker",
|
||||
};
|
||||
|
||||
function normalizeAgentNames(agents: Record<string, unknown>): Record<string, unknown> {
|
||||
const normalized: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(agents)) {
|
||||
const normalizedKey = AGENT_NAME_MAP[key.toLowerCase()] ?? key;
|
||||
normalized[normalizedKey] = value;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function loadConfigFromPath(configPath: string): OhMyOpenCodeConfig | null {
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const content = fs.readFileSync(configPath, "utf-8");
|
||||
const rawConfig = JSON.parse(content);
|
||||
|
||||
if (rawConfig.agents && typeof rawConfig.agents === "object") {
|
||||
rawConfig.agents = normalizeAgentNames(rawConfig.agents);
|
||||
}
|
||||
|
||||
const result = OhMyOpenCodeConfigSchema.safeParse(rawConfig);
|
||||
|
||||
if (!result.success) {
|
||||
@@ -213,6 +239,9 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const agentUsageReminder = isHookEnabled("agent-usage-reminder")
|
||||
? createAgentUsageReminderHook(ctx)
|
||||
: null;
|
||||
const nonInteractiveEnv = isHookEnabled("non-interactive-env")
|
||||
? createNonInteractiveEnvHook(ctx)
|
||||
: null;
|
||||
|
||||
updateTerminalTitle({ sessionId: "main" });
|
||||
|
||||
@@ -254,15 +283,41 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const userAgents = (pluginConfig.claude_code?.agents ?? true) ? loadUserAgents() : {};
|
||||
const projectAgents = (pluginConfig.claude_code?.agents ?? true) ? loadProjectAgents() : {};
|
||||
|
||||
const shouldHideBuild = pluginConfig.omo_agent?.disable_build !== false;
|
||||
const isOmoEnabled = pluginConfig.omo_agent?.disabled !== true;
|
||||
|
||||
config.agent = {
|
||||
...builtinAgents,
|
||||
...userAgents,
|
||||
...projectAgents,
|
||||
...config.agent,
|
||||
...(shouldHideBuild ? { build: { mode: "subagent" } } : {}),
|
||||
};
|
||||
if (isOmoEnabled && builtinAgents.OmO) {
|
||||
// TODO: When OpenCode releases `default_agent` config option (PR #5313),
|
||||
// use `config.default_agent = "OmO"` instead of demoting build/plan.
|
||||
// Tracking: https://github.com/sst/opencode/pull/5313
|
||||
const { name: _planName, ...planConfigWithoutName } = config.agent?.plan ?? {};
|
||||
const omoPlanOverride = pluginConfig.agents?.["OmO-Plan"];
|
||||
const omoPlanBase = {
|
||||
...builtinAgents.OmO,
|
||||
...planConfigWithoutName,
|
||||
description: `${config.agent?.plan?.description ?? "Plan agent"} (OhMyOpenCode version)`,
|
||||
color: config.agent?.plan?.color ?? "#6495ED",
|
||||
};
|
||||
|
||||
const omoPlanConfig = omoPlanOverride ? deepMerge(omoPlanBase, omoPlanOverride) : omoPlanBase;
|
||||
|
||||
config.agent = {
|
||||
OmO: builtinAgents.OmO,
|
||||
"OmO-Plan": omoPlanConfig,
|
||||
...Object.fromEntries(Object.entries(builtinAgents).filter(([k]) => k !== "OmO")),
|
||||
...userAgents,
|
||||
...projectAgents,
|
||||
...config.agent,
|
||||
build: { ...config.agent?.build, mode: "subagent" },
|
||||
plan: { ...config.agent?.plan, mode: "subagent" },
|
||||
};
|
||||
} else {
|
||||
config.agent = {
|
||||
...builtinAgents,
|
||||
...userAgents,
|
||||
...projectAgents,
|
||||
...config.agent,
|
||||
};
|
||||
}
|
||||
|
||||
config.tools = {
|
||||
...config.tools,
|
||||
@@ -428,6 +483,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
|
||||
"tool.execute.before": async (input, output) => {
|
||||
await claudeCodeHooks["tool.execute.before"](input, output);
|
||||
await nonInteractiveEnv?.["tool.execute.before"](input, output);
|
||||
await commentChecker?.["tool.execute.before"](input, output);
|
||||
|
||||
if (input.sessionID === getMainSessionID()) {
|
||||
|
||||
Reference in New Issue
Block a user