diff --git a/src/index.ts b/src/index.ts index 697fd04..c367239 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,7 +44,7 @@ import { getCurrentSessionTitle, } from "./features/claude-code-session-state"; import { updateTerminalTitle } from "./features/terminal"; -import { builtinTools, createCallOmoAgent, createBackgroundTools, createLookAt } from "./tools"; +import { builtinTools, createCallOmoAgent, createBackgroundTools, createLookAt, interactive_bash, getTmuxPath } from "./tools"; import { BackgroundManager } from "./features/background-agent"; import { createBuiltinMcps } from "./mcp"; import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig, type HookName } from "./config"; @@ -263,6 +263,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { ? await createGoogleAntigravityAuthPlugin(ctx) : null; + const tmuxAvailable = await getTmuxPath(); + return { ...(googleAuthHooks ? { auth: googleAuthHooks.auth } : {}), @@ -271,6 +273,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { ...backgroundTools, call_omo_agent: callOmoAgent, look_at: lookAt, + ...(tmuxAvailable ? { interactive_bash } : {}), }, "chat.message": async (input, output) => { diff --git a/src/tools/index.ts b/src/tools/index.ts index 33b7754..b61a006 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -21,7 +21,9 @@ import { grep } from "./grep" import { glob } from "./glob" import { slashcommand } from "./slashcommand" import { skill } from "./skill" -import { interactive_bash } from "./interactive-bash" + +export { interactive_bash, startBackgroundCheck as startTmuxCheck } from "./interactive-bash" +export { getTmuxPath } from "./interactive-bash/utils" import { createBackgroundTask, @@ -63,5 +65,4 @@ export const builtinTools = { glob, slashcommand, skill, - interactive_bash, } diff --git a/src/tools/interactive-bash/index.ts b/src/tools/interactive-bash/index.ts index 6353035..72b101e 100644 --- a/src/tools/interactive-bash/index.ts +++ b/src/tools/interactive-bash/index.ts @@ -1,3 +1,4 @@ import { interactive_bash } from "./tools" +import { startBackgroundCheck } from "./utils" -export { interactive_bash } +export { interactive_bash, startBackgroundCheck } diff --git a/src/tools/interactive-bash/tools.ts b/src/tools/interactive-bash/tools.ts index 714db9b..32ccadd 100644 --- a/src/tools/interactive-bash/tools.ts +++ b/src/tools/interactive-bash/tools.ts @@ -1,5 +1,6 @@ import { tool } from "@opencode-ai/plugin/tool" import { DEFAULT_TIMEOUT_MS, INTERACTIVE_BASH_DESCRIPTION } from "./constants" +import { getCachedTmuxPath } from "./utils" /** * Quote-aware command tokenizer with escape handling @@ -53,13 +54,15 @@ export const interactive_bash = tool({ }, execute: async (args) => { try { + const tmuxPath = getCachedTmuxPath() ?? "tmux" + const parts = tokenizeCommand(args.tmux_command) if (parts.length === 0) { return "Error: Empty tmux command" } - const proc = Bun.spawn(["tmux", ...parts], { + const proc = Bun.spawn([tmuxPath, ...parts], { stdout: "pipe", stderr: "pipe", }) diff --git a/src/tools/interactive-bash/utils.ts b/src/tools/interactive-bash/utils.ts new file mode 100644 index 0000000..1aa3462 --- /dev/null +++ b/src/tools/interactive-bash/utils.ts @@ -0,0 +1,71 @@ +import { spawn } from "bun" + +let tmuxPath: string | null = null +let initPromise: Promise | null = null + +async function findTmuxPath(): Promise { + const isWindows = process.platform === "win32" + const cmd = isWindows ? "where" : "which" + + try { + const proc = spawn([cmd, "tmux"], { + stdout: "pipe", + stderr: "pipe", + }) + + const exitCode = await proc.exited + if (exitCode !== 0) { + return null + } + + const stdout = await new Response(proc.stdout).text() + const path = stdout.trim().split("\n")[0] + + if (!path) { + return null + } + + const verifyProc = spawn([path, "-V"], { + stdout: "pipe", + stderr: "pipe", + }) + + const verifyExitCode = await verifyProc.exited + if (verifyExitCode !== 0) { + return null + } + + return path + } catch { + return null + } +} + +export async function getTmuxPath(): Promise { + if (tmuxPath !== null) { + return tmuxPath + } + + if (initPromise) { + return initPromise + } + + initPromise = (async () => { + const path = await findTmuxPath() + tmuxPath = path + return path + })() + + return initPromise +} + +export function getCachedTmuxPath(): string | null { + return tmuxPath +} + +export function startBackgroundCheck(): void { + if (!initPromise) { + initPromise = getTmuxPath() + initPromise.catch(() => {}) + } +}