Compare commits

..

5 Commits

Author SHA1 Message Date
github-actions[bot]
60d9513d3a release: v1.0.1 2025-12-13 05:06:31 +00:00
YeonGyu-Kim
55bc8f08df refactor(ultrawork-mode): use history injection instead of direct message modification
- Replace direct parts[idx].text modification with injectHookMessage
- Context now injected via filesystem (like UserPromptSubmitHook)
- Preserves original user message without modification

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-13 14:05:17 +09:00
YeonGyu-Kim
0ac4d223f9 feat(think-mode): inject thinking config with maxTokens for extended thinking
- Actually inject THINKING_CONFIGS into message (was defined but unused)
- Add maxTokens: 128000 for Anthropic (required for extended thinking)
- Add maxTokens: 64000 for Amazon Bedrock
- Track thinkingConfigInjected state

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-13 14:05:02 +09:00
YeonGyu-Kim
19b3690499 docs: add Ultrawork Mode hook documentation
🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2025-12-13 14:02:10 +09:00
Junho Yeo
564c8ae8bf fix: use lstatSync instead of statSync for symlink detection (#32) 2025-12-13 13:58:02 +09:00
9 changed files with 62 additions and 24 deletions

View File

@@ -332,6 +332,7 @@ OpenCode 는 아주 확장가능하고 아주 커스터마이저블합니다.
- Use camelCase for function names
```
- **Think Mode**: 확장된 사고(Extended Thinking)가 필요한 상황을 자동으로 감지하고 모드를 전환합니다. 사용자가 깊은 사고를 요청하는 표현(예: "think deeply", "ultrathink")을 감지하면, 추론 능력을 극대화하도록 모델 설정을 동적으로 조정합니다.
- **Ultrawork Mode**: 사용자가 "ultrawork" 또는 "ulw" 키워드를 입력하면 자동으로 에이전트 오케스트레이션 가이드를 주입합니다. 메인 에이전트가 모든 가용한 전문 에이전트(탐색, 사서, 계획, UI)를 백그라운드 작업을 통해 병렬로 최대한 활용하도록 강제하며, 엄격한 TODO 추적 및 검증 프로토콜을 따르게 합니다.
- **Anthropic Auto Compact**: Anthropic 모델 사용 시 컨텍스트 한계에 도달하면 대화 기록을 자동으로 압축하여 효율적으로 관리합니다.
- **Empty Task Response Detector**: 서브 에이전트가 수행한 작업이 비어있거나 무의미한 응답을 반환하는 경우를 감지하여, 오류 없이 우아하게 처리합니다.
- **Grep Output Truncator**: Grep 검색 결과가 너무 길어 컨텍스트를 장악해버리는 것을 방지하기 위해, 과도한 출력을 자동으로 자릅니다.
@@ -344,7 +345,7 @@ OpenCode 는 아주 확장가능하고 아주 커스터마이저블합니다.
}
```
사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`
사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `ultrawork-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`
> **참고**: `disabled_hooks`는 Oh My OpenCode의 내장 훅을 제어합니다. Claude Code의 `settings.json` 훅을 비활성화하려면 `claude_code.hooks: false`를 대신 사용하세요 ([호환성 토글](#호환성-토글) 참고).

View File

@@ -330,6 +330,7 @@ Example workflow:
- Use camelCase for function names
```
- **Think Mode**: Automatic extended thinking detection and mode switching. Detects when user requests deep thinking (e.g., "think deeply", "ultrathink") and dynamically adjusts model settings for enhanced reasoning.
- **Ultrawork Mode**: When user triggers "ultrawork" or "ulw" keywords, automatically injects agent orchestration guidance. Forces the main agent to leverage all available specialized agents (exploration, librarian, planning, UI) via background tasks in parallel, with strict TODO tracking and verification protocols.
- **Anthropic Auto Compact**: Automatically compacts conversation history when approaching context limits for Anthropic models.
- **Empty Task Response Detector**: Detects when subagent tasks return empty or meaningless responses and handles gracefully.
- **Grep Output Truncator**: Prevents grep output from overwhelming the context by truncating excessively long results.
@@ -342,7 +343,7 @@ You can disable specific built-in hooks using `disabled_hooks` in `~/.config/ope
}
```
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`
Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `ultrawork-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`
> **Note**: `disabled_hooks` controls Oh My OpenCode's built-in hooks. To disable Claude Code's `settings.json` hooks, use `claude_code.hooks: false` instead (see [Compatibility Toggles](#compatibility-toggles)).

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode",
"version": "1.0.0",
"version": "1.0.1",
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -1,4 +1,4 @@
import { existsSync, readdirSync, readFileSync, statSync, readlinkSync } from "fs"
import { existsSync, readdirSync, readFileSync, lstatSync, readlinkSync } from "fs"
import { homedir } from "os"
import { join, resolve } from "path"
import { parseFrontmatter } from "../../shared/frontmatter"
@@ -22,8 +22,12 @@ function loadSkillsFromDir(skillsDir: string, scope: SkillScope): LoadedSkillAsC
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
let resolvedPath = skillPath
if (statSync(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
resolvedPath = resolve(skillPath, "..", readlinkSync(skillPath))
try {
if (lstatSync(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
resolvedPath = resolve(skillPath, "..", readlinkSync(skillPath))
}
} catch {
continue
}
const skillMdPath = join(resolvedPath, "SKILL.md")

View File

@@ -1,6 +1,7 @@
import { detectThinkKeyword, extractPromptText } from "./detector"
import { getHighVariant, isAlreadyHighVariant } from "./switcher"
import { getHighVariant, isAlreadyHighVariant, getThinkingConfig } from "./switcher"
import type { ThinkModeState, ThinkModeInput } from "./types"
import { log } from "../../shared"
export * from "./detector"
export * from "./switcher"
@@ -23,6 +24,7 @@ export function createThinkModeHook() {
const state: ThinkModeState = {
requested: false,
modelSwitched: false,
thinkingConfigInjected: false,
}
if (!detectThinkKeyword(promptText)) {
@@ -47,17 +49,31 @@ export function createThinkModeHook() {
}
const highVariant = getHighVariant(currentModel.modelID)
const thinkingConfig = getThinkingConfig(currentModel.providerID, currentModel.modelID)
if (!highVariant) {
thinkModeState.set(sessionID, state)
return
if (highVariant) {
output.message.model = {
providerID: currentModel.providerID,
modelID: highVariant,
}
state.modelSwitched = true
log("Think mode: model switched to high variant", {
sessionID,
from: currentModel.modelID,
to: highVariant,
})
}
output.message.model = {
providerID: currentModel.providerID,
modelID: highVariant,
if (thinkingConfig) {
Object.assign(output.message, thinkingConfig)
state.thinkingConfigInjected = true
log("Think mode: thinking config injected", {
sessionID,
provider: currentModel.providerID,
config: thinkingConfig,
})
}
state.modelSwitched = true
thinkModeState.set(sessionID, state)
},

View File

@@ -55,12 +55,14 @@ export const THINKING_CONFIGS: Record<string, Record<string, unknown>> = {
type: "enabled",
budgetTokens: 64000,
},
maxTokens: 128000,
},
"amazon-bedrock": {
reasoningConfig: {
type: "enabled",
budgetTokens: 32000,
},
maxTokens: 64000,
},
google: {
providerOptions: {

View File

@@ -1,6 +1,7 @@
export interface ThinkModeState {
requested: boolean
modelSwitched: boolean
thinkingConfigInjected: boolean
providerID?: string
modelID?: string
}

View File

@@ -2,6 +2,7 @@ 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"
@@ -16,13 +17,13 @@ export function clearUltraworkModeState(sessionID: string): void {
export function createUltraworkModeHook() {
return {
/**
* chat.message hook - detect ultrawork/ulw keywords, inject context
* 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, prepend ULTRAWORK_CONTEXT to first text part
* 3. If detected, inject ULTRAWORK_CONTEXT via injectHookMessage (history injection)
*/
"chat.message": async (
input: {
@@ -51,13 +52,25 @@ export function createUltraworkModeHook() {
state.detected = true
log("Ultrawork keyword detected", { sessionID: input.sessionID })
const parts = output.parts as Array<{ type: string; text?: string }>
const idx = parts.findIndex((p) => p.type === "text" && p.text)
const message = output.message as {
agent?: string
model?: { modelID?: string; providerID?: string }
path?: { cwd?: string; root?: string }
tools?: Record<string, boolean>
}
if (idx >= 0) {
parts[idx].text = `${ULTRAWORK_CONTEXT}${parts[idx].text ?? ""}`
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", { sessionID: input.sessionID })
log("Ultrawork context injected via history", { sessionID: input.sessionID })
} else {
log("Ultrawork context injection failed", { sessionID: input.sessionID })
}
ultraworkModeState.set(input.sessionID, state)

View File

@@ -1,5 +1,5 @@
import { tool } from "@opencode-ai/plugin"
import { existsSync, readdirSync, statSync, readlinkSync, readFileSync } from "fs"
import { existsSync, readdirSync, lstatSync, readlinkSync, readFileSync } from "fs"
import { homedir } from "os"
import { join, resolve, basename } from "path"
import { z } from "zod/v4"
@@ -39,7 +39,7 @@ function discoverSkillsFromDir(
if (entry.isDirectory() || entry.isSymbolicLink()) {
let resolvedPath = skillPath
try {
const stats = statSync(skillPath, { throwIfNoEntry: false })
const stats = lstatSync(skillPath, { throwIfNoEntry: false })
if (stats?.isSymbolicLink()) {
resolvedPath = resolve(skillPath, "..", readlinkSync(skillPath))
}
@@ -85,7 +85,7 @@ const skillListForDescription = availableSkills
function resolveSymlink(skillPath: string): string {
try {
const stats = statSync(skillPath, { throwIfNoEntry: false })
const stats = lstatSync(skillPath, { throwIfNoEntry: false })
if (stats?.isSymbolicLink()) {
return resolve(skillPath, "..", readlinkSync(skillPath))
}