Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60d9513d3a | ||
|
|
55bc8f08df | ||
|
|
0ac4d223f9 | ||
|
|
19b3690499 | ||
|
|
564c8ae8bf | ||
|
|
03c61bf591 | ||
|
|
f57aa39d53 | ||
|
|
41a318df66 | ||
|
|
e533a35109 | ||
|
|
934d4bcf32 | ||
|
|
91ae0cc67d | ||
|
|
7859f0dd2d | ||
|
|
e131491db4 |
23
README.ko.md
23
README.ko.md
@@ -110,7 +110,7 @@ cat ~/.config/opencode/opencode.json # plugin 배열에 "oh-my-opencode"가 있
|
||||
|
||||
### 4단계: 인증정보 설정
|
||||
|
||||
plugin 에 oh my opencode 를 추가했다면, tmux 를 사용하여 사용자의 인증 설정을 도와주세요.
|
||||
plugin 에 oh my opencode 를 추가했다면, tmux 를 사용하여 사용자의 인증 설정을 도와주세요. **기본적으로 Google Auth (4.2단계) 설정을 권장합니다.**
|
||||
|
||||
#### 4.1 Anthropic (Claude)
|
||||
|
||||
@@ -123,7 +123,9 @@ opencode auth login
|
||||
|
||||
#### 4.2 Google Gemini (내장 Antigravity OAuth)
|
||||
|
||||
먼저 `~/.config/opencode/oh-my-opencode.json` (또는 프로젝트 레벨의 `.opencode/oh-my-opencode.json`)에서 Google auth를 활성화:
|
||||
**권장되는 기본 설정입니다.**
|
||||
|
||||
먼저 주도적으로 `~/.config/opencode/oh-my-opencode.json` (또는 프로젝트 레벨의 `.opencode/oh-my-opencode.json`)에서 Google auth를 활성화하세요:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -170,9 +172,9 @@ opencode auth login
|
||||
|
||||
### ⚠️ 주의
|
||||
|
||||
**사용자가 명시적으로 요청하지 않는 한, `oh-my-opencode.json` 설정 파일을 생성하거나 수정하지 마세요.**
|
||||
**사용자가 명시적으로 요청하지 않는 한, 모델 설정을 변경하거나 기능(에이전트, 훅, MCP)을 비활성화하지 마세요.**
|
||||
|
||||
플러그인은 설정 파일 없이도 기본값으로 완벽하게 동작합니다. 설정 파일은 사용자가 커스터마이징을 원할 때만 필요합니다.
|
||||
플러그인은 기본값으로 완벽하게 동작합니다. 권장 사항인 `google_auth: true` 설정을 제외하고는, 명시적인 요청 없이 다른 설정을 변경하거나 기능을 끄지 마세요.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -330,10 +332,23 @@ 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 검색 결과가 너무 길어 컨텍스트를 장악해버리는 것을 방지하기 위해, 과도한 출력을 자동으로 자릅니다.
|
||||
|
||||
필요 없는 훅이 있다면, `~/.config/opencode/oh-my-opencode.json` 혹은 `.opencode/oh-my-opencode.json`의 `disabled_hooks`를 사용하여 비활성화할 수 있습니다:
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_hooks": ["session-notification", "comment-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`를 대신 사용하세요 ([호환성 토글](#호환성-토글) 참고).
|
||||
|
||||
### Claude Code 호환성
|
||||
|
||||
Oh My OpenCode는 Claude Code 설정과 완벽하게 호환됩니다. Claude Code를 사용하셨다면, 기존 설정을 그대로 사용할 수 있습니다.
|
||||
|
||||
23
README.md
23
README.md
@@ -109,7 +109,7 @@ cat ~/.config/opencode/opencode.json # Should contain "oh-my-opencode" in plugi
|
||||
|
||||
### Step 4: Configure Authentication
|
||||
|
||||
Once `oh-my-opencode` is added to the plugin list, use `tmux` to assist the user with authentication setup.
|
||||
Once `oh-my-opencode` is added to the plugin list, use `tmux` to assist the user with authentication setup. **It is RECOMMENDED to set up Google Auth (Step 4.2) by default.**
|
||||
|
||||
#### 4.1 Anthropic (Claude)
|
||||
|
||||
@@ -122,7 +122,9 @@ opencode auth login
|
||||
|
||||
#### 4.2 Google Gemini (Built-in Antigravity OAuth)
|
||||
|
||||
First, enable Google auth in `~/.config/opencode/oh-my-opencode.json` (or `.opencode/oh-my-opencode.json` for project-level):
|
||||
**This is the RECOMMENDED default configuration.**
|
||||
|
||||
First, proactively enable Google auth in `~/.config/opencode/oh-my-opencode.json` (or `.opencode/oh-my-opencode.json` for project-level):
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -169,9 +171,9 @@ For users who prefer proxy-based authentication, [VibeProxy](https://github.com/
|
||||
|
||||
### ⚠️ Warning
|
||||
|
||||
**Do NOT create or modify the `oh-my-opencode.json` configuration file unless the user explicitly requests it.**
|
||||
**Do NOT modify model settings or disable features (agents, hooks, MCPs) unless the user explicitly requests it.**
|
||||
|
||||
The plugin works perfectly with default settings without any configuration file. The configuration file is only needed when the user wants customization.
|
||||
The plugin works perfectly with default settings. While you SHOULD enable `google_auth: true` as recommended, do not alter other configurations or disable features without a specific request.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -328,10 +330,23 @@ 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.
|
||||
|
||||
You can disable specific built-in hooks using `disabled_hooks` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"disabled_hooks": ["session-notification", "comment-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)).
|
||||
|
||||
### Claude Code Compatibility
|
||||
|
||||
Oh My OpenCode provides seamless Claude Code configuration compatibility. If you've been using Claude Code, your existing setup works out of the box.
|
||||
|
||||
@@ -31,6 +31,29 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"disabled_hooks": {
|
||||
"type": "array",
|
||||
"description": "List of built-in hooks to disable. Useful for selectively disabling hooks that may conflict with your workflow.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"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"
|
||||
]
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oh-my-opencode",
|
||||
"version": "0.4.4",
|
||||
"version": "1.0.1",
|
||||
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -70,6 +70,21 @@ function isRetryableError(status: number): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
const GCP_PERMISSION_ERROR_PATTERNS = [
|
||||
"PERMISSION_DENIED",
|
||||
"does not have permission",
|
||||
"Cloud AI Companion API has not been used",
|
||||
"has not been enabled",
|
||||
] as const
|
||||
|
||||
function isGcpPermissionError(text: string): boolean {
|
||||
return GCP_PERMISSION_ERROR_PATTERNS.some((pattern) => text.includes(pattern))
|
||||
}
|
||||
|
||||
function calculateRetryDelay(attempt: number): number {
|
||||
return Math.min(200 * Math.pow(2, attempt), 2000)
|
||||
}
|
||||
|
||||
async function isRetryableResponse(response: Response): Promise<boolean> {
|
||||
if (isRetryableError(response.status)) return true
|
||||
if (response.status === 403) {
|
||||
@@ -155,23 +170,43 @@ async function attemptFetch(
|
||||
|
||||
debugLog(`[REQ] streaming=${transformed.streaming}, url=${transformed.url}`)
|
||||
|
||||
const response = await fetch(transformed.url, {
|
||||
method: init.method || "POST",
|
||||
headers: transformed.headers,
|
||||
body: JSON.stringify(transformed.body),
|
||||
signal: init.signal,
|
||||
})
|
||||
const maxPermissionRetries = 10
|
||||
for (let attempt = 0; attempt <= maxPermissionRetries; attempt++) {
|
||||
const response = await fetch(transformed.url, {
|
||||
method: init.method || "POST",
|
||||
headers: transformed.headers,
|
||||
body: JSON.stringify(transformed.body),
|
||||
signal: init.signal,
|
||||
})
|
||||
|
||||
debugLog(
|
||||
`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`
|
||||
)
|
||||
debugLog(
|
||||
`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`
|
||||
)
|
||||
|
||||
if (!response.ok && (await isRetryableResponse(response))) {
|
||||
debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`)
|
||||
return null
|
||||
if (response.status === 403) {
|
||||
try {
|
||||
const text = await response.clone().text()
|
||||
if (isGcpPermissionError(text)) {
|
||||
if (attempt < maxPermissionRetries) {
|
||||
const delay = calculateRetryDelay(attempt)
|
||||
debugLog(`[RETRY] GCP permission error, retry ${attempt + 1}/${maxPermissionRetries} after ${delay}ms`)
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
continue
|
||||
}
|
||||
debugLog(`[RETRY] GCP permission error, max retries exceeded`)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!response.ok && (await isRetryableResponse(response))) {
|
||||
debugLog(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`)
|
||||
return null
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
return response
|
||||
return null
|
||||
} catch (error) {
|
||||
debugLog(
|
||||
`Endpoint failed: ${endpoint} (${error instanceof Error ? error.message : "Unknown error"}), trying next`
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
AgentOverridesSchema,
|
||||
McpNameSchema,
|
||||
AgentNameSchema,
|
||||
HookNameSchema,
|
||||
} from "./schema"
|
||||
|
||||
export type {
|
||||
@@ -12,4 +13,5 @@ export type {
|
||||
AgentOverrides,
|
||||
McpName,
|
||||
AgentName,
|
||||
HookName,
|
||||
} from "./schema"
|
||||
|
||||
@@ -24,6 +24,24 @@ export const AgentNameSchema = z.enum([
|
||||
"document-writer",
|
||||
])
|
||||
|
||||
export const HookNameSchema = z.enum([
|
||||
"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",
|
||||
"ultrawork-mode",
|
||||
])
|
||||
|
||||
export const AgentOverrideConfigSchema = z.object({
|
||||
model: z.string().optional(),
|
||||
temperature: z.number().min(0).max(2).optional(),
|
||||
@@ -62,6 +80,7 @@ export const OhMyOpenCodeConfigSchema = z.object({
|
||||
$schema: z.string().optional(),
|
||||
disabled_mcps: z.array(McpNameSchema).optional(),
|
||||
disabled_agents: z.array(AgentNameSchema).optional(),
|
||||
disabled_hooks: z.array(HookNameSchema).optional(),
|
||||
agents: AgentOverridesSchema.optional(),
|
||||
claude_code: ClaudeCodeConfigSchema.optional(),
|
||||
google_auth: z.boolean().optional(),
|
||||
@@ -71,5 +90,6 @@ export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>
|
||||
export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>
|
||||
export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
|
||||
export type AgentName = z.infer<typeof AgentNameSchema>
|
||||
export type HookName = z.infer<typeof HookNameSchema>
|
||||
|
||||
export { McpNameSchema, type McpName } from "../mcp/types"
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
LaunchInput,
|
||||
} from "./types"
|
||||
import { log } from "../../shared/logger"
|
||||
import { getMainSessionID } from "../claude-code-session-state"
|
||||
|
||||
type OpencodeClient = PluginInput["client"]
|
||||
|
||||
@@ -59,6 +58,7 @@ export class BackgroundManager {
|
||||
parentSessionID: input.parentSessionID,
|
||||
parentMessageID: input.parentMessageID,
|
||||
description: input.description,
|
||||
prompt: input.prompt,
|
||||
agent: input.agent,
|
||||
status: "running",
|
||||
startedAt: new Date(),
|
||||
@@ -239,25 +239,19 @@ export class BackgroundManager {
|
||||
|
||||
const message = `[BACKGROUND TASK COMPLETED] Task "${task.description}" finished in ${duration}. Use background_output with task_id="${task.id}" to get results.`
|
||||
|
||||
const mainSessionID = getMainSessionID()
|
||||
if (!mainSessionID) {
|
||||
log("[background-agent] No main session ID available, relying on pending queue")
|
||||
return
|
||||
}
|
||||
|
||||
log("[background-agent] Sending notification to main session:", mainSessionID)
|
||||
log("[background-agent] Sending notification to parent session:", { parentSessionID: task.parentSessionID })
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.client.session.prompt({
|
||||
path: { id: mainSessionID },
|
||||
path: { id: task.parentSessionID },
|
||||
body: {
|
||||
parts: [{ type: "text", text: message }],
|
||||
},
|
||||
query: { directory: this.directory },
|
||||
})
|
||||
this.clearNotificationsForTask(task.id)
|
||||
log("[background-agent] Successfully sent prompt to main session")
|
||||
log("[background-agent] Successfully sent prompt to parent session:", { parentSessionID: task.parentSessionID })
|
||||
} catch (error) {
|
||||
log("[background-agent] prompt failed:", String(error))
|
||||
}
|
||||
@@ -316,7 +310,7 @@ export class BackgroundManager {
|
||||
if (!messagesResult.error && messagesResult.data) {
|
||||
const messages = messagesResult.data as Array<{
|
||||
info?: { role?: string }
|
||||
parts?: Array<{ type?: string; tool?: string; name?: string }>
|
||||
parts?: Array<{ type?: string; tool?: string; name?: string; text?: string }>
|
||||
}>
|
||||
const assistantMsgs = messages.filter(
|
||||
(m) => m.info?.role === "assistant"
|
||||
@@ -324,6 +318,7 @@ export class BackgroundManager {
|
||||
|
||||
let toolCalls = 0
|
||||
let lastTool: string | undefined
|
||||
let lastMessage: string | undefined
|
||||
|
||||
for (const msg of assistantMsgs) {
|
||||
const parts = msg.parts ?? []
|
||||
@@ -332,6 +327,9 @@ export class BackgroundManager {
|
||||
toolCalls++
|
||||
lastTool = part.tool || part.name || "unknown"
|
||||
}
|
||||
if (part.type === "text" && part.text) {
|
||||
lastMessage = part.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +339,10 @@ export class BackgroundManager {
|
||||
task.progress.toolCalls = toolCalls
|
||||
task.progress.lastTool = lastTool
|
||||
task.progress.lastUpdate = new Date()
|
||||
if (lastMessage) {
|
||||
task.progress.lastMessage = lastMessage
|
||||
task.progress.lastMessageAt = new Date()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log("[background-agent] Poll error for task:", { taskId: task.id, error })
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface TaskProgress {
|
||||
toolCalls: number
|
||||
lastTool?: string
|
||||
lastUpdate: Date
|
||||
lastMessage?: string
|
||||
lastMessageAt?: Date
|
||||
}
|
||||
|
||||
export interface BackgroundTask {
|
||||
@@ -16,6 +18,7 @@ export interface BackgroundTask {
|
||||
parentSessionID: string
|
||||
parentMessageID: string
|
||||
description: string
|
||||
prompt: string
|
||||
agent: string
|
||||
status: BackgroundTaskStatus
|
||||
startedAt: Date
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -13,3 +13,4 @@ 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";
|
||||
|
||||
@@ -17,6 +17,8 @@ interface SessionNotificationConfig {
|
||||
idleConfirmationDelay?: number
|
||||
/** Skip notification if there are incomplete todos (default: true) */
|
||||
skipIfIncompleteTodos?: boolean
|
||||
/** Maximum number of sessions to track before cleanup (default: 100) */
|
||||
maxTrackedSessions?: number
|
||||
}
|
||||
|
||||
type Platform = "darwin" | "linux" | "win32" | "unsupported"
|
||||
@@ -103,12 +105,31 @@ export function createSessionNotification(
|
||||
soundPath: defaultSoundPath,
|
||||
idleConfirmationDelay: 1500,
|
||||
skipIfIncompleteTodos: true,
|
||||
maxTrackedSessions: 100,
|
||||
...config,
|
||||
}
|
||||
|
||||
const notifiedSessions = new Set<string>()
|
||||
const pendingTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
const sessionActivitySinceIdle = new Set<string>()
|
||||
// Track notification execution version to handle race conditions
|
||||
const notificationVersions = new Map<string, number>()
|
||||
|
||||
function cleanupOldSessions() {
|
||||
const maxSessions = mergedConfig.maxTrackedSessions
|
||||
if (notifiedSessions.size > maxSessions) {
|
||||
const sessionsToRemove = Array.from(notifiedSessions).slice(0, notifiedSessions.size - maxSessions)
|
||||
sessionsToRemove.forEach(id => notifiedSessions.delete(id))
|
||||
}
|
||||
if (sessionActivitySinceIdle.size > maxSessions) {
|
||||
const sessionsToRemove = Array.from(sessionActivitySinceIdle).slice(0, sessionActivitySinceIdle.size - maxSessions)
|
||||
sessionsToRemove.forEach(id => sessionActivitySinceIdle.delete(id))
|
||||
}
|
||||
if (notificationVersions.size > maxSessions) {
|
||||
const sessionsToRemove = Array.from(notificationVersions.keys()).slice(0, notificationVersions.size - maxSessions)
|
||||
sessionsToRemove.forEach(id => notificationVersions.delete(id))
|
||||
}
|
||||
}
|
||||
|
||||
function cancelPendingNotification(sessionID: string) {
|
||||
const timer = pendingTimers.get(sessionID)
|
||||
@@ -117,6 +138,8 @@ export function createSessionNotification(
|
||||
pendingTimers.delete(sessionID)
|
||||
}
|
||||
sessionActivitySinceIdle.add(sessionID)
|
||||
// Increment version to invalidate any in-flight notifications
|
||||
notificationVersions.set(sessionID, (notificationVersions.get(sessionID) ?? 0) + 1)
|
||||
}
|
||||
|
||||
function markSessionActivity(sessionID: string) {
|
||||
@@ -124,9 +147,14 @@ export function createSessionNotification(
|
||||
notifiedSessions.delete(sessionID)
|
||||
}
|
||||
|
||||
async function executeNotification(sessionID: string) {
|
||||
async function executeNotification(sessionID: string, version: number) {
|
||||
pendingTimers.delete(sessionID)
|
||||
|
||||
// Race condition fix: check if version matches (activity happened during async wait)
|
||||
if (notificationVersions.get(sessionID) !== version) {
|
||||
return
|
||||
}
|
||||
|
||||
if (sessionActivitySinceIdle.has(sessionID)) {
|
||||
sessionActivitySinceIdle.delete(sessionID)
|
||||
return
|
||||
@@ -136,9 +164,17 @@ export function createSessionNotification(
|
||||
|
||||
if (mergedConfig.skipIfIncompleteTodos) {
|
||||
const hasPendingWork = await hasIncompleteTodos(ctx, sessionID)
|
||||
// Re-check version after async call (race condition fix)
|
||||
if (notificationVersions.get(sessionID) !== version) {
|
||||
return
|
||||
}
|
||||
if (hasPendingWork) return
|
||||
}
|
||||
|
||||
if (notificationVersions.get(sessionID) !== version) {
|
||||
return
|
||||
}
|
||||
|
||||
notifiedSessions.add(sessionID)
|
||||
|
||||
try {
|
||||
@@ -172,20 +208,34 @@ export function createSessionNotification(
|
||||
if (pendingTimers.has(sessionID)) return
|
||||
|
||||
sessionActivitySinceIdle.delete(sessionID)
|
||||
|
||||
const currentVersion = (notificationVersions.get(sessionID) ?? 0) + 1
|
||||
notificationVersions.set(sessionID, currentVersion)
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
executeNotification(sessionID)
|
||||
executeNotification(sessionID, currentVersion)
|
||||
}, mergedConfig.idleConfirmationDelay)
|
||||
|
||||
pendingTimers.set(sessionID, timer)
|
||||
cleanupOldSessions()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "message.updated") {
|
||||
if (event.type === "message.updated" || event.type === "message.created") {
|
||||
const info = props?.info as Record<string, unknown> | undefined
|
||||
const sessionID = info?.sessionID as string | undefined
|
||||
if (sessionID) {
|
||||
markSessionActivity(sessionID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "tool.execute.before" || event.type === "tool.execute.after") {
|
||||
const sessionID = props?.sessionID as string | undefined
|
||||
if (sessionID) {
|
||||
markSessionActivity(sessionID)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "session.deleted") {
|
||||
@@ -194,6 +244,7 @@ export function createSessionNotification(
|
||||
cancelPendingNotification(sessionInfo.id)
|
||||
notifiedSessions.delete(sessionInfo.id)
|
||||
sessionActivitySinceIdle.delete(sessionInfo.id)
|
||||
notificationVersions.delete(sessionInfo.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface ThinkModeState {
|
||||
requested: boolean
|
||||
modelSwitched: boolean
|
||||
thinkingConfigInjected: boolean
|
||||
providerID?: string
|
||||
modelID?: string
|
||||
}
|
||||
|
||||
48
src/hooks/ultrawork-mode/constants.ts
Normal file
48
src/hooks/ultrawork-mode/constants.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/** 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>
|
||||
|
||||
---
|
||||
|
||||
`
|
||||
33
src/hooks/ultrawork-mode/detector.ts
Normal file
33
src/hooks/ultrawork-mode/detector.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
ULTRAWORK_PATTERNS,
|
||||
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 {
|
||||
const textWithoutCode = removeCodeBlocks(text)
|
||||
return ULTRAWORK_PATTERNS.some((pattern) => pattern.test(textWithoutCode))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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("")
|
||||
}
|
||||
97
src/hooks/ultrawork-mode/index.ts
Normal file
97
src/hooks/ultrawork-mode/index.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
20
src/hooks/ultrawork-mode/types.ts
Normal file
20
src/hooks/ultrawork-mode/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
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
|
||||
}
|
||||
117
src/index.ts
117
src/index.ts
@@ -4,6 +4,7 @@ import {
|
||||
createTodoContinuationEnforcer,
|
||||
createContextWindowMonitorHook,
|
||||
createSessionRecoveryHook,
|
||||
createSessionNotification,
|
||||
createCommentCheckerHooks,
|
||||
createGrepOutputTruncatorHook,
|
||||
createDirectoryAgentsInjectorHook,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
createRulesInjectorHook,
|
||||
createBackgroundNotificationHook,
|
||||
createAutoUpdateCheckerHook,
|
||||
createUltraworkModeHook,
|
||||
} from "./hooks";
|
||||
import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity";
|
||||
import {
|
||||
@@ -42,7 +44,7 @@ import { updateTerminalTitle } from "./features/terminal";
|
||||
import { builtinTools, createCallOmoAgent, createBackgroundTools } from "./tools";
|
||||
import { BackgroundManager } from "./features/background-agent";
|
||||
import { createBuiltinMcps } from "./mcp";
|
||||
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config";
|
||||
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig, type HookName } from "./config";
|
||||
import { log, deepMerge } from "./shared";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
@@ -103,6 +105,12 @@ function mergeConfigs(
|
||||
...(override.disabled_mcps ?? []),
|
||||
]),
|
||||
],
|
||||
disabled_hooks: [
|
||||
...new Set([
|
||||
...(base.disabled_hooks ?? []),
|
||||
...(override.disabled_hooks ?? []),
|
||||
]),
|
||||
],
|
||||
claude_code: deepMerge(base.claude_code, override.claude_code),
|
||||
};
|
||||
}
|
||||
@@ -135,6 +143,7 @@ function loadPluginConfig(directory: string): OhMyOpenCodeConfig {
|
||||
agents: config.agents,
|
||||
disabled_agents: config.disabled_agents,
|
||||
disabled_mcps: config.disabled_mcps,
|
||||
disabled_hooks: config.disabled_hooks,
|
||||
claude_code: config.claude_code,
|
||||
});
|
||||
return config;
|
||||
@@ -142,34 +151,70 @@ function loadPluginConfig(directory: string): OhMyOpenCodeConfig {
|
||||
|
||||
const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const pluginConfig = loadPluginConfig(ctx.directory);
|
||||
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
||||
const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName);
|
||||
|
||||
const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx);
|
||||
const contextWindowMonitor = createContextWindowMonitorHook(ctx);
|
||||
const sessionRecovery = createSessionRecoveryHook(ctx);
|
||||
const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer")
|
||||
? createTodoContinuationEnforcer(ctx)
|
||||
: null;
|
||||
const contextWindowMonitor = isHookEnabled("context-window-monitor")
|
||||
? createContextWindowMonitorHook(ctx)
|
||||
: null;
|
||||
const sessionRecovery = isHookEnabled("session-recovery")
|
||||
? createSessionRecoveryHook(ctx)
|
||||
: null;
|
||||
const sessionNotification = isHookEnabled("session-notification")
|
||||
? createSessionNotification(ctx)
|
||||
: null;
|
||||
|
||||
// Wire up recovery state tracking between session-recovery and todo-continuation-enforcer
|
||||
// This prevents the continuation enforcer from injecting prompts during active recovery
|
||||
sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering);
|
||||
sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete);
|
||||
if (sessionRecovery && todoContinuationEnforcer) {
|
||||
sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering);
|
||||
sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete);
|
||||
}
|
||||
|
||||
const commentChecker = createCommentCheckerHooks();
|
||||
const grepOutputTruncator = createGrepOutputTruncatorHook(ctx);
|
||||
const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
|
||||
const directoryReadmeInjector = createDirectoryReadmeInjectorHook(ctx);
|
||||
const emptyTaskResponseDetector = createEmptyTaskResponseDetectorHook(ctx);
|
||||
const thinkMode = createThinkModeHook();
|
||||
const commentChecker = isHookEnabled("comment-checker")
|
||||
? createCommentCheckerHooks()
|
||||
: null;
|
||||
const grepOutputTruncator = isHookEnabled("grep-output-truncator")
|
||||
? createGrepOutputTruncatorHook(ctx)
|
||||
: null;
|
||||
const directoryAgentsInjector = isHookEnabled("directory-agents-injector")
|
||||
? createDirectoryAgentsInjectorHook(ctx)
|
||||
: null;
|
||||
const directoryReadmeInjector = isHookEnabled("directory-readme-injector")
|
||||
? createDirectoryReadmeInjectorHook(ctx)
|
||||
: null;
|
||||
const emptyTaskResponseDetector = isHookEnabled("empty-task-response-detector")
|
||||
? createEmptyTaskResponseDetectorHook(ctx)
|
||||
: null;
|
||||
const thinkMode = isHookEnabled("think-mode")
|
||||
? createThinkModeHook()
|
||||
: null;
|
||||
const claudeCodeHooks = createClaudeCodeHooksHook(ctx, {
|
||||
disabledHooks: (pluginConfig.claude_code?.hooks ?? true) ? undefined : true,
|
||||
});
|
||||
const anthropicAutoCompact = createAnthropicAutoCompactHook(ctx);
|
||||
const rulesInjector = createRulesInjectorHook(ctx);
|
||||
const autoUpdateChecker = createAutoUpdateCheckerHook(ctx);
|
||||
const anthropicAutoCompact = isHookEnabled("anthropic-auto-compact")
|
||||
? createAnthropicAutoCompactHook(ctx)
|
||||
: null;
|
||||
const rulesInjector = isHookEnabled("rules-injector")
|
||||
? createRulesInjectorHook(ctx)
|
||||
: null;
|
||||
const autoUpdateChecker = isHookEnabled("auto-update-checker")
|
||||
? createAutoUpdateCheckerHook(ctx)
|
||||
: null;
|
||||
const ultraworkMode = isHookEnabled("ultrawork-mode")
|
||||
? createUltraworkModeHook()
|
||||
: null;
|
||||
|
||||
updateTerminalTitle({ sessionId: "main" });
|
||||
|
||||
const backgroundManager = new BackgroundManager(ctx);
|
||||
|
||||
const backgroundNotificationHook = createBackgroundNotificationHook(backgroundManager);
|
||||
const backgroundNotificationHook = isHookEnabled("background-notification")
|
||||
? createBackgroundNotificationHook(backgroundManager)
|
||||
: null;
|
||||
const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
|
||||
|
||||
const callOmoAgent = createCallOmoAgent(ctx, backgroundManager);
|
||||
@@ -189,6 +234,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
|
||||
"chat.message": async (input, output) => {
|
||||
await claudeCodeHooks["chat.message"]?.(input, output);
|
||||
await ultraworkMode?.["chat.message"]?.(input, output);
|
||||
},
|
||||
|
||||
config: async (config) => {
|
||||
@@ -252,16 +298,18 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
},
|
||||
|
||||
event: async (input) => {
|
||||
await autoUpdateChecker.event(input);
|
||||
await autoUpdateChecker?.event(input);
|
||||
await claudeCodeHooks.event(input);
|
||||
await backgroundNotificationHook.event(input);
|
||||
await todoContinuationEnforcer.handler(input);
|
||||
await contextWindowMonitor.event(input);
|
||||
await directoryAgentsInjector.event(input);
|
||||
await directoryReadmeInjector.event(input);
|
||||
await rulesInjector.event(input);
|
||||
await thinkMode.event(input);
|
||||
await anthropicAutoCompact.event(input);
|
||||
await backgroundNotificationHook?.event(input);
|
||||
await sessionNotification?.(input);
|
||||
await todoContinuationEnforcer?.handler(input);
|
||||
await contextWindowMonitor?.event(input);
|
||||
await directoryAgentsInjector?.event(input);
|
||||
await directoryReadmeInjector?.event(input);
|
||||
await rulesInjector?.event(input);
|
||||
await thinkMode?.event(input);
|
||||
await anthropicAutoCompact?.event(input);
|
||||
await ultraworkMode?.event(input);
|
||||
|
||||
const { event } = input;
|
||||
const props = event.properties as Record<string, unknown> | undefined;
|
||||
@@ -313,7 +361,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
const sessionID = props?.sessionID as string | undefined;
|
||||
const error = props?.error;
|
||||
|
||||
if (sessionRecovery.isRecoverableError(error)) {
|
||||
if (sessionRecovery?.isRecoverableError(error)) {
|
||||
const messageInfo = {
|
||||
id: props?.messageID as string | undefined,
|
||||
role: "assistant" as const,
|
||||
@@ -359,7 +407,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
|
||||
|
||||
"tool.execute.before": async (input, output) => {
|
||||
await claudeCodeHooks["tool.execute.before"](input, output);
|
||||
await commentChecker["tool.execute.before"](input, output);
|
||||
await commentChecker?.["tool.execute.before"](input, output);
|
||||
|
||||
if (input.sessionID === getMainSessionID()) {
|
||||
updateTerminalTitle({
|
||||
@@ -374,13 +422,13 @@ 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 contextWindowMonitor["tool.execute.after"](input, output);
|
||||
await commentChecker["tool.execute.after"](input, output);
|
||||
await directoryAgentsInjector["tool.execute.after"](input, output);
|
||||
await directoryReadmeInjector["tool.execute.after"](input, output);
|
||||
await rulesInjector["tool.execute.after"](input, output);
|
||||
await emptyTaskResponseDetector["tool.execute.after"](input, output);
|
||||
await grepOutputTruncator?.["tool.execute.after"](input, output);
|
||||
await contextWindowMonitor?.["tool.execute.after"](input, output);
|
||||
await commentChecker?.["tool.execute.after"](input, output);
|
||||
await directoryAgentsInjector?.["tool.execute.after"](input, output);
|
||||
await directoryReadmeInjector?.["tool.execute.after"](input, output);
|
||||
await rulesInjector?.["tool.execute.after"](input, output);
|
||||
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
|
||||
|
||||
if (input.sessionID === getMainSessionID()) {
|
||||
updateTerminalTitle({
|
||||
@@ -402,4 +450,5 @@ export type {
|
||||
AgentOverrideConfig,
|
||||
AgentOverrides,
|
||||
McpName,
|
||||
HookName,
|
||||
} from "./config";
|
||||
|
||||
@@ -62,21 +62,51 @@ function delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function truncateText(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text
|
||||
return text.slice(0, maxLength) + "..."
|
||||
}
|
||||
|
||||
function formatTaskStatus(task: BackgroundTask): string {
|
||||
const duration = formatDuration(task.startedAt, task.completedAt)
|
||||
const progress = task.progress
|
||||
? `\nTool calls: ${task.progress.toolCalls}\nLast tool: ${task.progress.lastTool ?? "N/A"}`
|
||||
: ""
|
||||
const promptPreview = truncateText(task.prompt, 500)
|
||||
|
||||
let progressSection = ""
|
||||
if (task.progress) {
|
||||
progressSection = `\nTool calls: ${task.progress.toolCalls}\nLast tool: ${task.progress.lastTool ?? "N/A"}`
|
||||
}
|
||||
|
||||
return `Task Status
|
||||
let lastMessageSection = ""
|
||||
if (task.progress?.lastMessage) {
|
||||
const truncated = truncateText(task.progress.lastMessage, 500)
|
||||
const messageTime = task.progress.lastMessageAt
|
||||
? task.progress.lastMessageAt.toISOString()
|
||||
: "N/A"
|
||||
lastMessageSection = `
|
||||
|
||||
Task ID: ${task.id}
|
||||
Description: ${task.description}
|
||||
Agent: ${task.agent}
|
||||
Status: ${task.status}
|
||||
Duration: ${duration}${progress}
|
||||
## Last Message (${messageTime})
|
||||
|
||||
Session ID: ${task.sessionID}`
|
||||
\`\`\`
|
||||
${truncated}
|
||||
\`\`\``
|
||||
}
|
||||
|
||||
return `# Task Status
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Task ID | \`${task.id}\` |
|
||||
| Description | ${task.description} |
|
||||
| Agent | ${task.agent} |
|
||||
| Status | **${task.status}** |
|
||||
| Duration | ${duration} |
|
||||
| Session ID | \`${task.sessionID}\` |${progressSection}
|
||||
|
||||
## Original Prompt
|
||||
|
||||
\`\`\`
|
||||
${promptPreview}
|
||||
\`\`\`${lastMessageSection}`
|
||||
}
|
||||
|
||||
async function formatTaskResult(task: BackgroundTask, client: OpencodeClient): Promise<string> {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user