diff --git a/README.ko.md b/README.ko.md index f3b93ac..83a14bc 100644 --- a/README.ko.md +++ b/README.ko.md @@ -608,7 +608,7 @@ OmO를 비활성화하고 원래 build/plan 에이전트를 복원하려면: } ``` -사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder` +사용 가능한 훅: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env` ### MCPs diff --git a/README.md b/README.md index 4bcac10..1a459d9 100644 --- a/README.md +++ b/README.md @@ -609,7 +609,7 @@ Disable specific built-in hooks via `disabled_hooks` in `~/.config/opencode/oh-m } ``` -Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder` +Available hooks: `todo-continuation-enforcer`, `context-window-monitor`, `session-recovery`, `session-notification`, `comment-checker`, `grep-output-truncator`, `tool-output-truncator`, `directory-agents-injector`, `directory-readme-injector`, `empty-task-response-detector`, `think-mode`, `anthropic-auto-compact`, `rules-injector`, `background-notification`, `auto-update-checker`, `startup-toast`, `keyword-detector`, `agent-usage-reminder`, `non-interactive-env` ### MCPs diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index bc5d0d1..dd54cd5 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -36,7 +36,6 @@ }, "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": [ @@ -58,214 +57,1153 @@ "startup-toast", "keyword-detector", "agent-usage-reminder", - "interactive-bash-blocker" + "non-interactive-env" ] } }, "agents": { "type": "object", - "propertyNames": { - "type": "string", - "enum": [ - "build", - "plan", - "OmO", - "OmO-Plan", - "oracle", - "librarian", - "explore", - "frontend-ui-ux-engineer", - "document-writer", - "multimodal-looker" - ] - }, - "additionalProperties": { - "type": "object", - "properties": { - "model": { - "type": "string" - }, - "temperature": { - "type": "number", - "minimum": 0, - "maximum": 2 - }, - "top_p": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "prompt": { - "type": "string" - }, - "tools": { - "type": "object", - "propertyNames": { + "properties": { + "build": { + "type": "object", + "properties": { + "model": { "type": "string" }, - "additionalProperties": { - "type": "boolean" - } - }, - "disable": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": [ - "subagent", - "primary", - "all" - ] - }, - "color": { - "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" - }, - "permission": { - "type": "object", - "properties": { - "edit": { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" }, - "bash": { - "anyOf": [ - { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] - }, - { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { "type": "string", "enum": [ "ask", "allow", "deny" ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } } - } - ] + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + } + } + }, + "plan": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" }, - "webfetch": { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + } + } + }, + "OmO": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" }, - "doom_loop": { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + } + } + }, + "OmO-Plan": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" }, - "external_directory": { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + } + } + }, + "oracle": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + } + } + }, + "librarian": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + } + } + }, + "explore": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + } + } + }, + "frontend-ui-ux-engineer": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + } + } + }, + "document-writer": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + } + } + }, + "multimodal-looker": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "temperature": { + "type": "number", + "minimum": 0, + "maximum": 2 + }, + "top_p": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "prompt": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "subagent", + "primary", + "all" + ] + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "permission": { + "type": "object", + "properties": { + "edit": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "bash": { + "anyOf": [ + { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } + } + ] + }, + "webfetch": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "doom_loop": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + }, + "external_directory": { + "type": "string", + "enum": [ + "ask", + "allow", + "deny" + ] + } } } } } } }, - "google_auth": { - "type": "boolean", - "description": "Enable built-in Antigravity OAuth for Google Gemini models. When true, adds 'OAuth with Google (Antigravity)' login option.", - "default": false - }, - "lsp": { - "type": "object", - "description": "Additional LSP server configurations specific to Oh My OpenCode.", - "additionalProperties": { - "type": "object", - "properties": { - "command": { - "type": "array", - "items": { "type": "string" }, - "description": "Command and arguments to start the LSP server" - }, - "extensions": { - "type": "array", - "items": { "type": "string" }, - "description": "File extensions this server handles (e.g., [\".ts\", \".tsx\"])" - }, - "priority": { - "type": "number", - "description": "Server priority (higher = preferred)" - }, - "env": { - "type": "object", - "additionalProperties": { "type": "string" }, - "description": "Environment variables for the LSP server" - }, - "initialization": { - "type": "object", - "description": "Custom initialization options" - }, - "disabled": { - "type": "boolean", - "description": "Disable this LSP server" - } - } - } - }, "claude_code": { "type": "object", - "description": "Toggle Claude Code compatibility features on/off. All default to true (enabled).", "properties": { "mcp": { - "type": "boolean", - "description": "Load MCP servers from ~/.claude/.mcp.json, ./.mcp.json, ./.claude/.mcp.json" + "type": "boolean" }, "commands": { - "type": "boolean", - "description": "Load commands from ~/.claude/commands/*.md, ./.claude/commands/*.md" + "type": "boolean" }, "skills": { - "type": "boolean", - "description": "Load skills from ~/.claude/skills/*/SKILL.md, ./.claude/skills/*/SKILL.md" + "type": "boolean" }, "agents": { - "type": "boolean", - "description": "Load agents from ~/.claude/agents/*.md, ./.claude/agents/*.md" + "type": "boolean" }, "hooks": { - "type": "boolean", - "description": "Execute hooks from ~/.claude/settings.json, ./.claude/settings.json, ./.claude/settings.local.json" + "type": "boolean" } } }, + "google_auth": { + "type": "boolean" + }, "omo_agent": { "type": "object", - "description": "Configuration for the default OmO agent behavior.", "properties": { "disabled": { - "type": "boolean", - "description": "When true, disables OmO agents and restores original build/plan as primary.", - "default": false + "type": "boolean" } } } diff --git a/src/config/schema.ts b/src/config/schema.ts index 334d15b..be44e43 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -60,7 +60,7 @@ export const HookNameSchema = z.enum([ "startup-toast", "keyword-detector", "agent-usage-reminder", - "interactive-bash-blocker", + "non-interactive-env", ]) export const AgentOverrideConfigSchema = z.object({ diff --git a/src/hooks/index.ts b/src/hooks/index.ts index a463533..0753ce9 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -17,4 +17,4 @@ export { createAutoUpdateCheckerHook } from "./auto-update-checker"; export { createAgentUsageReminderHook } from "./agent-usage-reminder"; export { createKeywordDetectorHook } from "./keyword-detector"; -export { createInteractiveBashBlockerHook } from "./interactive-bash-blocker"; +export { createNonInteractiveEnvHook } from "./non-interactive-env"; diff --git a/src/hooks/interactive-bash-blocker/index.ts b/src/hooks/interactive-bash-blocker/index.ts deleted file mode 100644 index 974975a..0000000 --- a/src/hooks/interactive-bash-blocker/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { PluginInput } from "@opencode-ai/plugin" -import { HOOK_NAME, NON_INTERACTIVE_ENV, ALWAYS_BLOCK_PATTERNS, TMUX_SUGGESTION } from "./constants" -import type { BlockResult } from "./types" -import { log } from "../../shared" - -export * from "./constants" -export * from "./types" - -function checkTUICommand(command: string): BlockResult { - const normalizedCmd = command.trim() - - for (const pattern of ALWAYS_BLOCK_PATTERNS) { - if (pattern.test(normalizedCmd)) { - return { - blocked: true, - reason: `Command requires full TUI`, - command: normalizedCmd, - matchedPattern: pattern.source, - } - } - } - - return { blocked: false } -} - -function wrapWithNonInteractiveEnv(command: string): string { - const envPrefix = Object.entries(NON_INTERACTIVE_ENV) - .map(([key, value]) => `${key}=${value}`) - .join(" ") - - return `${envPrefix} ${command} < /dev/null 2>&1 || ${envPrefix} ${command} 2>&1` -} - -export function createInteractiveBashBlockerHook(ctx: PluginInput) { - return { - "tool.execute.before": async ( - input: { tool: string; sessionID: string; callID: string }, - output: { args: Record } - ): Promise => { - const toolLower = input.tool.toLowerCase() - - if (toolLower !== "bash") { - return - } - - const command = output.args.command as string | undefined - if (!command) { - return - } - - const result = checkTUICommand(command) - - if (result.blocked) { - log(`[${HOOK_NAME}] Blocking TUI command`, { - sessionID: input.sessionID, - command: result.command, - pattern: result.matchedPattern, - }) - - ctx.client.tui - .showToast({ - body: { - title: "TUI Command Blocked", - message: `${result.reason}\nUse tmux or interactive-terminal skill instead.`, - variant: "error", - duration: 5000, - }, - }) - .catch(() => {}) - - throw new Error( - `[${HOOK_NAME}] ${result.reason}\n` + - `Command: ${result.command}\n` + - `Pattern: ${result.matchedPattern}\n` + - TMUX_SUGGESTION - ) - } - - output.args.command = wrapWithNonInteractiveEnv(command) - - log(`[${HOOK_NAME}] Wrapped command with non-interactive environment`, { - sessionID: input.sessionID, - original: command, - wrapped: output.args.command, - }) - }, - } -} diff --git a/src/hooks/interactive-bash-blocker/types.ts b/src/hooks/interactive-bash-blocker/types.ts deleted file mode 100644 index 454ca46..0000000 --- a/src/hooks/interactive-bash-blocker/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface InteractiveBashBlockerConfig { - disabled?: boolean -} - -export interface BlockResult { - blocked: boolean - reason?: string - command?: string - matchedPattern?: string -} diff --git a/src/hooks/interactive-bash-blocker/constants.ts b/src/hooks/non-interactive-env/constants.ts similarity index 79% rename from src/hooks/interactive-bash-blocker/constants.ts rename to src/hooks/non-interactive-env/constants.ts index f77f33d..a9065de 100644 --- a/src/hooks/interactive-bash-blocker/constants.ts +++ b/src/hooks/non-interactive-env/constants.ts @@ -1,6 +1,8 @@ -export const HOOK_NAME = "interactive-bash-blocker" +export const HOOK_NAME = "non-interactive-env" -export const NON_INTERACTIVE_ENV = { +export const NULL_DEVICE = process.platform === "win32" ? "NUL" : "/dev/null" + +export const NON_INTERACTIVE_ENV: Record = { CI: "true", DEBIAN_FRONTEND: "noninteractive", GIT_TERMINAL_PROMPT: "0", @@ -8,7 +10,7 @@ export const NON_INTERACTIVE_ENV = { HOMEBREW_NO_AUTO_UPDATE: "1", } -export const ALWAYS_BLOCK_PATTERNS = [ +export const TUI_PATTERNS = [ /\b(?:vim?|nvim|nano|emacs|pico|joe|micro|helix|hx)\b/, /^\s*(?:python|python3|ipython|node|bun|deno|irb|pry|ghci|erl|iex|lua|R)\s*$/, /\btop\b(?!\s+\|)/, @@ -33,8 +35,8 @@ export const ALWAYS_BLOCK_PATTERNS = [ /\bselect\b.*\bin\b/, ] -export const TMUX_SUGGESTION = ` -[interactive-bash-blocker] +export const TUI_SUGGESTION = ` +[non-interactive-env] This command requires a full interactive terminal (TUI) which cannot be emulated. **Recommendation**: Use tmux for TUI commands. diff --git a/src/hooks/non-interactive-env/index.ts b/src/hooks/non-interactive-env/index.ts new file mode 100644 index 0000000..830fa6f --- /dev/null +++ b/src/hooks/non-interactive-env/index.ts @@ -0,0 +1,40 @@ +import type { PluginInput } from "@opencode-ai/plugin" +import { HOOK_NAME, NULL_DEVICE, NON_INTERACTIVE_ENV } from "./constants" +import { log } from "../../shared" + +export * from "./constants" +export * from "./types" + +function wrapWithNonInteractiveEnv(command: string): string { + const envPrefix = Object.entries(NON_INTERACTIVE_ENV) + .map(([key, value]) => `${key}=${value}`) + .join(" ") + + return `${envPrefix} ${command} < ${NULL_DEVICE} 2>&1 || ${envPrefix} ${command} 2>&1` +} + +export function createNonInteractiveEnvHook(_ctx: PluginInput) { + return { + "tool.execute.before": async ( + input: { tool: string; sessionID: string; callID: string }, + output: { args: Record } + ): Promise => { + if (input.tool.toLowerCase() !== "bash") { + return + } + + const command = output.args.command as string | undefined + if (!command) { + return + } + + output.args.command = wrapWithNonInteractiveEnv(command) + + log(`[${HOOK_NAME}] Wrapped command with non-interactive environment`, { + sessionID: input.sessionID, + original: command, + wrapped: output.args.command, + }) + }, + } +} diff --git a/src/hooks/non-interactive-env/types.ts b/src/hooks/non-interactive-env/types.ts new file mode 100644 index 0000000..7911c4c --- /dev/null +++ b/src/hooks/non-interactive-env/types.ts @@ -0,0 +1,10 @@ +export interface NonInteractiveEnvConfig { + disabled?: boolean +} + +export interface TUICheckResult { + isTUI: boolean + reason?: string + command?: string + matchedPattern?: string +} diff --git a/src/index.ts b/src/index.ts index 1766a8e..efcfee4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,7 @@ import { createAutoUpdateCheckerHook, createKeywordDetectorHook, createAgentUsageReminderHook, - createInteractiveBashBlockerHook, + createNonInteractiveEnvHook, } from "./hooks"; import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity"; import { @@ -239,8 +239,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { const agentUsageReminder = isHookEnabled("agent-usage-reminder") ? createAgentUsageReminderHook(ctx) : null; - const interactiveBashBlocker = isHookEnabled("interactive-bash-blocker") - ? createInteractiveBashBlockerHook(ctx) + const nonInteractiveEnv = isHookEnabled("non-interactive-env") + ? createNonInteractiveEnvHook(ctx) : null; updateTerminalTitle({ sessionId: "main" }); @@ -483,7 +483,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { "tool.execute.before": async (input, output) => { await claudeCodeHooks["tool.execute.before"](input, output); - await interactiveBashBlocker?.["tool.execute.before"](input, output); + await nonInteractiveEnv?.["tool.execute.before"](input, output); await commentChecker?.["tool.execute.before"](input, output); if (input.sessionID === getMainSessionID()) {