oh-my-opencode 0.4.4 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -110,7 +110,7 @@ cat ~/.config/opencode/opencode.json # plugin 배열에 "oh-my-opencode"가 있
110
110
 
111
111
  ### 4단계: 인증정보 설정
112
112
 
113
- plugin 에 oh my opencode 를 추가했다면, tmux 를 사용하여 사용자의 인증 설정을 도와주세요.
113
+ plugin 에 oh my opencode 를 추가했다면, tmux 를 사용하여 사용자의 인증 설정을 도와주세요. **기본적으로 Google Auth (4.2단계) 설정을 권장합니다.**
114
114
 
115
115
  #### 4.1 Anthropic (Claude)
116
116
 
@@ -123,7 +123,9 @@ opencode auth login
123
123
 
124
124
  #### 4.2 Google Gemini (내장 Antigravity OAuth)
125
125
 
126
- 먼저 `~/.config/opencode/oh-my-opencode.json` (또는 프로젝트 레벨의 `.opencode/oh-my-opencode.json`)에서 Google auth를 활성화:
126
+ **권장되는 기본 설정입니다.**
127
+
128
+ 먼저 주도적으로 `~/.config/opencode/oh-my-opencode.json` (또는 프로젝트 레벨의 `.opencode/oh-my-opencode.json`)에서 Google auth를 활성화하세요:
127
129
 
128
130
  ```json
129
131
  {
@@ -170,9 +172,9 @@ opencode auth login
170
172
 
171
173
  ### ⚠️ 주의
172
174
 
173
- **사용자가 명시적으로 요청하지 않는 한, `oh-my-opencode.json` 설정 파일을 생성하거나 수정하지 마세요.**
175
+ **사용자가 명시적으로 요청하지 않는 한, 모델 설정을 변경하거나 기능(에이전트, 훅, MCP)을 비활성화하지 마세요.**
174
176
 
175
- 플러그인은 설정 파일 없이도 기본값으로 완벽하게 동작합니다. 설정 파일은 사용자가 커스터마이징을 원할 때만 필요합니다.
177
+ 플러그인은 기본값으로 완벽하게 동작합니다. 권장 사항인 `google_auth: true` 설정을 제외하고는, 명시적인 요청 없이 다른 설정을 변경하거나 기능을 끄지 마세요.
176
178
 
177
179
  </details>
178
180
 
@@ -330,10 +332,23 @@ OpenCode 는 아주 확장가능하고 아주 커스터마이저블합니다.
330
332
  - Use camelCase for function names
331
333
  ```
332
334
  - **Think Mode**: 확장된 사고(Extended Thinking)가 필요한 상황을 자동으로 감지하고 모드를 전환합니다. 사용자가 깊은 사고를 요청하는 표현(예: "think deeply", "ultrathink")을 감지하면, 추론 능력을 극대화하도록 모델 설정을 동적으로 조정합니다.
335
+ - **Ultrawork Mode**: 사용자가 "ultrawork" 또는 "ulw" 키워드를 입력하면 자동으로 에이전트 오케스트레이션 가이드를 주입합니다. 메인 에이전트가 모든 가용한 전문 에이전트(탐색, 사서, 계획, UI)를 백그라운드 작업을 통해 병렬로 최대한 활용하도록 강제하며, 엄격한 TODO 추적 및 검증 프로토콜을 따르게 합니다.
333
336
  - **Anthropic Auto Compact**: Anthropic 모델 사용 시 컨텍스트 한계에 도달하면 대화 기록을 자동으로 압축하여 효율적으로 관리합니다.
334
337
  - **Empty Task Response Detector**: 서브 에이전트가 수행한 작업이 비어있거나 무의미한 응답을 반환하는 경우를 감지하여, 오류 없이 우아하게 처리합니다.
335
338
  - **Grep Output Truncator**: Grep 검색 결과가 너무 길어 컨텍스트를 장악해버리는 것을 방지하기 위해, 과도한 출력을 자동으로 자릅니다.
336
339
 
340
+ 필요 없는 훅이 있다면, `~/.config/opencode/oh-my-opencode.json` 혹은 `.opencode/oh-my-opencode.json`의 `disabled_hooks`를 사용하여 비활성화할 수 있습니다:
341
+
342
+ ```json
343
+ {
344
+ "disabled_hooks": ["session-notification", "comment-checker"]
345
+ }
346
+ ```
347
+
348
+ 사용 가능한 훅: `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`
349
+
350
+ > **참고**: `disabled_hooks`는 Oh My OpenCode의 내장 훅을 제어합니다. Claude Code의 `settings.json` 훅을 비활성화하려면 `claude_code.hooks: false`를 대신 사용하세요 ([호환성 토글](#호환성-토글) 참고).
351
+
337
352
  ### Claude Code 호환성
338
353
 
339
354
  Oh My OpenCode는 Claude Code 설정과 완벽하게 호환됩니다. Claude Code를 사용하셨다면, 기존 설정을 그대로 사용할 수 있습니다.
package/README.md CHANGED
@@ -109,7 +109,7 @@ cat ~/.config/opencode/opencode.json # Should contain "oh-my-opencode" in plugi
109
109
 
110
110
  ### Step 4: Configure Authentication
111
111
 
112
- Once `oh-my-opencode` is added to the plugin list, use `tmux` to assist the user with authentication setup.
112
+ 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.**
113
113
 
114
114
  #### 4.1 Anthropic (Claude)
115
115
 
@@ -122,7 +122,9 @@ opencode auth login
122
122
 
123
123
  #### 4.2 Google Gemini (Built-in Antigravity OAuth)
124
124
 
125
- First, enable Google auth in `~/.config/opencode/oh-my-opencode.json` (or `.opencode/oh-my-opencode.json` for project-level):
125
+ **This is the RECOMMENDED default configuration.**
126
+
127
+ First, proactively enable Google auth in `~/.config/opencode/oh-my-opencode.json` (or `.opencode/oh-my-opencode.json` for project-level):
126
128
 
127
129
  ```json
128
130
  {
@@ -169,9 +171,9 @@ For users who prefer proxy-based authentication, [VibeProxy](https://github.com/
169
171
 
170
172
  ### ⚠️ Warning
171
173
 
172
- **Do NOT create or modify the `oh-my-opencode.json` configuration file unless the user explicitly requests it.**
174
+ **Do NOT modify model settings or disable features (agents, hooks, MCPs) unless the user explicitly requests it.**
173
175
 
174
- The plugin works perfectly with default settings without any configuration file. The configuration file is only needed when the user wants customization.
176
+ 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.
175
177
 
176
178
  </details>
177
179
 
@@ -328,10 +330,23 @@ Example workflow:
328
330
  - Use camelCase for function names
329
331
  ```
330
332
  - **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.
333
+ - **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.
331
334
  - **Anthropic Auto Compact**: Automatically compacts conversation history when approaching context limits for Anthropic models.
332
335
  - **Empty Task Response Detector**: Detects when subagent tasks return empty or meaningless responses and handles gracefully.
333
336
  - **Grep Output Truncator**: Prevents grep output from overwhelming the context by truncating excessively long results.
334
337
 
338
+ You can disable specific built-in hooks using `disabled_hooks` in `~/.config/opencode/oh-my-opencode.json` or `.opencode/oh-my-opencode.json`:
339
+
340
+ ```json
341
+ {
342
+ "disabled_hooks": ["session-notification", "comment-checker"]
343
+ }
344
+ ```
345
+
346
+ 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`
347
+
348
+ > **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)).
349
+
335
350
  ### Claude Code Compatibility
336
351
 
337
352
  Oh My OpenCode provides seamless Claude Code configuration compatibility. If you've been using Claude Code, your existing setup works out of the box.
@@ -1,2 +1,2 @@
1
- export { OhMyOpenCodeConfigSchema, AgentOverrideConfigSchema, AgentOverridesSchema, McpNameSchema, AgentNameSchema, } from "./schema";
2
- export type { OhMyOpenCodeConfig, AgentOverrideConfig, AgentOverrides, McpName, AgentName, } from "./schema";
1
+ export { OhMyOpenCodeConfigSchema, AgentOverrideConfigSchema, AgentOverridesSchema, McpNameSchema, AgentNameSchema, HookNameSchema, } from "./schema";
2
+ export type { OhMyOpenCodeConfig, AgentOverrideConfig, AgentOverrides, McpName, AgentName, HookName, } from "./schema";
@@ -6,6 +6,23 @@ export declare const AgentNameSchema: z.ZodEnum<{
6
6
  "frontend-ui-ux-engineer": "frontend-ui-ux-engineer";
7
7
  "document-writer": "document-writer";
8
8
  }>;
9
+ export declare const HookNameSchema: z.ZodEnum<{
10
+ "comment-checker": "comment-checker";
11
+ "rules-injector": "rules-injector";
12
+ "todo-continuation-enforcer": "todo-continuation-enforcer";
13
+ "context-window-monitor": "context-window-monitor";
14
+ "session-recovery": "session-recovery";
15
+ "session-notification": "session-notification";
16
+ "grep-output-truncator": "grep-output-truncator";
17
+ "directory-agents-injector": "directory-agents-injector";
18
+ "directory-readme-injector": "directory-readme-injector";
19
+ "empty-task-response-detector": "empty-task-response-detector";
20
+ "think-mode": "think-mode";
21
+ "anthropic-auto-compact": "anthropic-auto-compact";
22
+ "background-notification": "background-notification";
23
+ "auto-update-checker": "auto-update-checker";
24
+ "ultrawork-mode": "ultrawork-mode";
25
+ }>;
9
26
  export declare const AgentOverrideConfigSchema: z.ZodObject<{
10
27
  model: z.ZodOptional<z.ZodString>;
11
28
  temperature: z.ZodOptional<z.ZodNumber>;
@@ -304,6 +321,23 @@ export declare const OhMyOpenCodeConfigSchema: z.ZodObject<{
304
321
  "frontend-ui-ux-engineer": "frontend-ui-ux-engineer";
305
322
  "document-writer": "document-writer";
306
323
  }>>>;
324
+ disabled_hooks: z.ZodOptional<z.ZodArray<z.ZodEnum<{
325
+ "comment-checker": "comment-checker";
326
+ "rules-injector": "rules-injector";
327
+ "todo-continuation-enforcer": "todo-continuation-enforcer";
328
+ "context-window-monitor": "context-window-monitor";
329
+ "session-recovery": "session-recovery";
330
+ "session-notification": "session-notification";
331
+ "grep-output-truncator": "grep-output-truncator";
332
+ "directory-agents-injector": "directory-agents-injector";
333
+ "directory-readme-injector": "directory-readme-injector";
334
+ "empty-task-response-detector": "empty-task-response-detector";
335
+ "think-mode": "think-mode";
336
+ "anthropic-auto-compact": "anthropic-auto-compact";
337
+ "background-notification": "background-notification";
338
+ "auto-update-checker": "auto-update-checker";
339
+ "ultrawork-mode": "ultrawork-mode";
340
+ }>>>;
307
341
  agents: z.ZodOptional<z.ZodObject<{
308
342
  oracle: z.ZodOptional<z.ZodOptional<z.ZodObject<{
309
343
  model: z.ZodOptional<z.ZodString>;
@@ -549,4 +583,5 @@ export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>;
549
583
  export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>;
550
584
  export type AgentOverrides = z.infer<typeof AgentOverridesSchema>;
551
585
  export type AgentName = z.infer<typeof AgentNameSchema>;
586
+ export type HookName = z.infer<typeof HookNameSchema>;
552
587
  export { McpNameSchema, type McpName } from "../mcp/types";
@@ -3,6 +3,8 @@ export interface TaskProgress {
3
3
  toolCalls: number;
4
4
  lastTool?: string;
5
5
  lastUpdate: Date;
6
+ lastMessage?: string;
7
+ lastMessageAt?: Date;
6
8
  }
7
9
  export interface BackgroundTask {
8
10
  id: string;
@@ -10,6 +12,7 @@ export interface BackgroundTask {
10
12
  parentSessionID: string;
11
13
  parentMessageID: string;
12
14
  description: string;
15
+ prompt: string;
13
16
  agent: string;
14
17
  status: BackgroundTaskStatus;
15
18
  startedAt: Date;
@@ -13,3 +13,4 @@ export { createClaudeCodeHooksHook } from "./claude-code-hooks";
13
13
  export { createRulesInjectorHook } from "./rules-injector";
14
14
  export { createBackgroundNotificationHook } from "./background-notification";
15
15
  export { createAutoUpdateCheckerHook } from "./auto-update-checker";
16
+ export { createUltraworkModeHook } from "./ultrawork-mode";
@@ -8,6 +8,8 @@ interface SessionNotificationConfig {
8
8
  idleConfirmationDelay?: number;
9
9
  /** Skip notification if there are incomplete todos (default: true) */
10
10
  skipIfIncompleteTodos?: boolean;
11
+ /** Maximum number of sessions to track before cleanup (default: 100) */
12
+ maxTrackedSessions?: number;
11
13
  }
12
14
  export declare function createSessionNotification(ctx: PluginInput, config?: SessionNotificationConfig): ({ event }: {
13
15
  event: {
@@ -1,6 +1,7 @@
1
1
  export interface ThinkModeState {
2
2
  requested: boolean;
3
3
  modelSwitched: boolean;
4
+ thinkingConfigInjected: boolean;
4
5
  providerID?: string;
5
6
  modelID?: string;
6
7
  }
@@ -0,0 +1,15 @@
1
+ /** Keyword patterns - "ultrawork", "ulw" (case-insensitive, word boundary) */
2
+ export declare const ULTRAWORK_PATTERNS: RegExp[];
3
+ /** Code block pattern to exclude from keyword detection */
4
+ export declare const CODE_BLOCK_PATTERN: RegExp;
5
+ /** Inline code pattern to exclude */
6
+ export declare const INLINE_CODE_PATTERN: RegExp;
7
+ /**
8
+ * ULTRAWORK_CONTEXT - Agent-Agnostic Guidance
9
+ *
10
+ * Key principles:
11
+ * - NO specific agent names (oracle, librarian, etc.)
12
+ * - Only provide guidance based on agent role/capability
13
+ * - Emphasize parallel execution, TODO tracking, delegation
14
+ */
15
+ export declare const ULTRAWORK_CONTEXT = "<ultrawork-mode>\n[CODE RED] Maximum precision required. Ultrathink before acting.\n\nYOU MUST LEVERAGE ALL AVAILABLE AGENTS TO THEIR FULLEST POTENTIAL.\nTELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.\n\n## AGENT UTILIZATION PRINCIPLES (by capability, not by name)\n- **Codebase Exploration**: Spawn exploration agents using BACKGROUND TASKS for file patterns, internal implementations, project structure\n- **Documentation & References**: Use librarian-type agents via BACKGROUND TASKS for API references, examples, external library docs\n- **Planning & Strategy**: NEVER plan yourself - ALWAYS spawn a dedicated planning agent for work breakdown\n- **High-IQ Reasoning**: Leverage specialized agents for architecture decisions, code review, strategic planning\n- **Frontend/UI Tasks**: Delegate to UI-specialized agents for design and implementation\n\n## EXECUTION RULES\n- **TODO**: Track EVERY step. Mark complete IMMEDIATELY after each.\n- **PARALLEL**: Fire independent agent calls simultaneously via background_task - NEVER wait sequentially.\n- **BACKGROUND FIRST**: Use background_task for exploration/research agents (10+ concurrent if needed).\n- **VERIFY**: Re-read request after completion. Check ALL requirements met before reporting done.\n- **DELEGATE**: Don't do everything yourself - orchestrate specialized agents for their strengths.\n\n## WORKFLOW\n1. Analyze the request and identify required capabilities\n2. Spawn exploration/librarian agents via background_task in PARALLEL (10+ if needed)\n3. Use planning agents to create detailed work breakdown\n4. Execute with continuous verification against original requirements\n\n</ultrawork-mode>\n\n---\n\n";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Remove code blocks and inline code from text.
3
+ * Prevents false positives when keywords appear in code.
4
+ */
5
+ export declare function removeCodeBlocks(text: string): string;
6
+ /**
7
+ * Detect ultrawork keywords in text (excluding code blocks).
8
+ */
9
+ export declare function detectUltraworkKeyword(text: string): boolean;
10
+ /**
11
+ * Extract text content from message parts.
12
+ */
13
+ export declare function extractPromptText(parts: Array<{
14
+ type: string;
15
+ text?: string;
16
+ }>): string;
@@ -0,0 +1,40 @@
1
+ export * from "./detector";
2
+ export * from "./constants";
3
+ export * from "./types";
4
+ export declare function clearUltraworkModeState(sessionID: string): void;
5
+ export declare function createUltraworkModeHook(): {
6
+ /**
7
+ * chat.message hook - detect ultrawork/ulw keywords, inject context via history
8
+ *
9
+ * Execution timing: AFTER claudeCodeHooks["chat.message"]
10
+ * Behavior:
11
+ * 1. Extract text from user prompt
12
+ * 2. Detect ultrawork/ulw keywords (excluding code blocks)
13
+ * 3. If detected, inject ULTRAWORK_CONTEXT via injectHookMessage (history injection)
14
+ */
15
+ "chat.message": (input: {
16
+ sessionID: string;
17
+ agent?: string;
18
+ model?: {
19
+ providerID: string;
20
+ modelID: string;
21
+ };
22
+ messageID?: string;
23
+ }, output: {
24
+ message: Record<string, unknown>;
25
+ parts: Array<{
26
+ type: string;
27
+ text?: string;
28
+ [key: string]: unknown;
29
+ }>;
30
+ }) => Promise<void>;
31
+ /**
32
+ * event hook - cleanup session state on deletion
33
+ */
34
+ event: ({ event, }: {
35
+ event: {
36
+ type: string;
37
+ properties?: unknown;
38
+ };
39
+ }) => Promise<void>;
40
+ };
@@ -0,0 +1,20 @@
1
+ export interface UltraworkModeState {
2
+ /** Whether ultrawork keyword was detected */
3
+ detected: boolean;
4
+ /** Whether context was injected */
5
+ injected: boolean;
6
+ }
7
+ export interface ModelRef {
8
+ providerID: string;
9
+ modelID: string;
10
+ }
11
+ export interface MessageWithModel {
12
+ model?: ModelRef;
13
+ }
14
+ export interface UltraworkModeInput {
15
+ parts: Array<{
16
+ type: string;
17
+ text?: string;
18
+ }>;
19
+ message: MessageWithModel;
20
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
2
  declare const OhMyOpenCodePlugin: Plugin;
3
3
  export default OhMyOpenCodePlugin;
4
- export type { OhMyOpenCodeConfig, AgentName, AgentOverrideConfig, AgentOverrides, McpName, } from "./config";
4
+ export type { OhMyOpenCodeConfig, AgentName, AgentOverrideConfig, AgentOverrides, McpName, HookName, } from "./config";
package/dist/index.js CHANGED
@@ -184,8 +184,8 @@ var require_utils = __commonJS((exports) => {
184
184
  exports.toPosixSlashes = (str) => str.replace(REGEX_BACKSLASH, "/");
185
185
  exports.isWindows = () => {
186
186
  if (typeof navigator !== "undefined" && navigator.platform) {
187
- const platform = navigator.platform.toLowerCase();
188
- return platform === "win32" || platform === "windows";
187
+ const platform2 = navigator.platform.toLowerCase();
188
+ return platform2 === "win32" || platform2 === "windows";
189
189
  }
190
190
  if (typeof process !== "undefined" && process.platform) {
191
191
  return process.platform === "win32";
@@ -2934,6 +2934,198 @@ ${CONTEXT_REMINDER}
2934
2934
  event: eventHandler
2935
2935
  };
2936
2936
  }
2937
+ // src/hooks/session-notification.ts
2938
+ import { platform } from "os";
2939
+ function detectPlatform() {
2940
+ const p = platform();
2941
+ if (p === "darwin" || p === "linux" || p === "win32")
2942
+ return p;
2943
+ return "unsupported";
2944
+ }
2945
+ function getDefaultSoundPath(p) {
2946
+ switch (p) {
2947
+ case "darwin":
2948
+ return "/System/Library/Sounds/Glass.aiff";
2949
+ case "linux":
2950
+ return "/usr/share/sounds/freedesktop/stereo/complete.oga";
2951
+ case "win32":
2952
+ return "C:\\Windows\\Media\\notify.wav";
2953
+ default:
2954
+ return "";
2955
+ }
2956
+ }
2957
+ async function sendNotification(ctx, p, title, message) {
2958
+ const escapedTitle = title.replace(/"/g, "\\\"").replace(/'/g, "\\'");
2959
+ const escapedMessage = message.replace(/"/g, "\\\"").replace(/'/g, "\\'");
2960
+ switch (p) {
2961
+ case "darwin":
2962
+ await ctx.$`osascript -e ${'display notification "' + escapedMessage + '" with title "' + escapedTitle + '"'}`;
2963
+ break;
2964
+ case "linux":
2965
+ await ctx.$`notify-send ${escapedTitle} ${escapedMessage}`;
2966
+ break;
2967
+ case "win32":
2968
+ await ctx.$`powershell -Command ${"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('" + escapedMessage + "', '" + escapedTitle + "')"}`;
2969
+ break;
2970
+ }
2971
+ }
2972
+ async function playSound(ctx, p, soundPath) {
2973
+ switch (p) {
2974
+ case "darwin":
2975
+ ctx.$`afplay ${soundPath}`.catch(() => {});
2976
+ break;
2977
+ case "linux":
2978
+ ctx.$`paplay ${soundPath}`.catch(() => {
2979
+ ctx.$`aplay ${soundPath}`.catch(() => {});
2980
+ });
2981
+ break;
2982
+ case "win32":
2983
+ ctx.$`powershell -Command ${"(New-Object Media.SoundPlayer '" + soundPath + "').PlaySync()"}`.catch(() => {});
2984
+ break;
2985
+ }
2986
+ }
2987
+ async function hasIncompleteTodos(ctx, sessionID) {
2988
+ try {
2989
+ const response = await ctx.client.session.todo({ path: { id: sessionID } });
2990
+ const todos = response.data ?? response;
2991
+ if (!todos || todos.length === 0)
2992
+ return false;
2993
+ return todos.some((t) => t.status !== "completed" && t.status !== "cancelled");
2994
+ } catch {
2995
+ return false;
2996
+ }
2997
+ }
2998
+ function createSessionNotification(ctx, config = {}) {
2999
+ const currentPlatform = detectPlatform();
3000
+ const defaultSoundPath = getDefaultSoundPath(currentPlatform);
3001
+ const mergedConfig = {
3002
+ title: "OpenCode",
3003
+ message: "Agent is ready for input",
3004
+ playSound: false,
3005
+ soundPath: defaultSoundPath,
3006
+ idleConfirmationDelay: 1500,
3007
+ skipIfIncompleteTodos: true,
3008
+ maxTrackedSessions: 100,
3009
+ ...config
3010
+ };
3011
+ const notifiedSessions = new Set;
3012
+ const pendingTimers = new Map;
3013
+ const sessionActivitySinceIdle = new Set;
3014
+ const notificationVersions = new Map;
3015
+ function cleanupOldSessions() {
3016
+ const maxSessions = mergedConfig.maxTrackedSessions;
3017
+ if (notifiedSessions.size > maxSessions) {
3018
+ const sessionsToRemove = Array.from(notifiedSessions).slice(0, notifiedSessions.size - maxSessions);
3019
+ sessionsToRemove.forEach((id) => notifiedSessions.delete(id));
3020
+ }
3021
+ if (sessionActivitySinceIdle.size > maxSessions) {
3022
+ const sessionsToRemove = Array.from(sessionActivitySinceIdle).slice(0, sessionActivitySinceIdle.size - maxSessions);
3023
+ sessionsToRemove.forEach((id) => sessionActivitySinceIdle.delete(id));
3024
+ }
3025
+ if (notificationVersions.size > maxSessions) {
3026
+ const sessionsToRemove = Array.from(notificationVersions.keys()).slice(0, notificationVersions.size - maxSessions);
3027
+ sessionsToRemove.forEach((id) => notificationVersions.delete(id));
3028
+ }
3029
+ }
3030
+ function cancelPendingNotification(sessionID) {
3031
+ const timer = pendingTimers.get(sessionID);
3032
+ if (timer) {
3033
+ clearTimeout(timer);
3034
+ pendingTimers.delete(sessionID);
3035
+ }
3036
+ sessionActivitySinceIdle.add(sessionID);
3037
+ notificationVersions.set(sessionID, (notificationVersions.get(sessionID) ?? 0) + 1);
3038
+ }
3039
+ function markSessionActivity(sessionID) {
3040
+ cancelPendingNotification(sessionID);
3041
+ notifiedSessions.delete(sessionID);
3042
+ }
3043
+ async function executeNotification(sessionID, version) {
3044
+ pendingTimers.delete(sessionID);
3045
+ if (notificationVersions.get(sessionID) !== version) {
3046
+ return;
3047
+ }
3048
+ if (sessionActivitySinceIdle.has(sessionID)) {
3049
+ sessionActivitySinceIdle.delete(sessionID);
3050
+ return;
3051
+ }
3052
+ if (notifiedSessions.has(sessionID))
3053
+ return;
3054
+ if (mergedConfig.skipIfIncompleteTodos) {
3055
+ const hasPendingWork = await hasIncompleteTodos(ctx, sessionID);
3056
+ if (notificationVersions.get(sessionID) !== version) {
3057
+ return;
3058
+ }
3059
+ if (hasPendingWork)
3060
+ return;
3061
+ }
3062
+ if (notificationVersions.get(sessionID) !== version) {
3063
+ return;
3064
+ }
3065
+ notifiedSessions.add(sessionID);
3066
+ try {
3067
+ await sendNotification(ctx, currentPlatform, mergedConfig.title, mergedConfig.message);
3068
+ if (mergedConfig.playSound && mergedConfig.soundPath) {
3069
+ await playSound(ctx, currentPlatform, mergedConfig.soundPath);
3070
+ }
3071
+ } catch {}
3072
+ }
3073
+ return async ({ event }) => {
3074
+ if (currentPlatform === "unsupported")
3075
+ return;
3076
+ const props = event.properties;
3077
+ if (event.type === "session.updated" || event.type === "session.created") {
3078
+ const info = props?.info;
3079
+ const sessionID = info?.id;
3080
+ if (sessionID) {
3081
+ markSessionActivity(sessionID);
3082
+ }
3083
+ return;
3084
+ }
3085
+ if (event.type === "session.idle") {
3086
+ const sessionID = props?.sessionID;
3087
+ if (!sessionID)
3088
+ return;
3089
+ if (notifiedSessions.has(sessionID))
3090
+ return;
3091
+ if (pendingTimers.has(sessionID))
3092
+ return;
3093
+ sessionActivitySinceIdle.delete(sessionID);
3094
+ const currentVersion = (notificationVersions.get(sessionID) ?? 0) + 1;
3095
+ notificationVersions.set(sessionID, currentVersion);
3096
+ const timer = setTimeout(() => {
3097
+ executeNotification(sessionID, currentVersion);
3098
+ }, mergedConfig.idleConfirmationDelay);
3099
+ pendingTimers.set(sessionID, timer);
3100
+ cleanupOldSessions();
3101
+ return;
3102
+ }
3103
+ if (event.type === "message.updated" || event.type === "message.created") {
3104
+ const info = props?.info;
3105
+ const sessionID = info?.sessionID;
3106
+ if (sessionID) {
3107
+ markSessionActivity(sessionID);
3108
+ }
3109
+ return;
3110
+ }
3111
+ if (event.type === "tool.execute.before" || event.type === "tool.execute.after") {
3112
+ const sessionID = props?.sessionID;
3113
+ if (sessionID) {
3114
+ markSessionActivity(sessionID);
3115
+ }
3116
+ return;
3117
+ }
3118
+ if (event.type === "session.deleted") {
3119
+ const sessionInfo = props?.info;
3120
+ if (sessionInfo?.id) {
3121
+ cancelPendingNotification(sessionInfo.id);
3122
+ notifiedSessions.delete(sessionInfo.id);
3123
+ sessionActivitySinceIdle.delete(sessionInfo.id);
3124
+ notificationVersions.delete(sessionInfo.id);
3125
+ }
3126
+ }
3127
+ };
3128
+ }
2937
3129
  // src/hooks/session-recovery/storage.ts
2938
3130
  import { existsSync as existsSync3, mkdirSync, readdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
2939
3131
  import { join as join4 } from "path";
@@ -4625,6 +4817,46 @@ var ALREADY_HIGH = new Set([
4625
4817
  "gpt-5.2-chat-latest-high",
4626
4818
  "gpt-5.2-pro-high"
4627
4819
  ]);
4820
+ var THINKING_CONFIGS = {
4821
+ anthropic: {
4822
+ thinking: {
4823
+ type: "enabled",
4824
+ budgetTokens: 64000
4825
+ },
4826
+ maxTokens: 128000
4827
+ },
4828
+ "amazon-bedrock": {
4829
+ reasoningConfig: {
4830
+ type: "enabled",
4831
+ budgetTokens: 32000
4832
+ },
4833
+ maxTokens: 64000
4834
+ },
4835
+ google: {
4836
+ providerOptions: {
4837
+ google: {
4838
+ thinkingConfig: {
4839
+ thinkingLevel: "HIGH"
4840
+ }
4841
+ }
4842
+ }
4843
+ },
4844
+ "google-vertex": {
4845
+ providerOptions: {
4846
+ "google-vertex": {
4847
+ thinkingConfig: {
4848
+ thinkingLevel: "HIGH"
4849
+ }
4850
+ }
4851
+ }
4852
+ }
4853
+ };
4854
+ var THINKING_CAPABLE_MODELS = {
4855
+ anthropic: ["claude-sonnet-4", "claude-opus-4", "claude-3"],
4856
+ "amazon-bedrock": ["claude", "anthropic"],
4857
+ google: ["gemini-2", "gemini-3"],
4858
+ "google-vertex": ["gemini-2", "gemini-3"]
4859
+ };
4628
4860
  function getHighVariant(modelID) {
4629
4861
  if (ALREADY_HIGH.has(modelID)) {
4630
4862
  return null;
@@ -4634,6 +4866,19 @@ function getHighVariant(modelID) {
4634
4866
  function isAlreadyHighVariant(modelID) {
4635
4867
  return ALREADY_HIGH.has(modelID) || modelID.endsWith("-high");
4636
4868
  }
4869
+ function getThinkingConfig(providerID, modelID) {
4870
+ if (isAlreadyHighVariant(modelID)) {
4871
+ return null;
4872
+ }
4873
+ const config = THINKING_CONFIGS[providerID];
4874
+ const capablePatterns = THINKING_CAPABLE_MODELS[providerID];
4875
+ if (!config || !capablePatterns) {
4876
+ return null;
4877
+ }
4878
+ const modelLower = modelID.toLowerCase();
4879
+ const isCapable = capablePatterns.some((pattern) => modelLower.includes(pattern.toLowerCase()));
4880
+ return isCapable ? config : null;
4881
+ }
4637
4882
 
4638
4883
  // src/hooks/think-mode/index.ts
4639
4884
  var thinkModeState = new Map;
@@ -4643,7 +4888,8 @@ function createThinkModeHook() {
4643
4888
  const promptText = extractPromptText(output.parts);
4644
4889
  const state = {
4645
4890
  requested: false,
4646
- modelSwitched: false
4891
+ modelSwitched: false,
4892
+ thinkingConfigInjected: false
4647
4893
  };
4648
4894
  if (!detectThinkKeyword(promptText)) {
4649
4895
  thinkModeState.set(sessionID, state);
@@ -4662,15 +4908,28 @@ function createThinkModeHook() {
4662
4908
  return;
4663
4909
  }
4664
4910
  const highVariant = getHighVariant(currentModel.modelID);
4665
- if (!highVariant) {
4666
- thinkModeState.set(sessionID, state);
4667
- return;
4911
+ const thinkingConfig = getThinkingConfig(currentModel.providerID, currentModel.modelID);
4912
+ if (highVariant) {
4913
+ output.message.model = {
4914
+ providerID: currentModel.providerID,
4915
+ modelID: highVariant
4916
+ };
4917
+ state.modelSwitched = true;
4918
+ log("Think mode: model switched to high variant", {
4919
+ sessionID,
4920
+ from: currentModel.modelID,
4921
+ to: highVariant
4922
+ });
4923
+ }
4924
+ if (thinkingConfig) {
4925
+ Object.assign(output.message, thinkingConfig);
4926
+ state.thinkingConfigInjected = true;
4927
+ log("Think mode: thinking config injected", {
4928
+ sessionID,
4929
+ provider: currentModel.providerID,
4930
+ config: thinkingConfig
4931
+ });
4668
4932
  }
4669
- output.message.model = {
4670
- providerID: currentModel.providerID,
4671
- modelID: highVariant
4672
- };
4673
- state.modelSwitched = true;
4674
4933
  thinkModeState.set(sessionID, state);
4675
4934
  },
4676
4935
  event: async ({ event }) => {
@@ -6330,6 +6589,97 @@ function createAutoUpdateCheckerHook(ctx) {
6330
6589
  }
6331
6590
  };
6332
6591
  }
6592
+ // src/hooks/ultrawork-mode/constants.ts
6593
+ var ULTRAWORK_PATTERNS = [/\bultrawork\b/i, /\bulw\b/i];
6594
+ var CODE_BLOCK_PATTERN2 = /```[\s\S]*?```/g;
6595
+ var INLINE_CODE_PATTERN2 = /`[^`]+`/g;
6596
+ var ULTRAWORK_CONTEXT = `<ultrawork-mode>
6597
+ [CODE RED] Maximum precision required. Ultrathink before acting.
6598
+
6599
+ YOU MUST LEVERAGE ALL AVAILABLE AGENTS TO THEIR FULLEST POTENTIAL.
6600
+ TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
6601
+
6602
+ ## AGENT UTILIZATION PRINCIPLES (by capability, not by name)
6603
+ - **Codebase Exploration**: Spawn exploration agents using BACKGROUND TASKS for file patterns, internal implementations, project structure
6604
+ - **Documentation & References**: Use librarian-type agents via BACKGROUND TASKS for API references, examples, external library docs
6605
+ - **Planning & Strategy**: NEVER plan yourself - ALWAYS spawn a dedicated planning agent for work breakdown
6606
+ - **High-IQ Reasoning**: Leverage specialized agents for architecture decisions, code review, strategic planning
6607
+ - **Frontend/UI Tasks**: Delegate to UI-specialized agents for design and implementation
6608
+
6609
+ ## EXECUTION RULES
6610
+ - **TODO**: Track EVERY step. Mark complete IMMEDIATELY after each.
6611
+ - **PARALLEL**: Fire independent agent calls simultaneously via background_task - NEVER wait sequentially.
6612
+ - **BACKGROUND FIRST**: Use background_task for exploration/research agents (10+ concurrent if needed).
6613
+ - **VERIFY**: Re-read request after completion. Check ALL requirements met before reporting done.
6614
+ - **DELEGATE**: Don't do everything yourself - orchestrate specialized agents for their strengths.
6615
+
6616
+ ## WORKFLOW
6617
+ 1. Analyze the request and identify required capabilities
6618
+ 2. Spawn exploration/librarian agents via background_task in PARALLEL (10+ if needed)
6619
+ 3. Use planning agents to create detailed work breakdown
6620
+ 4. Execute with continuous verification against original requirements
6621
+
6622
+ </ultrawork-mode>
6623
+
6624
+ ---
6625
+
6626
+ `;
6627
+
6628
+ // src/hooks/ultrawork-mode/detector.ts
6629
+ function removeCodeBlocks2(text) {
6630
+ return text.replace(CODE_BLOCK_PATTERN2, "").replace(INLINE_CODE_PATTERN2, "");
6631
+ }
6632
+ function detectUltraworkKeyword(text) {
6633
+ const textWithoutCode = removeCodeBlocks2(text);
6634
+ return ULTRAWORK_PATTERNS.some((pattern) => pattern.test(textWithoutCode));
6635
+ }
6636
+ function extractPromptText2(parts) {
6637
+ return parts.filter((p) => p.type === "text").map((p) => p.text || "").join("");
6638
+ }
6639
+
6640
+ // src/hooks/ultrawork-mode/index.ts
6641
+ var ultraworkModeState = new Map;
6642
+ function createUltraworkModeHook() {
6643
+ return {
6644
+ "chat.message": async (input, output) => {
6645
+ const state = {
6646
+ detected: false,
6647
+ injected: false
6648
+ };
6649
+ const promptText = extractPromptText2(output.parts);
6650
+ if (!detectUltraworkKeyword(promptText)) {
6651
+ ultraworkModeState.set(input.sessionID, state);
6652
+ return;
6653
+ }
6654
+ state.detected = true;
6655
+ log("Ultrawork keyword detected", { sessionID: input.sessionID });
6656
+ const message = output.message;
6657
+ const success = injectHookMessage(input.sessionID, ULTRAWORK_CONTEXT, {
6658
+ agent: message.agent,
6659
+ model: message.model,
6660
+ path: message.path,
6661
+ tools: message.tools
6662
+ });
6663
+ if (success) {
6664
+ state.injected = true;
6665
+ log("Ultrawork context injected via history", { sessionID: input.sessionID });
6666
+ } else {
6667
+ log("Ultrawork context injection failed", { sessionID: input.sessionID });
6668
+ }
6669
+ ultraworkModeState.set(input.sessionID, state);
6670
+ },
6671
+ event: async ({
6672
+ event
6673
+ }) => {
6674
+ if (event.type === "session.deleted") {
6675
+ const props = event.properties;
6676
+ if (props?.info?.id) {
6677
+ ultraworkModeState.delete(props.info.id);
6678
+ }
6679
+ }
6680
+ }
6681
+ };
6682
+ }
6333
6683
  // src/auth/antigravity/constants.ts
6334
6684
  var ANTIGRAVITY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
6335
6685
  var ANTIGRAVITY_CLIENT_SECRET = "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";
@@ -7434,6 +7784,18 @@ function isRetryableError(status) {
7434
7784
  return true;
7435
7785
  return false;
7436
7786
  }
7787
+ var GCP_PERMISSION_ERROR_PATTERNS = [
7788
+ "PERMISSION_DENIED",
7789
+ "does not have permission",
7790
+ "Cloud AI Companion API has not been used",
7791
+ "has not been enabled"
7792
+ ];
7793
+ function isGcpPermissionError(text) {
7794
+ return GCP_PERMISSION_ERROR_PATTERNS.some((pattern) => text.includes(pattern));
7795
+ }
7796
+ function calculateRetryDelay2(attempt) {
7797
+ return Math.min(200 * Math.pow(2, attempt), 2000);
7798
+ }
7437
7799
  async function isRetryableResponse(response) {
7438
7800
  if (isRetryableError(response.status))
7439
7801
  return true;
@@ -7496,18 +7858,36 @@ async function attemptFetch(options) {
7496
7858
  thoughtSignature
7497
7859
  });
7498
7860
  debugLog6(`[REQ] streaming=${transformed.streaming}, url=${transformed.url}`);
7499
- const response = await fetch(transformed.url, {
7500
- method: init.method || "POST",
7501
- headers: transformed.headers,
7502
- body: JSON.stringify(transformed.body),
7503
- signal: init.signal
7504
- });
7505
- debugLog6(`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`);
7506
- if (!response.ok && await isRetryableResponse(response)) {
7507
- debugLog6(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`);
7508
- return null;
7861
+ const maxPermissionRetries = 10;
7862
+ for (let attempt = 0;attempt <= maxPermissionRetries; attempt++) {
7863
+ const response = await fetch(transformed.url, {
7864
+ method: init.method || "POST",
7865
+ headers: transformed.headers,
7866
+ body: JSON.stringify(transformed.body),
7867
+ signal: init.signal
7868
+ });
7869
+ debugLog6(`[RESP] status=${response.status} content-type=${response.headers.get("content-type") ?? ""} url=${response.url}`);
7870
+ if (response.status === 403) {
7871
+ try {
7872
+ const text = await response.clone().text();
7873
+ if (isGcpPermissionError(text)) {
7874
+ if (attempt < maxPermissionRetries) {
7875
+ const delay = calculateRetryDelay2(attempt);
7876
+ debugLog6(`[RETRY] GCP permission error, retry ${attempt + 1}/${maxPermissionRetries} after ${delay}ms`);
7877
+ await new Promise((resolve4) => setTimeout(resolve4, delay));
7878
+ continue;
7879
+ }
7880
+ debugLog6(`[RETRY] GCP permission error, max retries exceeded`);
7881
+ }
7882
+ } catch {}
7883
+ }
7884
+ if (!response.ok && await isRetryableResponse(response)) {
7885
+ debugLog6(`Endpoint failed: ${endpoint} (status: ${response.status}), trying next`);
7886
+ return null;
7887
+ }
7888
+ return response;
7509
7889
  }
7510
- return response;
7890
+ return null;
7511
7891
  } catch (error) {
7512
7892
  debugLog6(`Endpoint failed: ${endpoint} (${error instanceof Error ? error.message : "Unknown error"}), trying next`);
7513
7893
  return null;
@@ -7883,7 +8263,7 @@ function loadOpencodeProjectCommands() {
7883
8263
  return commandsToRecord(commands);
7884
8264
  }
7885
8265
  // src/features/claude-code-skill-loader/loader.ts
7886
- import { existsSync as existsSync20, readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync3, readlinkSync } from "fs";
8266
+ import { existsSync as existsSync20, readdirSync as readdirSync5, readFileSync as readFileSync12, lstatSync, readlinkSync } from "fs";
7887
8267
  import { homedir as homedir10 } from "os";
7888
8268
  import { join as join25, resolve as resolve4 } from "path";
7889
8269
  function loadSkillsFromDir(skillsDir, scope) {
@@ -7899,8 +8279,12 @@ function loadSkillsFromDir(skillsDir, scope) {
7899
8279
  if (!entry.isDirectory() && !entry.isSymbolicLink())
7900
8280
  continue;
7901
8281
  let resolvedPath = skillPath;
7902
- if (statSync3(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
7903
- resolvedPath = resolve4(skillPath, "..", readlinkSync(skillPath));
8282
+ try {
8283
+ if (lstatSync(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
8284
+ resolvedPath = resolve4(skillPath, "..", readlinkSync(skillPath));
8285
+ }
8286
+ } catch {
8287
+ continue;
7904
8288
  }
7905
8289
  const skillMdPath = join25(resolvedPath, "SKILL.md");
7906
8290
  if (!existsSync20(skillMdPath))
@@ -19435,10 +19819,10 @@ function _property(property, schema, params) {
19435
19819
  ...normalizeParams(params)
19436
19820
  });
19437
19821
  }
19438
- function _mime(types9, params) {
19822
+ function _mime(types10, params) {
19439
19823
  return new $ZodCheckMimeType({
19440
19824
  check: "mime_type",
19441
- mime: types9,
19825
+ mime: types10,
19442
19826
  ...normalizeParams(params)
19443
19827
  });
19444
19828
  }
@@ -21348,7 +21732,7 @@ var ZodFile = /* @__PURE__ */ $constructor("ZodFile", (inst, def) => {
21348
21732
  ZodType.init(inst, def);
21349
21733
  inst.min = (size, params) => inst.check(_minSize(size, params));
21350
21734
  inst.max = (size, params) => inst.check(_maxSize(size, params));
21351
- inst.mime = (types9, params) => inst.check(_mime(Array.isArray(types9) ? types9 : [types9], params));
21735
+ inst.mime = (types10, params) => inst.check(_mime(Array.isArray(types10) ? types10 : [types10], params));
21352
21736
  });
21353
21737
  function file(params) {
21354
21738
  return _file(ZodFile, params);
@@ -22001,7 +22385,7 @@ var lsp_code_action_resolve = tool({
22001
22385
  // src/tools/ast-grep/constants.ts
22002
22386
  import { createRequire as createRequire4 } from "module";
22003
22387
  import { dirname as dirname5, join as join30 } from "path";
22004
- import { existsSync as existsSync26, statSync as statSync4 } from "fs";
22388
+ import { existsSync as existsSync26, statSync as statSync3 } from "fs";
22005
22389
 
22006
22390
  // src/tools/ast-grep/downloader.ts
22007
22391
  var {spawn: spawn5 } = globalThis.Bun;
@@ -22115,13 +22499,13 @@ async function ensureAstGrepBinary() {
22115
22499
  // src/tools/ast-grep/constants.ts
22116
22500
  function isValidBinary(filePath) {
22117
22501
  try {
22118
- return statSync4(filePath).size > 1e4;
22502
+ return statSync3(filePath).size > 1e4;
22119
22503
  } catch {
22120
22504
  return false;
22121
22505
  }
22122
22506
  }
22123
22507
  function getPlatformPackageName() {
22124
- const platform = process.platform;
22508
+ const platform2 = process.platform;
22125
22509
  const arch = process.arch;
22126
22510
  const platformMap = {
22127
22511
  "darwin-arm64": "@ast-grep/cli-darwin-arm64",
@@ -22132,7 +22516,7 @@ function getPlatformPackageName() {
22132
22516
  "win32-arm64": "@ast-grep/cli-win32-arm64-msvc",
22133
22517
  "win32-ia32": "@ast-grep/cli-win32-ia32-msvc"
22134
22518
  };
22135
- return platformMap[`${platform}-${arch}`] ?? null;
22519
+ return platformMap[`${platform2}-${arch}`] ?? null;
22136
22520
  }
22137
22521
  function findSgCliPathSync() {
22138
22522
  const binaryName = process.platform === "win32" ? "sg.exe" : "sg";
@@ -23135,7 +23519,7 @@ var SkillFrontmatterSchema = exports_external.object({
23135
23519
  metadata: exports_external.record(exports_external.string(), exports_external.string()).optional()
23136
23520
  });
23137
23521
  // src/tools/skill/tools.ts
23138
- import { existsSync as existsSync30, readdirSync as readdirSync8, statSync as statSync5, readlinkSync as readlinkSync2, readFileSync as readFileSync18 } from "fs";
23522
+ import { existsSync as existsSync30, readdirSync as readdirSync8, lstatSync as lstatSync2, readlinkSync as readlinkSync2, readFileSync as readFileSync18 } from "fs";
23139
23523
  import { homedir as homedir16 } from "os";
23140
23524
  import { join as join33, resolve as resolve7, basename as basename4 } from "path";
23141
23525
  function parseSkillFrontmatter(data) {
@@ -23160,7 +23544,7 @@ function discoverSkillsFromDir(skillsDir, scope) {
23160
23544
  if (entry.isDirectory() || entry.isSymbolicLink()) {
23161
23545
  let resolvedPath = skillPath;
23162
23546
  try {
23163
- const stats = statSync5(skillPath, { throwIfNoEntry: false });
23547
+ const stats = lstatSync2(skillPath, { throwIfNoEntry: false });
23164
23548
  if (stats?.isSymbolicLink()) {
23165
23549
  resolvedPath = resolve7(skillPath, "..", readlinkSync2(skillPath));
23166
23550
  }
@@ -23197,7 +23581,7 @@ var skillListForDescription = availableSkills.map((s) => `- ${s.name}: ${s.descr
23197
23581
  `);
23198
23582
  function resolveSymlink(skillPath) {
23199
23583
  try {
23200
- const stats = statSync5(skillPath, { throwIfNoEntry: false });
23584
+ const stats = lstatSync2(skillPath, { throwIfNoEntry: false });
23201
23585
  if (stats?.isSymbolicLink()) {
23202
23586
  return resolve7(skillPath, "..", readlinkSync2(skillPath));
23203
23587
  }
@@ -23479,20 +23863,48 @@ Use \`background_output\` tool with task_id="${task.id}" to check progress:
23479
23863
  function delay(ms) {
23480
23864
  return new Promise((resolve8) => setTimeout(resolve8, ms));
23481
23865
  }
23866
+ function truncateText(text, maxLength) {
23867
+ if (text.length <= maxLength)
23868
+ return text;
23869
+ return text.slice(0, maxLength) + "...";
23870
+ }
23482
23871
  function formatTaskStatus(task) {
23483
23872
  const duration3 = formatDuration(task.startedAt, task.completedAt);
23484
- const progress = task.progress ? `
23873
+ const promptPreview = truncateText(task.prompt, 500);
23874
+ let progressSection = "";
23875
+ if (task.progress) {
23876
+ progressSection = `
23485
23877
  Tool calls: ${task.progress.toolCalls}
23486
- Last tool: ${task.progress.lastTool ?? "N/A"}` : "";
23487
- return `Task Status
23878
+ Last tool: ${task.progress.lastTool ?? "N/A"}`;
23879
+ }
23880
+ let lastMessageSection = "";
23881
+ if (task.progress?.lastMessage) {
23882
+ const truncated = truncateText(task.progress.lastMessage, 500);
23883
+ const messageTime = task.progress.lastMessageAt ? task.progress.lastMessageAt.toISOString() : "N/A";
23884
+ lastMessageSection = `
23488
23885
 
23489
- Task ID: ${task.id}
23490
- Description: ${task.description}
23491
- Agent: ${task.agent}
23492
- Status: ${task.status}
23493
- Duration: ${duration3}${progress}
23886
+ ## Last Message (${messageTime})
23887
+
23888
+ \`\`\`
23889
+ ${truncated}
23890
+ \`\`\``;
23891
+ }
23892
+ return `# Task Status
23893
+
23894
+ | Field | Value |
23895
+ |-------|-------|
23896
+ | Task ID | \`${task.id}\` |
23897
+ | Description | ${task.description} |
23898
+ | Agent | ${task.agent} |
23899
+ | Status | **${task.status}** |
23900
+ | Duration | ${duration3} |
23901
+ | Session ID | \`${task.sessionID}\` |${progressSection}
23494
23902
 
23495
- Session ID: ${task.sessionID}`;
23903
+ ## Original Prompt
23904
+
23905
+ \`\`\`
23906
+ ${promptPreview}
23907
+ \`\`\`${lastMessageSection}`;
23496
23908
  }
23497
23909
  async function formatTaskResult(task, client2) {
23498
23910
  const messagesResult = await client2.session.messages({
@@ -23834,6 +24246,7 @@ class BackgroundManager {
23834
24246
  parentSessionID: input.parentSessionID,
23835
24247
  parentMessageID: input.parentMessageID,
23836
24248
  description: input.description,
24249
+ prompt: input.prompt,
23837
24250
  agent: input.agent,
23838
24251
  status: "running",
23839
24252
  startedAt: new Date,
@@ -23991,23 +24404,18 @@ class BackgroundManager {
23991
24404
  }).catch(() => {});
23992
24405
  }
23993
24406
  const message = `[BACKGROUND TASK COMPLETED] Task "${task.description}" finished in ${duration3}. Use background_output with task_id="${task.id}" to get results.`;
23994
- const mainSessionID2 = getMainSessionID();
23995
- if (!mainSessionID2) {
23996
- log("[background-agent] No main session ID available, relying on pending queue");
23997
- return;
23998
- }
23999
- log("[background-agent] Sending notification to main session:", mainSessionID2);
24407
+ log("[background-agent] Sending notification to parent session:", { parentSessionID: task.parentSessionID });
24000
24408
  setTimeout(async () => {
24001
24409
  try {
24002
24410
  await this.client.session.prompt({
24003
- path: { id: mainSessionID2 },
24411
+ path: { id: task.parentSessionID },
24004
24412
  body: {
24005
24413
  parts: [{ type: "text", text: message }]
24006
24414
  },
24007
24415
  query: { directory: this.directory }
24008
24416
  });
24009
24417
  this.clearNotificationsForTask(task.id);
24010
- log("[background-agent] Successfully sent prompt to main session");
24418
+ log("[background-agent] Successfully sent prompt to parent session:", { parentSessionID: task.parentSessionID });
24011
24419
  } catch (error45) {
24012
24420
  log("[background-agent] prompt failed:", String(error45));
24013
24421
  }
@@ -24060,6 +24468,7 @@ class BackgroundManager {
24060
24468
  const assistantMsgs = messages.filter((m) => m.info?.role === "assistant");
24061
24469
  let toolCalls = 0;
24062
24470
  let lastTool;
24471
+ let lastMessage;
24063
24472
  for (const msg of assistantMsgs) {
24064
24473
  const parts = msg.parts ?? [];
24065
24474
  for (const part of parts) {
@@ -24067,6 +24476,9 @@ class BackgroundManager {
24067
24476
  toolCalls++;
24068
24477
  lastTool = part.tool || part.name || "unknown";
24069
24478
  }
24479
+ if (part.type === "text" && part.text) {
24480
+ lastMessage = part.text;
24481
+ }
24070
24482
  }
24071
24483
  }
24072
24484
  if (!task.progress) {
@@ -24075,6 +24487,10 @@ class BackgroundManager {
24075
24487
  task.progress.toolCalls = toolCalls;
24076
24488
  task.progress.lastTool = lastTool;
24077
24489
  task.progress.lastUpdate = new Date;
24490
+ if (lastMessage) {
24491
+ task.progress.lastMessage = lastMessage;
24492
+ task.progress.lastMessageAt = new Date;
24493
+ }
24078
24494
  }
24079
24495
  } catch (error45) {
24080
24496
  log("[background-agent] Poll error for task:", { taskId: task.id, error: error45 });
@@ -24137,6 +24553,23 @@ var AgentNameSchema = exports_external.enum([
24137
24553
  "frontend-ui-ux-engineer",
24138
24554
  "document-writer"
24139
24555
  ]);
24556
+ var HookNameSchema = exports_external.enum([
24557
+ "todo-continuation-enforcer",
24558
+ "context-window-monitor",
24559
+ "session-recovery",
24560
+ "session-notification",
24561
+ "comment-checker",
24562
+ "grep-output-truncator",
24563
+ "directory-agents-injector",
24564
+ "directory-readme-injector",
24565
+ "empty-task-response-detector",
24566
+ "think-mode",
24567
+ "anthropic-auto-compact",
24568
+ "rules-injector",
24569
+ "background-notification",
24570
+ "auto-update-checker",
24571
+ "ultrawork-mode"
24572
+ ]);
24140
24573
  var AgentOverrideConfigSchema = exports_external.object({
24141
24574
  model: exports_external.string().optional(),
24142
24575
  temperature: exports_external.number().min(0).max(2).optional(),
@@ -24167,6 +24600,7 @@ var OhMyOpenCodeConfigSchema = exports_external.object({
24167
24600
  $schema: exports_external.string().optional(),
24168
24601
  disabled_mcps: exports_external.array(McpNameSchema).optional(),
24169
24602
  disabled_agents: exports_external.array(AgentNameSchema).optional(),
24603
+ disabled_hooks: exports_external.array(HookNameSchema).optional(),
24170
24604
  agents: AgentOverridesSchema.optional(),
24171
24605
  claude_code: ClaudeCodeConfigSchema.optional(),
24172
24606
  google_auth: exports_external.boolean().optional()
@@ -24216,6 +24650,12 @@ function mergeConfigs(base, override) {
24216
24650
  ...override.disabled_mcps ?? []
24217
24651
  ])
24218
24652
  ],
24653
+ disabled_hooks: [
24654
+ ...new Set([
24655
+ ...base.disabled_hooks ?? [],
24656
+ ...override.disabled_hooks ?? []
24657
+ ])
24658
+ ],
24219
24659
  claude_code: deepMerge(base.claude_code, override.claude_code)
24220
24660
  };
24221
24661
  }
@@ -24231,32 +24671,39 @@ function loadPluginConfig(directory) {
24231
24671
  agents: config3.agents,
24232
24672
  disabled_agents: config3.disabled_agents,
24233
24673
  disabled_mcps: config3.disabled_mcps,
24674
+ disabled_hooks: config3.disabled_hooks,
24234
24675
  claude_code: config3.claude_code
24235
24676
  });
24236
24677
  return config3;
24237
24678
  }
24238
24679
  var OhMyOpenCodePlugin = async (ctx) => {
24239
24680
  const pluginConfig = loadPluginConfig(ctx.directory);
24240
- const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx);
24241
- const contextWindowMonitor = createContextWindowMonitorHook(ctx);
24242
- const sessionRecovery = createSessionRecoveryHook(ctx);
24243
- sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering);
24244
- sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete);
24245
- const commentChecker = createCommentCheckerHooks();
24246
- const grepOutputTruncator = createGrepOutputTruncatorHook(ctx);
24247
- const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
24248
- const directoryReadmeInjector = createDirectoryReadmeInjectorHook(ctx);
24249
- const emptyTaskResponseDetector = createEmptyTaskResponseDetectorHook(ctx);
24250
- const thinkMode = createThinkModeHook();
24681
+ const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
24682
+ const isHookEnabled = (hookName) => !disabledHooks.has(hookName);
24683
+ const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer") ? createTodoContinuationEnforcer(ctx) : null;
24684
+ const contextWindowMonitor = isHookEnabled("context-window-monitor") ? createContextWindowMonitorHook(ctx) : null;
24685
+ const sessionRecovery = isHookEnabled("session-recovery") ? createSessionRecoveryHook(ctx) : null;
24686
+ const sessionNotification = isHookEnabled("session-notification") ? createSessionNotification(ctx) : null;
24687
+ if (sessionRecovery && todoContinuationEnforcer) {
24688
+ sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering);
24689
+ sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete);
24690
+ }
24691
+ const commentChecker = isHookEnabled("comment-checker") ? createCommentCheckerHooks() : null;
24692
+ const grepOutputTruncator = isHookEnabled("grep-output-truncator") ? createGrepOutputTruncatorHook(ctx) : null;
24693
+ const directoryAgentsInjector = isHookEnabled("directory-agents-injector") ? createDirectoryAgentsInjectorHook(ctx) : null;
24694
+ const directoryReadmeInjector = isHookEnabled("directory-readme-injector") ? createDirectoryReadmeInjectorHook(ctx) : null;
24695
+ const emptyTaskResponseDetector = isHookEnabled("empty-task-response-detector") ? createEmptyTaskResponseDetectorHook(ctx) : null;
24696
+ const thinkMode = isHookEnabled("think-mode") ? createThinkModeHook() : null;
24251
24697
  const claudeCodeHooks = createClaudeCodeHooksHook(ctx, {
24252
24698
  disabledHooks: pluginConfig.claude_code?.hooks ?? true ? undefined : true
24253
24699
  });
24254
- const anthropicAutoCompact = createAnthropicAutoCompactHook(ctx);
24255
- const rulesInjector = createRulesInjectorHook(ctx);
24256
- const autoUpdateChecker = createAutoUpdateCheckerHook(ctx);
24700
+ const anthropicAutoCompact = isHookEnabled("anthropic-auto-compact") ? createAnthropicAutoCompactHook(ctx) : null;
24701
+ const rulesInjector = isHookEnabled("rules-injector") ? createRulesInjectorHook(ctx) : null;
24702
+ const autoUpdateChecker = isHookEnabled("auto-update-checker") ? createAutoUpdateCheckerHook(ctx) : null;
24703
+ const ultraworkMode = isHookEnabled("ultrawork-mode") ? createUltraworkModeHook() : null;
24257
24704
  updateTerminalTitle({ sessionId: "main" });
24258
24705
  const backgroundManager = new BackgroundManager(ctx);
24259
- const backgroundNotificationHook = createBackgroundNotificationHook(backgroundManager);
24706
+ const backgroundNotificationHook = isHookEnabled("background-notification") ? createBackgroundNotificationHook(backgroundManager) : null;
24260
24707
  const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
24261
24708
  const callOmoAgent = createCallOmoAgent(ctx, backgroundManager);
24262
24709
  const googleAuthHooks = pluginConfig.google_auth ? await createGoogleAntigravityAuthPlugin(ctx) : null;
@@ -24269,6 +24716,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
24269
24716
  },
24270
24717
  "chat.message": async (input, output) => {
24271
24718
  await claudeCodeHooks["chat.message"]?.(input, output);
24719
+ await ultraworkMode?.["chat.message"]?.(input, output);
24272
24720
  },
24273
24721
  config: async (config3) => {
24274
24722
  const builtinAgents = createBuiltinAgents(pluginConfig.disabled_agents, pluginConfig.agents);
@@ -24319,16 +24767,18 @@ var OhMyOpenCodePlugin = async (ctx) => {
24319
24767
  };
24320
24768
  },
24321
24769
  event: async (input) => {
24322
- await autoUpdateChecker.event(input);
24770
+ await autoUpdateChecker?.event(input);
24323
24771
  await claudeCodeHooks.event(input);
24324
- await backgroundNotificationHook.event(input);
24325
- await todoContinuationEnforcer.handler(input);
24326
- await contextWindowMonitor.event(input);
24327
- await directoryAgentsInjector.event(input);
24328
- await directoryReadmeInjector.event(input);
24329
- await rulesInjector.event(input);
24330
- await thinkMode.event(input);
24331
- await anthropicAutoCompact.event(input);
24772
+ await backgroundNotificationHook?.event(input);
24773
+ await sessionNotification?.(input);
24774
+ await todoContinuationEnforcer?.handler(input);
24775
+ await contextWindowMonitor?.event(input);
24776
+ await directoryAgentsInjector?.event(input);
24777
+ await directoryReadmeInjector?.event(input);
24778
+ await rulesInjector?.event(input);
24779
+ await thinkMode?.event(input);
24780
+ await anthropicAutoCompact?.event(input);
24781
+ await ultraworkMode?.event(input);
24332
24782
  const { event } = input;
24333
24783
  const props = event.properties;
24334
24784
  if (event.type === "session.created") {
@@ -24370,7 +24820,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
24370
24820
  if (event.type === "session.error") {
24371
24821
  const sessionID = props?.sessionID;
24372
24822
  const error45 = props?.error;
24373
- if (sessionRecovery.isRecoverableError(error45)) {
24823
+ if (sessionRecovery?.isRecoverableError(error45)) {
24374
24824
  const messageInfo = {
24375
24825
  id: props?.messageID,
24376
24826
  role: "assistant",
@@ -24409,7 +24859,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
24409
24859
  },
24410
24860
  "tool.execute.before": async (input, output) => {
24411
24861
  await claudeCodeHooks["tool.execute.before"](input, output);
24412
- await commentChecker["tool.execute.before"](input, output);
24862
+ await commentChecker?.["tool.execute.before"](input, output);
24413
24863
  if (input.sessionID === getMainSessionID()) {
24414
24864
  updateTerminalTitle({
24415
24865
  sessionId: input.sessionID,
@@ -24422,13 +24872,13 @@ var OhMyOpenCodePlugin = async (ctx) => {
24422
24872
  },
24423
24873
  "tool.execute.after": async (input, output) => {
24424
24874
  await claudeCodeHooks["tool.execute.after"](input, output);
24425
- await grepOutputTruncator["tool.execute.after"](input, output);
24426
- await contextWindowMonitor["tool.execute.after"](input, output);
24427
- await commentChecker["tool.execute.after"](input, output);
24428
- await directoryAgentsInjector["tool.execute.after"](input, output);
24429
- await directoryReadmeInjector["tool.execute.after"](input, output);
24430
- await rulesInjector["tool.execute.after"](input, output);
24431
- await emptyTaskResponseDetector["tool.execute.after"](input, output);
24875
+ await grepOutputTruncator?.["tool.execute.after"](input, output);
24876
+ await contextWindowMonitor?.["tool.execute.after"](input, output);
24877
+ await commentChecker?.["tool.execute.after"](input, output);
24878
+ await directoryAgentsInjector?.["tool.execute.after"](input, output);
24879
+ await directoryReadmeInjector?.["tool.execute.after"](input, output);
24880
+ await rulesInjector?.["tool.execute.after"](input, output);
24881
+ await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
24432
24882
  if (input.sessionID === getMainSessionID()) {
24433
24883
  updateTerminalTitle({
24434
24884
  sessionId: input.sessionID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "0.4.4",
3
+ "version": "1.0.1",
4
4
  "description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",