Alvin Unreal 2 months ago
parent
commit
065d46c752

+ 244 - 0
.orchestrator/plans/2026-01-21-codebase-cleanup.md

@@ -0,0 +1,244 @@
+# Codebase Cleanup & Simplification Plan
+
+**Created:** 2026-01-21
+**Goal:** Review, simplify, and clean up the codebase following YAGNI principles before writing tests
+**Estimated Effort:** 30-45 hours
+
+---
+
+## Goal & Constraints
+
+### Primary Goals
+1. **Remove complexity** - Simplify overly complex functions and classes
+2. **Eliminate YAGNI violations** - Remove unused code, over-engineered solutions
+3. **Standardize patterns** - Consistent error handling, logging, and coding style
+4. **Improve maintainability** - Make code easier to understand and test
+
+### Constraints
+- Maintain backward compatibility
+- Don't break existing functionality
+- Keep changes atomic and verifiable
+- Focus on cleanup only (no new features)
+
+---
+
+## Stage 1: Quick Wins (Low Effort, High Impact)
+
+### Tasks
+
+#### 1.1 Remove Commented-Out Code
+- [ ] Remove commented code in `cli/install.ts:159-162`
+- [ ] Remove commented code in `cli/install.ts:238-240`
+- [ ] Search entire codebase for other commented blocks
+- [ ] Remove all commented-out code (keep meaningful comments)
+
+#### 1.2 Standardize Logging
+- [ ] Replace `console.log()` with `log()` in `cli/install.ts`
+- [ ] Replace `console.error()` with `log()` in `cli/index.ts`
+- [ ] Replace `console.warn()` with `log()` in `cli/config-manager.ts`
+- [ ] Replace `console.log/error()` in `tools/grep/downloader.ts`
+- [ ] Replace `console.log/error()` in `tools/ast-grep/downloader.ts`
+- [ ] Verify all logging uses the `log()` function from `shared/logger.ts`
+
+#### 1.3 Remove Unused Imports
+- [ ] Check `tools/lsp/client.ts` for unused imports
+- [ ] Check `tools/lsp/utils.ts` for unused imports
+- [ ] Check `cli/config-manager.ts` for unused imports
+- [ ] Run TypeScript compiler to find unused imports
+- [ ] Remove all unused imports
+
+#### 1.4 Verify Stage 1 Completion
+- [ ] Verify stage completion and update plan if necessary (@orchestrator)
+
+---
+
+## Stage 2: Standardize Error Handling
+
+### Tasks
+
+#### 2.1 Define Error Handling Standard
+- [ ] Document the chosen error handling pattern (throw errors with context)
+- [ ] Create error types if needed (e.g., `ConfigError`, `LSPError`, `ToolError`)
+- [ ] Document error handling guidelines in a comment or doc
+
+#### 2.2 Update Error Handling in Tools
+- [ ] Update `tools/lsp/client.ts` - Ensure all errors are thrown with context
+- [ ] Update `tools/lsp/utils.ts` - Standardize error handling
+- [ ] Update `tools/grep/cli.ts` - Change from returning error objects to throwing
+- [ ] Update `tools/grep/downloader.ts` - Standardize error handling
+- [ ] Update `tools/ast-grep/cli.ts` - Standardize error handling
+- [ ] Update `tools/ast-grep/downloader.ts` - Standardize error handling
+
+#### 2.3 Update Error Handling in Other Modules
+- [ ] Update `cli/config-manager.ts` - Remove silent catches, throw errors
+- [ ] Update `hooks/auto-update-checker/checker.ts` - Remove silent catches
+- [ ] Update `utils/tmux.ts` - Standardize error handling
+- [ ] Update `tools/background.ts` - Standardize error handling
+- [ ] Update `tools/skill/mcp-manager.ts` - Standardize error handling
+
+#### 2.4 Verify Stage 2 Completion
+- [ ] Verify stage completion and update plan if necessary (@orchestrator)
+
+---
+
+## Stage 3: Simplify Complex Functions
+
+### Tasks
+
+#### 3.1 Extract LSP Buffer Processing
+- [ ] Extract `processBuffer()` from `tools/lsp/client.ts:266-317` to new module
+- [ ] Create `tools/lsp/buffer-processor.ts` with extracted logic
+- [ ] Update `tools/lsp/client.ts` to use the new module
+- [ ] Add JSDoc comments to the new module
+
+#### 3.2 Break Down Background Task Execution
+- [ ] Extract session creation logic from `tools/background.ts:140-292`
+- [ ] Extract polling logic from `tools/background.ts:217-249`
+- [ ] Extract message extraction logic from `tools/background.ts`
+- [ ] Create helper functions with clear names
+- [ ] Update `executeSync()` to use the new helper functions
+
+#### 3.3 Extract Text Edit Logic
+- [ ] Extract `applyTextEditsToFile()` from `tools/lsp/utils.ts:145-179`
+- [ ] Create `tools/lsp/text-editor.ts` with extracted logic
+- [ ] Update `tools/lsp/utils.ts` to use the new module
+- [ ] Add JSDoc comments to the new module
+
+#### 3.4 Simplify Config Manager
+- [ ] Extract `MODEL_MAPPINGS` from `cli/config-manager.ts:293-318` to separate file
+- [ ] Create `cli/model-mappings.ts` with the constant
+- [ ] Simplify provider config logic in `cli/config-manager.ts`
+- [ ] Consider using `jsonc-parser` library for JSONC parsing (optional)
+
+#### 3.5 Verify Stage 3 Completion
+- [ ] Verify stage completion and update plan if necessary (@orchestrator)
+
+---
+
+## Stage 4: Remove YAGNI Violations
+
+### Tasks
+
+#### 4.1 Evaluate LSP Connection Pooling
+- [ ] Review `tools/lsp/client.ts:18-153` - LSPServerManager class
+- [ ] Determine if connection pooling is actually needed
+- [ ] If not needed, simplify to single connection
+- [ ] If needed, document why it's necessary
+- [ ] Remove idle timeout cleanup if not needed
+
+#### 4.2 Simplify Skill Template
+- [ ] Move hardcoded template from `tools/skill/builtin.ts:20-100` to external file
+- [ ] Create `tools/skill/template.md` or similar
+- [ ] Update `tools/skill/builtin.ts` to load template from file
+- [ ] Consider making template configurable
+
+#### 4.3 Remove Unused Functions
+- [ ] Check if `formatSymbolKind()` in `tools/lsp/utils.ts:106-113` is used
+- [ ] Check if `formatSeverity()` in `tools/lsp/utils.ts` is used
+- [ ] Check if `ensureCliAvailable()` in `tools/ast-grep/cli.ts:224-232` is used
+- [ ] Remove unused functions
+- [ ] Search for other potentially unused exports
+
+#### 4.4 Consolidate Binary Download Logic
+- [ ] Create shared downloader utility in `tools/shared/downloader.ts`
+- [ ] Unify tar.gz extraction logic from `tools/grep/downloader.ts`
+- [ ] Unify zip extraction logic from `tools/ast-grep/downloader.ts`
+- [ ] Standardize error handling in downloader
+- [ ] Update both modules to use the shared utility
+
+#### 4.5 Verify Stage 4 Completion
+- [ ] Verify stage completion and update plan if necessary (@orchestrator)
+
+---
+
+## Stage 5: Code Quality & Consistency
+
+### Tasks
+
+#### 5.1 Standardize Async Patterns
+- [ ] Replace promise chaining with async/await where appropriate
+- [ ] Update `tools/lsp/client.ts:334` - Use async/await instead of setTimeout
+- [ ] Review all `.then()`/`.catch()` usage
+- [ ] Convert to async/await for consistency
+
+#### 5.2 Improve Naming Consistency
+- [ ] Review all type names for consistency (PascalCase for types)
+- [ ] Review all function names for consistency (camelCase)
+- [ ] Add `is` prefix to boolean functions where missing
+- [ ] Ensure parameter names follow conventions
+
+#### 5.3 Add Missing JSDoc Comments
+- [ ] Add JSDoc to complex functions
+- [ ] Add JSDoc to exported functions
+- [ ] Add JSDoc to classes and interfaces
+- [ ] Document parameters and return types
+
+#### 5.4 Run TypeScript Compiler
+- [ ] Run `tsc --noEmit` to check for type errors
+- [ ] Fix any type errors found
+- [ ] Ensure strict mode compliance
+
+#### 5.5 Verify Stage 5 Completion
+- [ ] Verify stage completion and update plan if necessary (@orchestrator)
+
+---
+
+## Stage 6: Final Verification
+
+### Tasks
+
+#### 6.1 Run Existing Tests
+- [ ] Run all existing tests to ensure nothing is broken
+- [ ] Fix any failing tests
+- [ ] Ensure test suite passes
+
+#### 6.2 Manual Testing
+- [ ] Test basic plugin functionality
+- [ ] Test LSP features
+- [ ] Test background tasks
+- [ ] Test MCP skills
+- [ ] Test CLI commands
+
+#### 6.3 Code Review Checklist
+- [ ] All commented code removed
+- [ ] All logging uses `log()` function
+- [ ] All unused imports removed
+- [ ] Error handling is consistent
+- [ ] Complex functions extracted
+- [ ] YAGNI violations removed
+- [ ] Async patterns standardized
+- [ ] Naming is consistent
+- [ ] JSDoc comments added
+- [ ] No TypeScript errors
+
+#### 6.4 Update Documentation
+- [ ] Update README if needed
+- [ ] Update any inline documentation
+- [ ] Document any breaking changes
+
+#### 6.5 Verify Stage 6 Completion
+- [ ] Verify stage completion and update plan if necessary (@orchestrator)
+
+---
+
+## Success Criteria
+
+✅ Code is easier to understand and maintain
+✅ No commented-out code remains
+✅ Logging is consistent throughout
+✅ Error handling follows a standard pattern
+✅ Complex functions are broken down
+✅ YAGNI violations are removed
+✅ All existing tests pass
+✅ No TypeScript errors
+✅ Manual testing confirms functionality
+
+---
+
+## Notes
+
+- This plan focuses on cleanup only - no new features
+- Each stage should be verified before moving to the next
+- Update this plan as needed based on findings during execution
+- Keep changes atomic and testable
+- Document any decisions made during the process

+ 26 - 0
docs/error-handling.md

@@ -0,0 +1,26 @@
+# Error Handling Guidelines
+
+To maintain consistency and reliability across the `oh-my-opencode-slim` codebase, follow these guidelines:
+
+## Core Principles
+
+- **Throw `Error`** for invariant violations, programmer errors, or when the caller cannot reasonably continue.
+    - *Example*: Missing LSP server in `src/tools/lsp/utils.ts` or failed binary download in `src/tools/grep/downloader.ts`.
+- **Return Structured Results** for tool/CLI boundaries and long-running workflows.
+    - *Format*: `{ success: false, error: string }`.
+    - *Example*: Configuration merging or model mapping results.
+- **Prefix Tool Outputs** with `Error:` for LLM-facing results.
+    - *Example*: `src/tools/background.ts` outputs. Keep formatting stable for automated consumers.
+- **Include Context** in every message.
+    - *Format*: `[component] operation: <message>`
+    - *Example*: `[tmux] findTmuxPath: command not found`
+- **Normalize Unknown Errors**. Use `err instanceof Error ? err.message : String(err)`. Avoid stringifying full error objects in user-facing messages.
+- **Log and Continue** only for optional integrations.
+    - *Example*: Tmux, auto-update, background init. Never swallow errors in required setup paths.
+- **No Custom Error Classes** unless programmatic branching is strictly required across module boundaries.
+
+## Compatibility Notes
+
+- `src/tools/background.ts`: Tool outputs must stay string-based and prefixed with `Error:`.
+- `src/tools/ast-grep/cli.ts` & `src/tools/grep/cli.ts`: Return `{ error }` fields in result objects instead of throwing.
+- `src/cli/index.ts`: The top-level `main().catch` is the final safety net. Fatal errors should propagate here.

+ 1 - 0
src/agents/index.ts

@@ -121,6 +121,7 @@ export function getAgentConfigs(config?: PluginConfig): Record<string, SDKAgentC
       // Apply classification-based visibility and mode
       // Apply classification-based visibility and mode
       if (isSubagent(a.name)) {
       if (isSubagent(a.name)) {
         sdkConfig.mode = "subagent";
         sdkConfig.mode = "subagent";
+        sdkConfig.hidden = true;
       } else if (a.name === "orchestrator") {
       } else if (a.name === "orchestrator") {
         sdkConfig.mode = "primary";
         sdkConfig.mode = "primary";
       }
       }

+ 19 - 39
src/cli/config-manager.ts

@@ -3,6 +3,8 @@ import { homedir } from "node:os"
 import { join } from "node:path"
 import { join } from "node:path"
 import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types"
 import type { ConfigMergeResult, DetectedConfig, InstallConfig } from "./types"
 import { DEFAULT_AGENT_SKILLS } from "../tools/skill/builtin"
 import { DEFAULT_AGENT_SKILLS } from "../tools/skill/builtin"
+import { MODEL_MAPPINGS } from "./model-mappings"
+import { log } from "../shared/logger"
 
 
 const PACKAGE_NAME = "oh-my-opencode-slim"
 const PACKAGE_NAME = "oh-my-opencode-slim"
 
 
@@ -70,7 +72,8 @@ function parseConfigFile(path: string): OpenCodeConfig | null {
     const content = readFileSync(path, "utf-8")
     const content = readFileSync(path, "utf-8")
     if (content.trim().length === 0) return null
     if (content.trim().length === 0) return null
     return JSON.parse(stripJsonComments(content)) as OpenCodeConfig
     return JSON.parse(stripJsonComments(content)) as OpenCodeConfig
-  } catch {
+  } catch (err) {
+    log(`[config-manager] parse: failed to parse ${path}: ${err instanceof Error ? err.message : String(err)}`)
     return null
     return null
   }
   }
 }
 }
@@ -102,7 +105,7 @@ function getExistingConfigPath(): string {
  */
  */
 function writeConfig(configPath: string, config: OpenCodeConfig): void {
 function writeConfig(configPath: string, config: OpenCodeConfig): void {
   if (configPath.endsWith(".jsonc")) {
   if (configPath.endsWith(".jsonc")) {
-    console.warn(
+    log(
       "[config-manager] Writing to .jsonc file - comments will not be preserved"
       "[config-manager] Writing to .jsonc file - comments will not be preserved"
     )
     )
   }
   }
@@ -117,7 +120,8 @@ export async function isOpenCodeInstalled(): Promise<boolean> {
     })
     })
     await proc.exited
     await proc.exited
     return proc.exitCode === 0
     return proc.exitCode === 0
-  } catch {
+  } catch (err) {
+    log(`[config-manager] check: failed to check opencode installation: ${err instanceof Error ? err.message : String(err)}`)
     return false
     return false
   }
   }
 }
 }
@@ -130,7 +134,8 @@ export async function isTmuxInstalled(): Promise<boolean> {
     })
     })
     await proc.exited
     await proc.exited
     return proc.exitCode === 0
     return proc.exitCode === 0
-  } catch {
+  } catch (err) {
+    log(`[config-manager] check: failed to check tmux installation: ${err instanceof Error ? err.message : String(err)}`)
     return false
     return false
   }
   }
 }
 }
@@ -144,7 +149,8 @@ export async function getOpenCodeVersion(): Promise<string | null> {
     const output = await new Response(proc.stdout).text()
     const output = await new Response(proc.stdout).text()
     await proc.exited
     await proc.exited
     return proc.exitCode === 0 ? output.trim() : null
     return proc.exitCode === 0 ? output.trim() : null
-  } catch {
+  } catch (err) {
+    log(`[config-manager] version: failed to get opencode version: ${err instanceof Error ? err.message : String(err)}`)
     return null
     return null
   }
   }
 }
 }
@@ -155,7 +161,8 @@ export async function fetchLatestVersion(packageName: string): Promise<string |
     if (!res.ok) return null
     if (!res.ok) return null
     const data = (await res.json()) as { version: string }
     const data = (await res.json()) as { version: string }
     return data.version
     return data.version
-  } catch {
+  } catch (err) {
+    log(`[config-manager] fetch: failed to fetch latest version for ${packageName}: ${err instanceof Error ? err.message : String(err)}`)
     return null
     return null
   }
   }
 }
 }
@@ -167,7 +174,7 @@ export async function addPluginToOpenCodeConfig(): Promise<ConfigMergeResult> {
     return {
     return {
       success: false,
       success: false,
       configPath: getConfigDir(),
       configPath: getConfigDir(),
-      error: `Failed to create config directory: ${err}`,
+      error: `[config-manager] install: failed to create config directory: ${err instanceof Error ? err.message : String(err)}`,
     }
     }
   }
   }
 
 
@@ -193,7 +200,7 @@ export async function addPluginToOpenCodeConfig(): Promise<ConfigMergeResult> {
     return {
     return {
       success: false,
       success: false,
       configPath,
       configPath,
-      error: `Failed to update opencode config: ${err}`,
+      error: `[config-manager] install: failed to update opencode config: ${err instanceof Error ? err.message : String(err)}`,
     }
     }
   }
   }
 }
 }
@@ -224,7 +231,7 @@ export async function addAuthPlugins(installConfig: InstallConfig): Promise<Conf
     return {
     return {
       success: false,
       success: false,
       configPath,
       configPath,
-      error: `Failed to add auth plugins: ${err}`,
+      error: `[config-manager] auth: failed to add auth plugins: ${err instanceof Error ? err.message : String(err)}`,
     }
     }
   }
   }
 }
 }
@@ -284,39 +291,12 @@ export function addProviderConfig(installConfig: InstallConfig): ConfigMergeResu
     return {
     return {
       success: false,
       success: false,
       configPath,
       configPath,
-      error: `Failed to add provider config: ${err}`,
+      error: `[config-manager] provider: failed to add provider config: ${err instanceof Error ? err.message : String(err)}`,
     }
     }
   }
   }
 }
 }
 
 
 
 
-// Model mappings by provider priority
-const MODEL_MAPPINGS = {
-  antigravity: {
-    orchestrator: "google/claude-opus-4-5-thinking",
-    oracle: "google/claude-opus-4-5-thinking",
-    librarian: "google/gemini-3-flash",
-    explorer: "google/gemini-3-flash",
-    designer: "google/gemini-3-flash",
-    fixer: "google/gemini-3-flash",
-  },
-  openai: {
-    orchestrator: "openai/gpt-5.2-codex",
-    oracle: "openai/gpt-5.2-codex",
-    librarian: "openai/gpt-5.1-codex-mini",
-    explorer: "openai/gpt-5.1-codex-mini",
-    designer: "openai/gpt-5.1-codex-mini",
-    fixer: "openai/gpt-5.1-codex-mini",
-  },
-  opencode: {
-    orchestrator: "opencode/glm-4.7-free",
-    oracle: "opencode/glm-4.7-free",
-    librarian: "opencode/glm-4.7-free",
-    explorer: "opencode/glm-4.7-free",
-    designer: "opencode/glm-4.7-free",
-  },
-} as const;
-
 export function generateLiteConfig(installConfig: InstallConfig): Record<string, unknown> {
 export function generateLiteConfig(installConfig: InstallConfig): Record<string, unknown> {
   // Priority: antigravity > openai > opencode (Zen free models)
   // Priority: antigravity > openai > opencode (Zen free models)
   const baseProvider = installConfig.hasAntigravity
   const baseProvider = installConfig.hasAntigravity
@@ -370,7 +350,7 @@ export function writeLiteConfig(installConfig: InstallConfig): ConfigMergeResult
     return {
     return {
       success: false,
       success: false,
       configPath,
       configPath,
-      error: `Failed to write lite config: ${err}`,
+      error: `[config-manager] lite: failed to write lite config: ${err instanceof Error ? err.message : String(err)}`,
     }
     }
   }
   }
 }
 }
@@ -396,7 +376,7 @@ export function disableDefaultAgents(): ConfigMergeResult {
     return {
     return {
       success: false,
       success: false,
       configPath,
       configPath,
-      error: `Failed to disable default agents: ${err}`,
+      error: `[config-manager] agents: failed to disable default agents: ${err instanceof Error ? err.message : String(err)}`,
     }
     }
   }
   }
 }
 }

+ 5 - 4
src/cli/index.ts

@@ -1,6 +1,7 @@
 #!/usr/bin/env bun
 #!/usr/bin/env bun
 import { install } from "./install"
 import { install } from "./install"
 import type { InstallArgs, BooleanArg } from "./types"
 import type { InstallArgs, BooleanArg } from "./types"
+import { log } from "../shared/logger"
 
 
 function parseArgs(args: string[]): InstallArgs {
 function parseArgs(args: string[]): InstallArgs {
   const result: InstallArgs = {
   const result: InstallArgs = {
@@ -28,7 +29,7 @@ function parseArgs(args: string[]): InstallArgs {
 }
 }
 
 
 function printHelp(): void {
 function printHelp(): void {
-  console.log(`
+  log(`
 oh-my-opencode-slim installer
 oh-my-opencode-slim installer
 
 
 Usage: bunx oh-my-opencode-slim install [OPTIONS]
 Usage: bunx oh-my-opencode-slim install [OPTIONS]
@@ -58,13 +59,13 @@ async function main(): Promise<void> {
     printHelp()
     printHelp()
     process.exit(0)
     process.exit(0)
   } else {
   } else {
-    console.error(`Unknown command: ${args[0]}`)
-    console.error("Run with --help for usage information")
+    log(`Unknown command: ${args[0]}`)
+    log("Run with --help for usage information")
     process.exit(1)
     process.exit(1)
   }
   }
 }
 }
 
 
 main().catch((err) => {
 main().catch((err) => {
-  console.error("Fatal error:", err)
+  log("Fatal error:", err)
   process.exit(1)
   process.exit(1)
 })
 })

+ 37 - 56
src/cli/install.ts

@@ -12,6 +12,7 @@ import {
   isTmuxInstalled,
   isTmuxInstalled,
   generateLiteConfig,
   generateLiteConfig,
 } from "./config-manager"
 } from "./config-manager"
+import { log } from "../shared/logger"
 
 
 // Colors
 // Colors
 const GREEN = "\x1b[32m"
 const GREEN = "\x1b[32m"
@@ -33,30 +34,30 @@ const SYMBOLS = {
 }
 }
 
 
 function printHeader(isUpdate: boolean): void {
 function printHeader(isUpdate: boolean): void {
-  console.log()
-  console.log(`${BOLD}oh-my-opencode-slim ${isUpdate ? "Update" : "Install"}${RESET}`)
-  console.log("=".repeat(30))
-  console.log()
+  log("")
+  log(`${BOLD}oh-my-opencode-slim ${isUpdate ? "Update" : "Install"}${RESET}`)
+  log("=".repeat(30))
+  log("")
 }
 }
 
 
 function printStep(step: number, total: number, message: string): void {
 function printStep(step: number, total: number, message: string): void {
-  console.log(`${DIM}[${step}/${total}]${RESET} ${message}`)
+  log(`${DIM}[${step}/${total}]${RESET} ${message}`)
 }
 }
 
 
 function printSuccess(message: string): void {
 function printSuccess(message: string): void {
-  console.log(`${SYMBOLS.check} ${message}`)
+  log(`${SYMBOLS.check} ${message}`)
 }
 }
 
 
 function printError(message: string): void {
 function printError(message: string): void {
-  console.log(`${SYMBOLS.cross} ${RED}${message}${RESET}`)
+  log(`${SYMBOLS.cross} ${RED}${message}${RESET}`)
 }
 }
 
 
 function printInfo(message: string): void {
 function printInfo(message: string): void {
-  console.log(`${SYMBOLS.info} ${message}`)
+  log(`${SYMBOLS.info} ${message}`)
 }
 }
 
 
 function printWarning(message: string): void {
 function printWarning(message: string): void {
-  console.log(`${SYMBOLS.warn} ${YELLOW}${message}${RESET}`)
+  log(`${SYMBOLS.warn} ${YELLOW}${message}${RESET}`)
 }
 }
 
 
 async function checkOpenCodeInstalled(): Promise<{ ok: boolean; version?: string }> {
 async function checkOpenCodeInstalled(): Promise<{ ok: boolean; version?: string }> {
@@ -64,7 +65,7 @@ async function checkOpenCodeInstalled(): Promise<{ ok: boolean; version?: string
   if (!installed) {
   if (!installed) {
     printError("OpenCode is not installed on this system.")
     printError("OpenCode is not installed on this system.")
     printInfo("Install it with:")
     printInfo("Install it with:")
-    console.log(`     ${BLUE}curl -fsSL https://opencode.ai/install | bash${RESET}`)
+    log(`     ${BLUE}curl -fsSL https://opencode.ai/install | bash${RESET}`)
     return { ok: false }
     return { ok: false }
   }
   }
   const version = await getOpenCodeVersion()
   const version = await getOpenCodeVersion()
@@ -100,17 +101,17 @@ function printAgentModels(config: InstallConfig): void {
 
 
   if (!agents || Object.keys(agents).length === 0) return
   if (!agents || Object.keys(agents).length === 0) return
 
 
-  console.log(`${BOLD}Agent Configuration:${RESET}`)
-  console.log()
+  log(`${BOLD}Agent Configuration:${RESET}`)
+  log("")
 
 
   const maxAgentLen = Math.max(...Object.keys(agents).map((a) => a.length))
   const maxAgentLen = Math.max(...Object.keys(agents).map((a) => a.length))
 
 
   for (const [agent, info] of Object.entries(agents)) {
   for (const [agent, info] of Object.entries(agents)) {
     const padding = " ".repeat(maxAgentLen - agent.length)
     const padding = " ".repeat(maxAgentLen - agent.length)
     const skillsStr = info.skills.length > 0 ? ` ${DIM}[${info.skills.join(", ")}]${RESET}` : ""
     const skillsStr = info.skills.length > 0 ? ` ${DIM}[${info.skills.join(", ")}]${RESET}` : ""
-    console.log(`  ${DIM}${agent}${RESET}${padding} ${SYMBOLS.arrow} ${BLUE}${info.model}${RESET}${skillsStr}`)
+    log(`  ${DIM}${agent}${RESET}${padding} ${SYMBOLS.arrow} ${BLUE}${info.model}${RESET}${skillsStr}`)
   }
   }
-  console.log()
+  log("")
 }
 }
 
 
 function argsToConfig(args: InstallArgs): InstallConfig {
 function argsToConfig(args: InstallArgs): InstallConfig {
@@ -138,30 +139,17 @@ async function askYesNo(
 
 
 async function runInteractiveMode(detected: DetectedConfig): Promise<InstallConfig> {
 async function runInteractiveMode(detected: DetectedConfig): Promise<InstallConfig> {
   const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
   const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
-  // TODO: tmux has a bug, disabled for now
-  // const tmuxInstalled = await isTmuxInstalled()
-  // const totalQuestions = tmuxInstalled ? 3 : 2
   const totalQuestions = 2
   const totalQuestions = 2
 
 
   try {
   try {
-    console.log(`${BOLD}Question 1/${totalQuestions}:${RESET}`)
+    log(`${BOLD}Question 1/${totalQuestions}:${RESET}`)
     printInfo("The Pantheon is tuned for Antigravity's model routing. Other models work, but results may vary.")
     printInfo("The Pantheon is tuned for Antigravity's model routing. Other models work, but results may vary.")
     const antigravity = await askYesNo(rl, "Do you have an Antigravity subscription?", "yes")
     const antigravity = await askYesNo(rl, "Do you have an Antigravity subscription?", "yes")
-    console.log()
+    log("")
 
 
-    console.log(`${BOLD}Question 2/${totalQuestions}:${RESET}`)
+    log(`${BOLD}Question 2/${totalQuestions}:${RESET}`)
     const openai = await askYesNo(rl, "Do you have access to OpenAI API?", detected.hasOpenAI ? "yes" : "no")
     const openai = await askYesNo(rl, "Do you have access to OpenAI API?", detected.hasOpenAI ? "yes" : "no")
-    console.log()
-
-    // TODO: tmux has a bug, disabled for now
-    // let tmux: BooleanArg = "no"
-    // if (tmuxInstalled) {
-    //   console.log(`${BOLD}Question 3/3:${RESET}`)
-    //   printInfo(`${BOLD}Tmux detected!${RESET} We can enable tmux integration for you.`)
-    //   printInfo("This will spawn new panes for sub-agents, letting you watch them work in real-time.")
-    //   tmux = await askYesNo(rl, "Enable tmux integration?", detected.hasTmux ? "yes" : "no")
-    //   console.log()
-    // }
+    log("")
 
 
     return {
     return {
       hasAntigravity: antigravity === "yes",
       hasAntigravity: antigravity === "yes",
@@ -213,9 +201,9 @@ async function runInstall(config: InstallConfig): Promise<number> {
   if (!handleStepResult(liteResult, "Config written")) return 1
   if (!handleStepResult(liteResult, "Config written")) return 1
 
 
   // Summary
   // Summary
-  console.log()
-  console.log(formatConfigSummary(config))
-  console.log()
+  log("")
+  log(formatConfigSummary(config))
+  log("")
 
 
   printAgentModels(config)
   printAgentModels(config)
 
 
@@ -223,26 +211,19 @@ async function runInstall(config: InstallConfig): Promise<number> {
     printWarning("No providers configured. Zen free models will be used as fallback.")
     printWarning("No providers configured. Zen free models will be used as fallback.")
   }
   }
 
 
-  console.log(`${SYMBOLS.star} ${BOLD}${GREEN}${isUpdate ? "Configuration updated!" : "Installation complete!"}${RESET}`)
-  console.log()
-  console.log(`${BOLD}Next steps:${RESET}`)
-  console.log()
+  log(`${SYMBOLS.star} ${BOLD}${GREEN}${isUpdate ? "Configuration updated!" : "Installation complete!"}${RESET}`)
+  log("")
+  log(`${BOLD}Next steps:${RESET}`)
+  log("")
 
 
   let nextStep = 1
   let nextStep = 1
-  console.log(`  ${nextStep++}. Authenticate with your providers:`)
-  console.log(`     ${BLUE}$ opencode auth login${RESET}`)
-  console.log()
-
-  // TODO: tmux has a bug, disabled for now
-  // if (config.hasTmux) {
-  //   console.log(`  ${nextStep++}. Run OpenCode inside tmux:`)
-  //   console.log(`     ${BLUE}$ tmux${RESET}`)
-  //   console.log(`     ${BLUE}$ opencode${RESET}`)
-  // } else {
-  console.log(`  ${nextStep++}. Start OpenCode:`)
-  console.log(`     ${BLUE}$ opencode${RESET}`)
-  // }
-  console.log()
+  log(`  ${nextStep++}. Authenticate with your providers:`)
+  log(`     ${BLUE}$ opencode auth login${RESET}`)
+  log("")
+
+  log(`  ${nextStep++}. Start OpenCode:`)
+  log(`     ${BLUE}$ opencode${RESET}`)
+  log("")
 
 
   return 0
   return 0
 }
 }
@@ -260,11 +241,11 @@ export async function install(args: InstallArgs): Promise<number> {
       printHeader(false)
       printHeader(false)
       printError("Missing or invalid arguments:")
       printError("Missing or invalid arguments:")
       for (const key of errors) {
       for (const key of errors) {
-        console.log(`  ${SYMBOLS.bullet} --${key}=<yes|no>`)
+        log(`  ${SYMBOLS.bullet} --${key}=<yes|no>`)
       }
       }
-      console.log()
+      log("")
       printInfo("Usage: bunx oh-my-opencode-slim install --no-tui --antigravity=<yes|no> --openai=<yes|no> --tmux=<yes|no>")
       printInfo("Usage: bunx oh-my-opencode-slim install --no-tui --antigravity=<yes|no> --openai=<yes|no> --tmux=<yes|no>")
-      console.log()
+      log("")
       return 1
       return 1
     }
     }
 
 
@@ -279,7 +260,7 @@ export async function install(args: InstallArgs): Promise<number> {
   printStep(1, 1, "Checking OpenCode installation...")
   printStep(1, 1, "Checking OpenCode installation...")
   const { ok } = await checkOpenCodeInstalled()
   const { ok } = await checkOpenCodeInstalled()
   if (!ok) return 1
   if (!ok) return 1
-  console.log()
+  log("")
 
 
   const config = await runInteractiveMode(detected)
   const config = await runInteractiveMode(detected)
   return runInstall(config)
   return runInstall(config)

+ 26 - 0
src/cli/model-mappings.ts

@@ -0,0 +1,26 @@
+// Model mappings by provider priority
+export const MODEL_MAPPINGS = {
+  antigravity: {
+    orchestrator: "google/claude-opus-4-5-thinking",
+    oracle: "google/claude-opus-4-5-thinking",
+    librarian: "google/gemini-3-flash",
+    explorer: "google/gemini-3-flash",
+    designer: "google/gemini-3-flash",
+    fixer: "google/gemini-3-flash",
+  },
+  openai: {
+    orchestrator: "openai/gpt-5.2-codex",
+    oracle: "openai/gpt-5.2-codex",
+    librarian: "openai/gpt-5.1-codex-mini",
+    explorer: "openai/gpt-5.1-codex-mini",
+    designer: "openai/gpt-5.1-codex-mini",
+    fixer: "openai/gpt-5.1-codex-mini",
+  },
+  opencode: {
+    orchestrator: "opencode/glm-4.7-free",
+    oracle: "opencode/glm-4.7-free",
+    librarian: "opencode/glm-4.7-free",
+    explorer: "opencode/glm-4.7-free",
+    designer: "opencode/glm-4.7-free",
+  },
+} as const;

+ 2 - 1
src/features/background-manager.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect, test, beforeEach, mock } from "bun:test"
 import { describe, expect, test, beforeEach, mock } from "bun:test"
 import { BackgroundTaskManager, type BackgroundTask, type LaunchOptions } from "./background-manager"
 import { BackgroundTaskManager, type BackgroundTask, type LaunchOptions } from "./background-manager"
+import { sleep } from "../utils/polling"
 
 
 // Mock the plugin context
 // Mock the plugin context
 function createMockContext(overrides?: {
 function createMockContext(overrides?: {
@@ -130,7 +131,7 @@ describe("BackgroundTaskManager", () => {
       })
       })
 
 
       // Wait a bit for polling to complete the task
       // Wait a bit for polling to complete the task
-      await new Promise(r => setTimeout(r, 100))
+      await sleep(100)
 
 
       const result = await manager.getResult(task.id, true)
       const result = await manager.getResult(task.id, true)
       expect(result).toBeDefined()
       expect(result).toBeDefined()

+ 3 - 2
src/features/background-manager.ts

@@ -3,6 +3,7 @@ import { POLL_INTERVAL_BACKGROUND_MS, POLL_INTERVAL_SLOW_MS } from "../config";
 import type { TmuxConfig } from "../config/schema";
 import type { TmuxConfig } from "../config/schema";
 import type { PluginConfig } from "../config";
 import type { PluginConfig } from "../config";
 import { applyAgentVariant, resolveAgentVariant } from "../utils";
 import { applyAgentVariant, resolveAgentVariant } from "../utils";
+import { sleep } from "../utils/polling";
 import { log } from "../shared/logger";
 import { log } from "../shared/logger";
 type PromptBody = {
 type PromptBody = {
   messageID?: string;
   messageID?: string;
@@ -84,7 +85,7 @@ export class BackgroundTaskManager {
     // Give TmuxSessionManager time to spawn the pane via event hook
     // Give TmuxSessionManager time to spawn the pane via event hook
     // before we send the prompt (so the TUI can receive streaming updates)
     // before we send the prompt (so the TUI can receive streaming updates)
     if (this.tmuxEnabled) {
     if (this.tmuxEnabled) {
-      await new Promise((r) => setTimeout(r, 500));
+      await sleep(500);
     }
     }
 
 
     const promptQuery: Record<string, string> = {
     const promptQuery: Record<string, string> = {
@@ -136,7 +137,7 @@ export class BackgroundTaskManager {
       if (status === "completed" || status === "failed") {
       if (status === "completed" || status === "failed") {
         return task;
         return task;
       }
       }
-      await new Promise((r) => setTimeout(r, POLL_INTERVAL_SLOW_MS));
+      await sleep(POLL_INTERVAL_SLOW_MS);
     }
     }
 
 
     return task;
     return task;

+ 8 - 7
src/hooks/auto-update-checker/checker.ts

@@ -176,7 +176,7 @@ export function getCachedVersion(): string | null {
       if (pkg.version) return pkg.version
       if (pkg.version) return pkg.version
     }
     }
   } catch (err) {
   } catch (err) {
-    log("[auto-update-checker] Failed to resolve version from current directory:", err)
+    log("[auto-update] check: Failed to resolve version from current directory:", err instanceof Error ? err.message : String(err))
   }
   }
 
 
   return null
   return null
@@ -189,7 +189,7 @@ export function updatePinnedVersion(configPath: string, oldEntry: string, newVer
     
     
     const pluginMatch = content.match(/"plugin"\s*:\s*\[/)
     const pluginMatch = content.match(/"plugin"\s*:\s*\[/)
     if (!pluginMatch || pluginMatch.index === undefined) {
     if (!pluginMatch || pluginMatch.index === undefined) {
-      log(`[auto-update-checker] No "plugin" array found in ${configPath}`)
+      log(`[auto-update] update: No "plugin" array found in ${configPath}`)
       return false
       return false
     }
     }
     
     
@@ -211,7 +211,7 @@ export function updatePinnedVersion(configPath: string, oldEntry: string, newVer
     const regex = new RegExp(`["']${escapedOldEntry}["']`)
     const regex = new RegExp(`["']${escapedOldEntry}["']`)
     
     
     if (!regex.test(pluginArrayContent)) {
     if (!regex.test(pluginArrayContent)) {
-      log(`[auto-update-checker] Entry "${oldEntry}" not found in plugin array of ${configPath}`)
+      log(`[auto-update] update: Entry "${oldEntry}" not found in plugin array of ${configPath}`)
       return false
       return false
     }
     }
     
     
@@ -219,15 +219,15 @@ export function updatePinnedVersion(configPath: string, oldEntry: string, newVer
     const updatedContent = before + updatedPluginArray + after
     const updatedContent = before + updatedPluginArray + after
     
     
     if (updatedContent === content) {
     if (updatedContent === content) {
-      log(`[auto-update-checker] No changes made to ${configPath}`)
+      log(`[auto-update] update: No changes made to ${configPath}`)
       return false
       return false
     }
     }
     
     
     fs.writeFileSync(configPath, updatedContent, "utf-8")
     fs.writeFileSync(configPath, updatedContent, "utf-8")
-    log(`[auto-update-checker] Updated ${configPath}: ${oldEntry} → ${newEntry}`)
+    log(`[auto-update] update: Updated ${configPath}: ${oldEntry} → ${newEntry}`)
     return true
     return true
   } catch (err) {
   } catch (err) {
-    log(`[auto-update-checker] Failed to update config file ${configPath}:`, err)
+    log(`[auto-update] update: Failed to update config file ${configPath}:`, err instanceof Error ? err.message : String(err))
     return false
     return false
   }
   }
 }
 }
@@ -246,7 +246,8 @@ export async function getLatestVersion(channel: string = "latest"): Promise<stri
 
 
     const data = (await response.json()) as NpmDistTags
     const data = (await response.json()) as NpmDistTags
     return data[channel] ?? data.latest ?? null
     return data[channel] ?? data.latest ?? null
-  } catch {
+  } catch (err) {
+    log(`[auto-update] fetch: failed to fetch latest version: ${err instanceof Error ? err.message : String(err)}`)
     return null
     return null
   } finally {
   } finally {
     clearTimeout(timeoutId)
     clearTimeout(timeoutId)

+ 29 - 8
src/tools/ast-grep/cli.ts

@@ -24,6 +24,10 @@ export interface RunOptions {
 // Use a single init promise to avoid race conditions
 // Use a single init promise to avoid race conditions
 let initPromise: Promise<string | null> | null = null
 let initPromise: Promise<string | null> | null = null
 
 
+/**
+ * Resolves the path to the ast-grep binary, downloading it if necessary.
+ * @returns A promise resolving to the binary path, or null if it couldn't be found/downloaded.
+ */
 export async function getAstGrepPath(): Promise<string | null> {
 export async function getAstGrepPath(): Promise<string | null> {
   const currentPath = getSgCliPath()
   const currentPath = getSgCliPath()
   if (currentPath !== "sg" && existsSync(currentPath)) {
   if (currentPath !== "sg" && existsSync(currentPath)) {
@@ -53,6 +57,9 @@ export async function getAstGrepPath(): Promise<string | null> {
   return initPromise
   return initPromise
 }
 }
 
 
+/**
+ * Starts background initialization of the ast-grep binary.
+ */
 export function startBackgroundInit(): void {
 export function startBackgroundInit(): void {
   if (!initPromise) {
   if (!initPromise) {
     initPromise = getAstGrepPath()
     initPromise = getAstGrepPath()
@@ -60,6 +67,11 @@ export function startBackgroundInit(): void {
   }
   }
 }
 }
 
 
+/**
+ * Runs an ast-grep search or rewrite with the provided options.
+ * @param options - ast-grep configuration.
+ * @returns A promise resolving to the search/rewrite results.
+ */
 export async function runSg(options: RunOptions): Promise<SgResult> {
 export async function runSg(options: RunOptions): Promise<SgResult> {
   const args = ["run", "-p", options.pattern, "--lang", options.lang, "--json=compact"]
   const args = ["run", "-p", options.pattern, "--lang", options.lang, "--json=compact"]
 
 
@@ -99,12 +111,13 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
     stderr: "pipe",
     stderr: "pipe",
   })
   })
 
 
-  const timeoutPromise = new Promise<never>((_, reject) => {
+  const timeoutPromise = new Promise<never>(async (_, reject) => {
     const id = setTimeout(() => {
     const id = setTimeout(() => {
       proc.kill()
       proc.kill()
-      reject(new Error(`Search timeout after ${timeout}ms`))
+      reject(new Error(`[ast-grep] run: Search timeout after ${timeout}ms`))
     }, timeout)
     }, timeout)
-    proc.exited.then(() => clearTimeout(id))
+    await proc.exited
+    clearTimeout(id)
   })
   })
 
 
   let stdout: string
   let stdout: string
@@ -123,7 +136,7 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
         totalMatches: 0,
         totalMatches: 0,
         truncated: true,
         truncated: true,
         truncatedReason: "timeout",
         truncatedReason: "timeout",
-        error: error.message,
+        error: `[ast-grep] run: ${error.message}`,
       }
       }
     }
     }
 
 
@@ -143,7 +156,7 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
           totalMatches: 0,
           totalMatches: 0,
           truncated: false,
           truncated: false,
           error:
           error:
-            `ast-grep CLI binary not found.\n\n` +
+            `[ast-grep] run: ast-grep CLI binary not found.\n\n` +
             `Auto-download failed. Manual install options:\n` +
             `Auto-download failed. Manual install options:\n` +
             `  bun add -D @ast-grep/cli\n` +
             `  bun add -D @ast-grep/cli\n` +
             `  cargo install ast-grep --locked\n` +
             `  cargo install ast-grep --locked\n` +
@@ -156,7 +169,7 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
       matches: [],
       matches: [],
       totalMatches: 0,
       totalMatches: 0,
       truncated: false,
       truncated: false,
-      error: `Failed to spawn ast-grep: ${error.message}`,
+      error: `[ast-grep] run: Failed to spawn ast-grep: ${error.message}`,
     }
     }
   }
   }
 
 
@@ -165,7 +178,7 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
       return { matches: [], totalMatches: 0, truncated: false }
       return { matches: [], totalMatches: 0, truncated: false }
     }
     }
     if (stderr.trim()) {
     if (stderr.trim()) {
-      return { matches: [], totalMatches: 0, truncated: false, error: stderr.trim() }
+      return { matches: [], totalMatches: 0, truncated: false, error: `[ast-grep] run: ${stderr.trim()}` }
     }
     }
     return { matches: [], totalMatches: 0, truncated: false }
     return { matches: [], totalMatches: 0, truncated: false }
   }
   }
@@ -197,7 +210,7 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
           totalMatches: 0,
           totalMatches: 0,
           truncated: true,
           truncated: true,
           truncatedReason: "max_output_bytes",
           truncatedReason: "max_output_bytes",
-          error: "Output too large and could not be parsed",
+          error: "[ast-grep] run: Output too large and could not be parsed",
         }
         }
       }
       }
     } else {
     } else {
@@ -221,11 +234,19 @@ export async function runSg(options: RunOptions): Promise<SgResult> {
   }
   }
 }
 }
 
 
+/**
+ * Checks if the ast-grep CLI is available on the system.
+ * @returns True if the CLI is available, false otherwise.
+ */
 export function isCliAvailable(): boolean {
 export function isCliAvailable(): boolean {
   const path = findSgCliPathSync()
   const path = findSgCliPathSync()
   return path !== null && existsSync(path)
   return path !== null && existsSync(path)
 }
 }
 
 
+/**
+ * Ensures that the ast-grep CLI is available, downloading it if necessary.
+ * @returns A promise resolving to true if available, false otherwise.
+ */
 export async function ensureCliAvailable(): Promise<boolean> {
 export async function ensureCliAvailable(): Promise<boolean> {
   const path = await getAstGrepPath()
   const path = await getAstGrepPath()
   return path !== null && existsSync(path)
   return path !== null && existsSync(path)

+ 14 - 59
src/tools/ast-grep/downloader.ts

@@ -1,8 +1,8 @@
-import { existsSync, mkdirSync, chmodSync, unlinkSync } from "node:fs"
+import { existsSync } from "node:fs"
 import { join } from "node:path"
 import { join } from "node:path"
-import { homedir } from "node:os"
 import { createRequire } from "node:module"
 import { createRequire } from "node:module"
-import { extractZip } from "../../shared"
+import { getDefaultInstallDir, ensureBinary } from "../shared/downloader-utils"
+export { getDefaultInstallDir as getCacheDir } from "../shared/downloader-utils"
 
 
 const REPO = "ast-grep/ast-grep"
 const REPO = "ast-grep/ast-grep"
 
 
@@ -35,24 +35,12 @@ const PLATFORM_MAP: Record<string, PlatformInfo> = {
   "win32-ia32": { arch: "i686", os: "pc-windows-msvc" },
   "win32-ia32": { arch: "i686", os: "pc-windows-msvc" },
 }
 }
 
 
-export function getCacheDir(): string {
-  if (process.platform === "win32") {
-    const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA
-    const base = localAppData || join(homedir(), "AppData", "Local")
-    return join(base, "oh-my-opencode-slim", "bin")
-  }
-
-  const xdgCache = process.env.XDG_CACHE_HOME
-  const base = xdgCache || join(homedir(), ".cache")
-  return join(base, "oh-my-opencode-slim", "bin")
-}
-
 export function getBinaryName(): string {
 export function getBinaryName(): string {
   return process.platform === "win32" ? "sg.exe" : "sg"
   return process.platform === "win32" ? "sg.exe" : "sg"
 }
 }
 
 
 export function getCachedBinaryPath(): string | null {
 export function getCachedBinaryPath(): string | null {
-  const binaryPath = join(getCacheDir(), getBinaryName())
+  const binaryPath = join(getDefaultInstallDir(), getBinaryName())
   return existsSync(binaryPath) ? binaryPath : null
   return existsSync(binaryPath) ? binaryPath : null
 }
 }
 
 
@@ -61,56 +49,23 @@ export async function downloadAstGrep(version: string = DEFAULT_VERSION): Promis
   const platformInfo = PLATFORM_MAP[platformKey]
   const platformInfo = PLATFORM_MAP[platformKey]
 
 
   if (!platformInfo) {
   if (!platformInfo) {
-    console.error(`[oh-my-opencode-slim] Unsupported platform for ast-grep: ${platformKey}`)
     return null
     return null
   }
   }
 
 
-  const cacheDir = getCacheDir()
-  const binaryName = getBinaryName()
-  const binaryPath = join(cacheDir, binaryName)
-
-  if (existsSync(binaryPath)) {
-    return binaryPath
-  }
-
   const { arch, os } = platformInfo
   const { arch, os } = platformInfo
   const assetName = `app-${arch}-${os}.zip`
   const assetName = `app-${arch}-${os}.zip`
-  const downloadUrl = `https://github.com/${REPO}/releases/download/${version}/${assetName}`
-
-  console.log(`[oh-my-opencode-slim] Downloading ast-grep binary...`)
+  const url = `https://github.com/${REPO}/releases/download/${version}/${assetName}`
 
 
   try {
   try {
-    if (!existsSync(cacheDir)) {
-      mkdirSync(cacheDir, { recursive: true })
-    }
-
-    const response = await fetch(downloadUrl, { redirect: "follow" })
-
-    if (!response.ok) {
-      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
-    }
-
-    const archivePath = join(cacheDir, assetName)
-    const arrayBuffer = await response.arrayBuffer()
-    await Bun.write(archivePath, arrayBuffer)
-
-    await extractZip(archivePath, cacheDir)
-
-    if (existsSync(archivePath)) {
-      unlinkSync(archivePath)
-    }
-
-    if (process.platform !== "win32" && existsSync(binaryPath)) {
-      chmodSync(binaryPath, 0o755)
-    }
-
-    console.log(`[oh-my-opencode-slim] ast-grep binary ready.`)
-
-    return binaryPath
-  } catch (err) {
-    console.error(
-      `[oh-my-opencode-slim] Failed to download ast-grep: ${err instanceof Error ? err.message : err}`
-    )
+    return await ensureBinary({
+      binaryName: "sg",
+      version,
+      url,
+      installDir: getDefaultInstallDir(),
+      archiveName: assetName,
+      isZip: true,
+    })
+  } catch {
     return null
     return null
   }
   }
 }
 }

+ 17 - 0
src/tools/ast-grep/utils.ts

@@ -1,5 +1,10 @@
 import type { SgResult, CliLanguage } from "./types"
 import type { SgResult, CliLanguage } from "./types"
 
 
+/**
+ * Formats an ast-grep search result into a human-readable summary.
+ * @param result - The SgResult containing matches or error.
+ * @returns A formatted string summary.
+ */
 export function formatSearchResult(result: SgResult): string {
 export function formatSearchResult(result: SgResult): string {
   if (result.error) {
   if (result.error) {
     return `Error: ${result.error}`
     return `Error: ${result.error}`
@@ -39,6 +44,12 @@ export function formatSearchResult(result: SgResult): string {
   return lines.join("\n")
   return lines.join("\n")
 }
 }
 
 
+/**
+ * Formats an ast-grep replacement result into a human-readable summary.
+ * @param result - The SgResult containing matches/replacements.
+ * @param isDryRun - Whether this was a dry run.
+ * @returns A formatted string summary.
+ */
 export function formatReplaceResult(result: SgResult, isDryRun: boolean): string {
 export function formatReplaceResult(result: SgResult, isDryRun: boolean): string {
   if (result.error) {
   if (result.error) {
     return `Error: ${result.error}`
     return `Error: ${result.error}`
@@ -83,6 +94,12 @@ export function formatReplaceResult(result: SgResult, isDryRun: boolean): string
   return lines.join("\n")
   return lines.join("\n")
 }
 }
 
 
+/**
+ * Provides helpful hints for common mistakes when an ast-grep search returns no results.
+ * @param pattern - The pattern that was searched.
+ * @param lang - The language used for the search.
+ * @returns A hint string if a common mistake is detected, otherwise null.
+ */
 export function getEmptyResultHint(pattern: string, lang: CliLanguage): string | null {
 export function getEmptyResultHint(pattern: string, lang: CliLanguage): string | null {
   const src = pattern.trim()
   const src = pattern.trim()
 
 

+ 121 - 64
src/tools/background.ts

@@ -10,6 +10,7 @@ import {
 import type { TmuxConfig } from "../config/schema";
 import type { TmuxConfig } from "../config/schema";
 import type { PluginConfig } from "../config";
 import type { PluginConfig } from "../config";
 import { applyAgentVariant, resolveAgentVariant } from "../utils";
 import { applyAgentVariant, resolveAgentVariant } from "../utils";
+import { sleep } from "../utils/polling";
 import { log } from "../shared/logger";
 import { log } from "../shared/logger";
 
 
 const z = tool.schema;
 const z = tool.schema;
@@ -21,6 +22,14 @@ type ToolContext = {
   abort: AbortSignal;
   abort: AbortSignal;
 };
 };
 
 
+/**
+ * Creates background task tools (background_task, background_output, background_cancel).
+ * @param ctx - The plugin input context.
+ * @param manager - The background task manager instance.
+ * @param tmuxConfig - Optional tmux configuration for spawning panes.
+ * @param pluginConfig - Optional plugin configuration for agent variants.
+ * @returns A record of tool definitions.
+ */
 export function createBackgroundTools(
 export function createBackgroundTools(
   ctx: PluginInput,
   ctx: PluginInput,
   manager: BackgroundTaskManager,
   manager: BackgroundTaskManager,
@@ -85,7 +94,7 @@ Use \`background_output\` with task_id="${task.id}" to get results.`;
 
 
       const task = await manager.getResult(taskId, block, timeout);
       const task = await manager.getResult(taskId, block, timeout);
       if (!task) {
       if (!task) {
-        return `Task not found: ${taskId}`;
+        return `Error: [background] task not found: ${taskId}`;
       }
       }
 
 
       const duration = task.completedAt
       const duration = task.completedAt
@@ -147,37 +156,11 @@ async function executeSync(
   pluginConfig?: PluginConfig,
   pluginConfig?: PluginConfig,
   existingSessionId?: string
   existingSessionId?: string
 ): Promise<string> {
 ): Promise<string> {
-  let sessionID: string;
-
-  if (existingSessionId) {
-    const sessionResult = await ctx.client.session.get({ path: { id: existingSessionId } });
-    if (sessionResult.error) {
-      return `Error: Failed to get session: ${sessionResult.error}`;
-    }
-    sessionID = existingSessionId;
-  } else {
-    const parentSession = await ctx.client.session.get({ path: { id: toolContext.sessionID } }).catch(() => null);
-    const parentDirectory = parentSession?.data?.directory ?? ctx.directory;
-
-    const createResult = await ctx.client.session.create({
-      body: {
-        parentID: toolContext.sessionID,
-        title: `${description} (@${agent})`,
-      },
-      query: { directory: parentDirectory },
-    });
-
-    if (createResult.error) {
-      return `Error: Failed to create session: ${createResult.error}`;
-    }
-    sessionID = createResult.data.id;
-
-    // Give TmuxSessionManager time to spawn the pane via event hook
-    // before we send the prompt (so the TUI can receive streaming updates)
-    if (tmuxConfig?.enabled) {
-      await new Promise((r) => setTimeout(r, 500));
-    }
+  const sessionResult = await getOrCreateSession(description, agent, toolContext, ctx, tmuxConfig, existingSessionId);
+  if ("error" in sessionResult) {
+    return sessionResult.error;
   }
   }
+  const sessionID = sessionResult.sessionID;
 
 
   // Disable recursive delegation tools to prevent infinite loops
   // Disable recursive delegation tools to prevent infinite loops
   log(`[background-sync] launching sync task for agent="${agent}"`, { description });
   log(`[background-sync] launching sync task for agent="${agent}"`, { description });
@@ -203,27 +186,116 @@ async function executeSync(
       body: promptBody,
       body: promptBody,
     });
     });
   } catch (error) {
   } catch (error) {
-    return `Error: Failed to send prompt: ${error instanceof Error ? error.message : String(error)}
+    return `Error: [background] executeSync: failed to send prompt: ${error instanceof Error ? error.message : String(error)}
+
+<task_metadata>
+session_id: ${sessionID}
+</task_metadata>`;
+  }
+
+  const pollStatus = await pollForCompletion(sessionID, toolContext, ctx);
+  if (pollStatus === "aborted") {
+    return `Task aborted.
+
+<task_metadata>
+session_id: ${sessionID}
+</task_metadata>`;
+  } else if (pollStatus === "timeout") {
+    return `Error: [background] executeSync: agent timed out after 5 minutes.
+
+<task_metadata>
+session_id: ${sessionID}
+</task_metadata>`;
+  }
+
+  const messagesResult = await ctx.client.session.messages({ path: { id: sessionID } });
+  if (messagesResult.error) {
+    return `Error: [background] executeSync: failed to get messages: ${messagesResult.error}`;
+  }
+
+  const responseText = extractAssistantResponse(
+    messagesResult.data as Array<{ info?: { role: string }; parts?: Array<{ type: string; text?: string }> }>
+  );
+
+  if (responseText === null) {
+    return `Error: [background] executeSync: no response from agent.
+
+<task_metadata>
+session_id: ${sessionID}
+</task_metadata>`;
+  }
+
+  // Pane closing is handled by TmuxSessionManager via polling
+  return `${responseText}
 
 
 <task_metadata>
 <task_metadata>
 session_id: ${sessionID}
 session_id: ${sessionID}
 </task_metadata>`;
 </task_metadata>`;
+}
+
+/**
+ * Retrieves an existing session or creates a new one for background task execution.
+ */
+async function getOrCreateSession(
+  description: string,
+  agent: string,
+  toolContext: ToolContext,
+  ctx: PluginInput,
+  tmuxConfig?: TmuxConfig,
+  existingSessionId?: string
+): Promise<{ sessionID: string } | { error: string }> {
+  if (existingSessionId) {
+    const sessionResult = await ctx.client.session.get({ path: { id: existingSessionId } });
+    if (sessionResult.error) {
+      return { error: `Error: [background] executeSync: failed to get session: ${sessionResult.error}` };
+    }
+    return { sessionID: existingSessionId };
+  }
+
+  const parentSession = await ctx.client.session.get({ path: { id: toolContext.sessionID } }).catch(() => null);
+  const parentDirectory = parentSession?.data?.directory ?? ctx.directory;
+
+  const createResult = await ctx.client.session.create({
+    body: {
+      parentID: toolContext.sessionID,
+      title: `${description} (@${agent})`,
+    },
+    query: { directory: parentDirectory },
+  });
+
+  if (createResult.error) {
+    return { error: `Error: [background] executeSync: failed to create session: ${createResult.error}` };
+  }
+
+  const sessionID = createResult.data.id;
+
+  // Give TmuxSessionManager time to spawn the pane via event hook
+  // before we send the prompt (so the TUI can receive streaming updates)
+  if (tmuxConfig?.enabled) {
+    await sleep(500);
   }
   }
 
 
+  return { sessionID };
+}
+
+/**
+ * Polls for the completion of a session's execution.
+ */
+async function pollForCompletion(
+  sessionID: string,
+  toolContext: ToolContext,
+  ctx: PluginInput
+): Promise<"completed" | "aborted" | "timeout"> {
   const pollStart = Date.now();
   const pollStart = Date.now();
   let lastMsgCount = 0;
   let lastMsgCount = 0;
   let stablePolls = 0;
   let stablePolls = 0;
 
 
   while (Date.now() - pollStart < MAX_POLL_TIME_MS) {
   while (Date.now() - pollStart < MAX_POLL_TIME_MS) {
     if (toolContext.abort?.aborted) {
     if (toolContext.abort?.aborted) {
-      return `Task aborted.
-
-<task_metadata>
-session_id: ${sessionID}
-</task_metadata>`;
+      return "aborted";
     }
     }
 
 
-    await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
+    await sleep(POLL_INTERVAL_MS);
 
 
     const statusResult = await ctx.client.session.status();
     const statusResult = await ctx.client.session.status();
     const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>;
     const allStatuses = (statusResult.data ?? {}) as Record<string, { type: string }>;
@@ -241,35 +313,26 @@ session_id: ${sessionID}
 
 
     if (currentMsgCount > 0 && currentMsgCount === lastMsgCount) {
     if (currentMsgCount > 0 && currentMsgCount === lastMsgCount) {
       stablePolls++;
       stablePolls++;
-      if (stablePolls >= STABLE_POLLS_THRESHOLD) break;
+      if (stablePolls >= STABLE_POLLS_THRESHOLD) return "completed";
     } else {
     } else {
       stablePolls = 0;
       stablePolls = 0;
       lastMsgCount = currentMsgCount;
       lastMsgCount = currentMsgCount;
     }
     }
   }
   }
 
 
-  if (Date.now() - pollStart >= MAX_POLL_TIME_MS) {
-    return `Error: Agent timed out after 5 minutes.
-
-<task_metadata>
-session_id: ${sessionID}
-</task_metadata>`;
-  }
-
-  const messagesResult = await ctx.client.session.messages({ path: { id: sessionID } });
-  if (messagesResult.error) {
-    return `Error: Failed to get messages: ${messagesResult.error}`;
-  }
+  return "timeout";
+}
 
 
-  const messages = messagesResult.data as Array<{ info?: { role: string }; parts?: Array<{ type: string; text?: string }> }>;
+/**
+ * Extracts the assistant's response text from the session messages.
+ */
+function extractAssistantResponse(
+  messages: Array<{ info?: { role: string }; parts?: Array<{ type: string; text?: string }> }>
+): string | null {
   const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
   const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
 
 
   if (assistantMessages.length === 0) {
   if (assistantMessages.length === 0) {
-    return `Error: No response from agent.
-
-<task_metadata>
-session_id: ${sessionID}
-</task_metadata>`;
+    return null;
   }
   }
 
 
   const extractedContent: string[] = [];
   const extractedContent: string[] = [];
@@ -282,11 +345,5 @@ session_id: ${sessionID}
   }
   }
 
 
   const responseText = extractedContent.filter((t) => t.length > 0).join("\n\n");
   const responseText = extractedContent.filter((t) => t.length > 0).join("\n\n");
-
-  // Pane closing is handled by TmuxSessionManager via polling
-  return `${responseText}
-
-<task_metadata>
-session_id: ${sessionID}
-</task_metadata>`;
+  return responseText.length > 0 ? responseText : null;
 }
 }

+ 23 - 10
src/tools/grep/cli.ts

@@ -129,6 +129,12 @@ function parseCountOutput(output: string): CountResult[] {
   return results
   return results
 }
 }
 
 
+/**
+ * Runs a ripgrep (or grep) search with the provided options.
+ * @param options - Grep search configuration.
+ * @returns A promise resolving to the search results.
+ * @throws Error if the search times out.
+ */
 export async function runRg(options: GrepOptions): Promise<GrepResult> {
 export async function runRg(options: GrepOptions): Promise<GrepResult> {
   const cli = resolveGrepCli()
   const cli = resolveGrepCli()
   const args = buildArgs(options, cli.backend)
   const args = buildArgs(options, cli.backend)
@@ -147,12 +153,13 @@ export async function runRg(options: GrepOptions): Promise<GrepResult> {
     stderr: "pipe",
     stderr: "pipe",
   })
   })
 
 
-  const timeoutPromise = new Promise<never>((_, reject) => {
+  const timeoutPromise = new Promise<never>(async (_, reject) => {
     const id = setTimeout(() => {
     const id = setTimeout(() => {
       proc.kill()
       proc.kill()
-      reject(new Error(`Search timeout after ${timeout}ms`))
+      reject(new Error(`[grep] run: Search timeout after ${timeout}ms`))
     }, timeout)
     }, timeout)
-    proc.exited.then(() => clearTimeout(id))
+    await proc.exited
+    clearTimeout(id)
   })
   })
 
 
   try {
   try {
@@ -169,7 +176,7 @@ export async function runRg(options: GrepOptions): Promise<GrepResult> {
         totalMatches: 0,
         totalMatches: 0,
         filesSearched: 0,
         filesSearched: 0,
         truncated: false,
         truncated: false,
-        error: stderr.trim(),
+        error: `[grep] run: ${stderr.trim()}`,
       }
       }
     }
     }
 
 
@@ -188,11 +195,16 @@ export async function runRg(options: GrepOptions): Promise<GrepResult> {
       totalMatches: 0,
       totalMatches: 0,
       filesSearched: 0,
       filesSearched: 0,
       truncated: false,
       truncated: false,
-      error: e instanceof Error ? e.message : String(e),
+      error: `[grep] run: ${e instanceof Error ? e.message : String(e)}`,
     }
     }
   }
   }
 }
 }
-
+/**
+ * Runs a ripgrep (or grep) count search with the provided options.
+ * @param options - Grep search configuration (context ignored).
+ * @returns A promise resolving to an array of per-file match counts.
+ * @throws Error if the search fails or times out.
+ */
 export async function runRgCount(options: Omit<GrepOptions, "context">): Promise<CountResult[]> {
 export async function runRgCount(options: Omit<GrepOptions, "context">): Promise<CountResult[]> {
   const cli = resolveGrepCli()
   const cli = resolveGrepCli()
   const args = buildArgs({ ...options, context: 0 }, cli.backend)
   const args = buildArgs({ ...options, context: 0 }, cli.backend)
@@ -212,18 +224,19 @@ export async function runRgCount(options: Omit<GrepOptions, "context">): Promise
     stderr: "pipe",
     stderr: "pipe",
   })
   })
 
 
-  const timeoutPromise = new Promise<never>((_, reject) => {
+  const timeoutPromise = new Promise<never>(async (_, reject) => {
     const id = setTimeout(() => {
     const id = setTimeout(() => {
       proc.kill()
       proc.kill()
-      reject(new Error(`Search timeout after ${timeout}ms`))
+      reject(new Error(`[grep] run: Search timeout after ${timeout}ms`))
     }, timeout)
     }, timeout)
-    proc.exited.then(() => clearTimeout(id))
+    await proc.exited
+    clearTimeout(id)
   })
   })
 
 
   try {
   try {
     const stdout = await Promise.race([new Response(proc.stdout).text(), timeoutPromise])
     const stdout = await Promise.race([new Response(proc.stdout).text(), timeoutPromise])
     return parseCountOutput(stdout)
     return parseCountOutput(stdout)
   } catch (e) {
   } catch (e) {
-    throw new Error(`Count search failed: ${e instanceof Error ? e.message : String(e)}`)
+    throw new Error(`[grep] run: Count search failed: ${e instanceof Error ? e.message : String(e)}`)
   }
   }
 }
 }

+ 14 - 110
src/tools/grep/downloader.ts

@@ -1,25 +1,10 @@
-import { existsSync, mkdirSync, chmodSync, unlinkSync, readdirSync } from "node:fs"
+import { existsSync } from "node:fs"
 import { join } from "node:path"
 import { join } from "node:path"
-import { spawn } from "bun"
-import { extractZip } from "../../shared"
-
-export function findFileRecursive(dir: string, filename: string): string | null {
-  try {
-    const entries = readdirSync(dir, { withFileTypes: true, recursive: true })
-    for (const entry of entries) {
-      if (entry.isFile() && entry.name === filename) {
-        return join(entry.parentPath ?? dir, entry.name)
-      }
-    }
-  } catch {
-    return null
-  }
-  return null
-}
+import { getDefaultInstallDir, ensureBinary } from "../shared/downloader-utils"
 
 
 const RG_VERSION = "14.1.1"
 const RG_VERSION = "14.1.1"
 
 
-// Platform key format: ${process.platform}-${process.arch} (consistent with ast-grep)
+// Platform key format: ${process.platform}-${process.arch}
 const PLATFORM_CONFIG: Record<string, { platform: string; extension: "tar.gz" | "zip" } | undefined> =
 const PLATFORM_CONFIG: Record<string, { platform: string; extension: "tar.gz" | "zip" } | undefined> =
   {
   {
     "darwin-arm64": { platform: "aarch64-apple-darwin", extension: "tar.gz" },
     "darwin-arm64": { platform: "aarch64-apple-darwin", extension: "tar.gz" },
@@ -33,60 +18,9 @@ function getPlatformKey(): string {
   return `${process.platform}-${process.arch}`
   return `${process.platform}-${process.arch}`
 }
 }
 
 
-function getInstallDir(): string {
-  const homeDir = process.env.HOME || process.env.USERPROFILE || "."
-  return join(homeDir, ".cache", "oh-my-opencode-slim", "bin")
-}
-
 function getRgPath(): string {
 function getRgPath(): string {
   const isWindows = process.platform === "win32"
   const isWindows = process.platform === "win32"
-  return join(getInstallDir(), isWindows ? "rg.exe" : "rg")
-}
-
-async function downloadFile(url: string, destPath: string): Promise<void> {
-  const response = await fetch(url)
-  if (!response.ok) {
-    throw new Error(`Failed to download: ${response.status} ${response.statusText}`)
-  }
-
-  const buffer = await response.arrayBuffer()
-  await Bun.write(destPath, buffer)
-}
-
-async function extractTarGz(archivePath: string, destDir: string): Promise<void> {
-  const args = ["tar", "-xzf", archivePath, "--strip-components=1"]
-
-  if (process.platform === "darwin") {
-    args.push("--include=*/rg")
-  } else if (process.platform === "linux") {
-    args.push("--wildcards", "*/rg")
-  }
-
-  const proc = spawn(args, {
-    cwd: destDir,
-    stdout: "pipe",
-    stderr: "pipe",
-  })
-
-  const exitCode = await proc.exited
-  if (exitCode !== 0) {
-    const stderr = await new Response(proc.stderr).text()
-    throw new Error(`Failed to extract tar.gz: ${stderr}`)
-  }
-}
-
-async function extractZipArchive(archivePath: string, destDir: string): Promise<void> {
-  await extractZip(archivePath, destDir)
-
-  const binaryName = process.platform === "win32" ? "rg.exe" : "rg"
-  const foundPath = findFileRecursive(destDir, binaryName)
-  if (foundPath) {
-    const destPath = join(destDir, binaryName)
-    if (foundPath !== destPath) {
-      const { renameSync } = await import("node:fs")
-      renameSync(foundPath, destPath)
-    }
-  }
+  return join(getDefaultInstallDir(), isWindows ? "rg.exe" : "rg")
 }
 }
 
 
 export async function downloadAndInstallRipgrep(): Promise<string> {
 export async function downloadAndInstallRipgrep(): Promise<string> {
@@ -94,51 +28,21 @@ export async function downloadAndInstallRipgrep(): Promise<string> {
   const config = PLATFORM_CONFIG[platformKey]
   const config = PLATFORM_CONFIG[platformKey]
 
 
   if (!config) {
   if (!config) {
-    throw new Error(`Unsupported platform: ${platformKey}`)
-  }
-
-  const installDir = getInstallDir()
-  const rgPath = getRgPath()
-
-  if (existsSync(rgPath)) {
-    return rgPath
+    throw new Error(`[grep] download: Unsupported platform: ${platformKey}`)
   }
   }
 
 
-  mkdirSync(installDir, { recursive: true })
-
   const filename = `ripgrep-${RG_VERSION}-${config.platform}.${config.extension}`
   const filename = `ripgrep-${RG_VERSION}-${config.platform}.${config.extension}`
   const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${filename}`
   const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${filename}`
-  const archivePath = join(installDir, filename)
-
-  try {
-    console.log(`[oh-my-opencode-slim] Downloading ripgrep...`)
-    await downloadFile(url, archivePath)
-
-    if (config.extension === "tar.gz") {
-      await extractTarGz(archivePath, installDir)
-    } else {
-      await extractZipArchive(archivePath, installDir)
-    }
 
 
-    if (process.platform !== "win32") {
-      chmodSync(rgPath, 0o755)
-    }
-
-    if (!existsSync(rgPath)) {
-      throw new Error("ripgrep binary not found after extraction")
-    }
-
-    console.log(`[oh-my-opencode-slim] ripgrep ready.`)
-    return rgPath
-  } finally {
-    if (existsSync(archivePath)) {
-      try {
-        unlinkSync(archivePath)
-      } catch {
-        // Cleanup failures are non-critical
-      }
-    }
-  }
+  return ensureBinary({
+    binaryName: "rg",
+    version: RG_VERSION,
+    url,
+    installDir: getDefaultInstallDir(),
+    archiveName: filename,
+    isZip: config.extension === "zip",
+    tarInclude: "*/rg",
+  })
 }
 }
 
 
 export function getInstalledRipgrepPath(): string | null {
 export function getInstalledRipgrepPath(): string | null {

+ 81 - 93
src/tools/lsp/client.ts

@@ -6,6 +6,8 @@ import { extname, resolve } from "path"
 import { pathToFileURL } from "node:url"
 import { pathToFileURL } from "node:url"
 import { getLanguageId } from "./config"
 import { getLanguageId } from "./config"
 import type { Diagnostic, ResolvedServer } from "./types"
 import type { Diagnostic, ResolvedServer } from "./types"
+import { parseMessages } from "./protocol-parser"
+import { sleep } from "../../utils/polling"
 
 
 interface ManagedClient {
 interface ManagedClient {
   client: LSPClient
   client: LSPClient
@@ -119,7 +121,7 @@ class LSPServerManager {
       }
       }
     } catch (err) {
     } catch (err) {
       this.clients.delete(key)
       this.clients.delete(key)
-      throw err
+      throw new Error(`[lsp-client] getClient: ${err instanceof Error ? err.message : String(err)}`)
     }
     }
 
 
     return client
     return client
@@ -182,18 +184,19 @@ export class LSPClient {
     })
     })
 
 
     if (!this.proc) {
     if (!this.proc) {
-      throw new Error(`Failed to spawn LSP server: ${this.server.command.join(" ")}`)
+      throw new Error(`[lsp-client] start: Failed to spawn LSP server: ${this.server.command.join(" ")}`)
     }
     }
 
 
     this.startReading()
     this.startReading()
     this.startStderrReading()
     this.startStderrReading()
 
 
-    await new Promise((resolve) => setTimeout(resolve, 100))
+    await sleep(100)
 
 
     if (this.proc.exitCode !== null) {
     if (this.proc.exitCode !== null) {
       const stderr = this.stderrBuffer.join("\n")
       const stderr = this.stderrBuffer.join("\n")
       throw new Error(
       throw new Error(
-        `LSP server exited immediately with code ${this.proc.exitCode}` + (stderr ? `\nstderr: ${stderr}` : "")
+        `[lsp-client] start: LSP server exited immediately with code ${this.proc.exitCode}` +
+          (stderr ? `\nstderr: ${stderr}` : "")
       )
       )
     }
     }
   }
   }
@@ -208,7 +211,7 @@ export class LSPClient {
           const { done, value } = await reader.read()
           const { done, value } = await reader.read()
           if (done) {
           if (done) {
             this.processExited = true
             this.processExited = true
-            this.rejectAllPending("LSP server stdout closed")
+            this.rejectAllPending("[lsp-client] startReading: LSP server stdout closed")
             break
             break
           }
           }
           const newBuf = new Uint8Array(this.buffer.length + value.length)
           const newBuf = new Uint8Array(this.buffer.length + value.length)
@@ -219,7 +222,8 @@ export class LSPClient {
         }
         }
       } catch (err) {
       } catch (err) {
         this.processExited = true
         this.processExited = true
-        this.rejectAllPending(`LSP stdout read error: ${err}`)
+        const message = err instanceof Error ? err.message : String(err)
+        this.rejectAllPending(`[lsp-client] startReading: ${message}`)
       }
       }
     }
     }
     read()
     read()
@@ -253,50 +257,12 @@ export class LSPClient {
     }
     }
   }
   }
 
 
-  private findSequence(haystack: Uint8Array, needle: number[]): number {
-    outer: for (let i = 0; i <= haystack.length - needle.length; i++) {
-      for (let j = 0; j < needle.length; j++) {
-        if (haystack[i + j] !== needle[j]) continue outer
-      }
-      return i
-    }
-    return -1
-  }
-
   private processBuffer(): void {
   private processBuffer(): void {
-    const decoder = new TextDecoder()
-    const CONTENT_LENGTH = [67, 111, 110, 116, 101, 110, 116, 45, 76, 101, 110, 103, 116, 104, 58]
-    const CRLF_CRLF = [13, 10, 13, 10]
-    const LF_LF = [10, 10]
-
-    while (true) {
-      const headerStart = this.findSequence(this.buffer, CONTENT_LENGTH)
-      if (headerStart === -1) break
-      if (headerStart > 0) this.buffer = this.buffer.slice(headerStart)
-
-      let headerEnd = this.findSequence(this.buffer, CRLF_CRLF)
-      let sepLen = 4
-      if (headerEnd === -1) {
-        headerEnd = this.findSequence(this.buffer, LF_LF)
-        sepLen = 2
-      }
-      if (headerEnd === -1) break
-
-      const header = decoder.decode(this.buffer.slice(0, headerEnd))
-      const match = header.match(/Content-Length:\s*(\d+)/i)
-      if (!match) break
-
-      const len = parseInt(match[1], 10)
-      const start = headerEnd + sepLen
-      const end = start + len
-      if (this.buffer.length < end) break
-
-      const content = decoder.decode(this.buffer.slice(start, end))
-      this.buffer = this.buffer.slice(end)
+    const { messages, remainingBuffer } = parseMessages(this.buffer)
+    this.buffer = remainingBuffer
 
 
+    for (const msg of messages) {
       try {
       try {
-        const msg = JSON.parse(content)
-
         if ("method" in msg && !("id" in msg)) {
         if ("method" in msg && !("id" in msg)) {
           if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri) {
           if (msg.method === "textDocument/publishDiagnostics" && msg.params?.uri) {
             this.diagnosticsStore.set(msg.params.uri, msg.params.diagnostics ?? [])
             this.diagnosticsStore.set(msg.params.uri, msg.params.diagnostics ?? [])
@@ -307,21 +273,25 @@ export class LSPClient {
           const handler = this.pending.get(msg.id)!
           const handler = this.pending.get(msg.id)!
           this.pending.delete(msg.id)
           this.pending.delete(msg.id)
           if ("error" in msg) {
           if ("error" in msg) {
-            handler.reject(new Error(msg.error.message))
+            handler.reject(new Error(`[lsp-client] response: ${msg.error.message}`))
           } else {
           } else {
             handler.resolve(msg.result)
             handler.resolve(msg.result)
           }
           }
         }
         }
-      } catch {}
+      } catch (err) {
+        console.error(`[lsp-client] Error handling message: ${err instanceof Error ? err.message : String(err)}`)
+      }
     }
     }
   }
   }
 
 
   private send(method: string, params?: unknown): Promise<unknown> {
   private send(method: string, params?: unknown): Promise<unknown> {
-    if (!this.proc) throw new Error("LSP client not started")
+    if (!this.proc) throw new Error("[lsp-client] send: LSP client not started")
 
 
     if (this.processExited || this.proc.exitCode !== null) {
     if (this.processExited || this.proc.exitCode !== null) {
       const stderr = this.stderrBuffer.slice(-10).join("\n")
       const stderr = this.stderrBuffer.slice(-10).join("\n")
-      throw new Error(`LSP server already exited (code: ${this.proc.exitCode})` + (stderr ? `\nstderr: ${stderr}` : ""))
+      throw new Error(
+        `[lsp-client] send: LSP server already exited (code: ${this.proc.exitCode})` + (stderr ? `\nstderr: ${stderr}` : "")
+      )
     }
     }
 
 
     const id = ++this.requestIdCounter
     const id = ++this.requestIdCounter
@@ -335,18 +305,28 @@ export class LSPClient {
         if (this.pending.has(id)) {
         if (this.pending.has(id)) {
           this.pending.delete(id)
           this.pending.delete(id)
           const stderr = this.stderrBuffer.slice(-5).join("\n")
           const stderr = this.stderrBuffer.slice(-5).join("\n")
-          reject(new Error(`LSP request timeout (method: ${method})` + (stderr ? `\nrecent stderr: ${stderr}` : "")))
+          reject(
+            new Error(
+              `[lsp-client] send: LSP request timeout (method: ${method})` + (stderr ? `\nrecent stderr: ${stderr}` : "")
+            )
+          )
         }
         }
       }, 15000)
       }, 15000)
     })
     })
   }
   }
 
 
   private notify(method: string, params?: unknown): void {
   private notify(method: string, params?: unknown): void {
-    if (!this.proc) return
-    if (this.processExited || this.proc.exitCode !== null) return
+    if (!this.proc) throw new Error("[lsp-client] notify: LSP client not started")
+    if (this.processExited || this.proc.exitCode !== null) {
+      throw new Error("[lsp-client] notify: LSP server already exited")
+    }
 
 
     const msg = JSON.stringify({ jsonrpc: "2.0", method, params })
     const msg = JSON.stringify({ jsonrpc: "2.0", method, params })
-    this.proc.stdin.write(`Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n${msg}`)
+    try {
+      this.proc.stdin.write(`Content-Length: ${Buffer.byteLength(msg)}\r\n\r\n${msg}`)
+    } catch (err) {
+      throw new Error(`[lsp-client] notify: ${err instanceof Error ? err.message : String(err)}`)
+    }
   }
   }
 
 
   private respond(id: number | string, result: unknown): void {
   private respond(id: number | string, result: unknown): void {
@@ -373,37 +353,41 @@ export class LSPClient {
   }
   }
 
 
   async initialize(): Promise<void> {
   async initialize(): Promise<void> {
-    const rootUri = pathToFileURL(this.root).href
-    await this.send("initialize", {
-      processId: process.pid,
-      rootUri,
-      rootPath: this.root,
-      workspaceFolders: [{ uri: rootUri, name: "workspace" }],
-      capabilities: {
-        textDocument: {
-          hover: { contentFormat: ["markdown", "plaintext"] },
-          definition: { linkSupport: true },
-          references: {},
-          documentSymbol: { hierarchicalDocumentSymbolSupport: true },
-          publishDiagnostics: {},
-          rename: {
-            prepareSupport: true,
-            prepareSupportDefaultBehavior: 1,
-            honorsChangeAnnotations: true,
+    try {
+      const rootUri = pathToFileURL(this.root).href
+      await this.send("initialize", {
+        processId: process.pid,
+        rootUri,
+        rootPath: this.root,
+        workspaceFolders: [{ uri: rootUri, name: "workspace" }],
+        capabilities: {
+          textDocument: {
+            hover: { contentFormat: ["markdown", "plaintext"] },
+            definition: { linkSupport: true },
+            references: {},
+            documentSymbol: { hierarchicalDocumentSymbolSupport: true },
+            publishDiagnostics: {},
+            rename: {
+              prepareSupport: true,
+              prepareSupportDefaultBehavior: 1,
+              honorsChangeAnnotations: true,
+            },
+          },
+          workspace: {
+            symbol: {},
+            workspaceFolders: true,
+            configuration: true,
+            applyEdit: true,
+            workspaceEdit: { documentChanges: true },
           },
           },
         },
         },
-        workspace: {
-          symbol: {},
-          workspaceFolders: true,
-          configuration: true,
-          applyEdit: true,
-          workspaceEdit: { documentChanges: true },
-        },
-      },
-      ...this.server.initialization,
-    })
-    this.notify("initialized")
-    await new Promise((r) => setTimeout(r, 300))
+        ...this.server.initialization,
+      })
+      this.notify("initialized")
+      await sleep(300)
+    } catch (err) {
+      throw new Error(`[lsp-client] initialize: ${err instanceof Error ? err.message : String(err)}`)
+    }
   }
   }
 
 
   async openFile(filePath: string): Promise<void> {
   async openFile(filePath: string): Promise<void> {
@@ -424,7 +408,7 @@ export class LSPClient {
     })
     })
     this.openedFiles.add(absPath)
     this.openedFiles.add(absPath)
 
 
-    await new Promise((r) => setTimeout(r, 1000))
+    await sleep(1000)
   }
   }
 
 
   async definition(filePath: string, line: number, character: number): Promise<unknown> {
   async definition(filePath: string, line: number, character: number): Promise<unknown> {
@@ -450,7 +434,7 @@ export class LSPClient {
     const absPath = resolve(filePath)
     const absPath = resolve(filePath)
     const uri = pathToFileURL(absPath).href
     const uri = pathToFileURL(absPath).href
     await this.openFile(absPath)
     await this.openFile(absPath)
-    await new Promise((r) => setTimeout(r, 500))
+    await sleep(500)
 
 
     try {
     try {
       const result = await this.send("textDocument/diagnostic", {
       const result = await this.send("textDocument/diagnostic", {
@@ -480,12 +464,16 @@ export class LSPClient {
 
 
   async stop(): Promise<void> {
   async stop(): Promise<void> {
     try {
     try {
-      this.notify("shutdown", {})
-      this.notify("exit")
-    } catch {}
-    this.proc?.kill()
-    this.proc = null
-    this.processExited = true
-    this.diagnosticsStore.clear()
+      try {
+        this.notify("shutdown", {})
+        this.notify("exit")
+      } catch {}
+      this.proc?.kill()
+      this.proc = null
+      this.processExited = true
+      this.diagnosticsStore.clear()
+    } catch (err) {
+      throw new Error(`[lsp-client] stop: ${err instanceof Error ? err.message : String(err)}`)
+    }
   }
   }
 }
 }

+ 16 - 0
src/tools/lsp/config.ts

@@ -6,6 +6,11 @@ import { homedir } from "os"
 import { BUILTIN_SERVERS, EXT_TO_LANG, LSP_INSTALL_HINTS } from "./constants"
 import { BUILTIN_SERVERS, EXT_TO_LANG, LSP_INSTALL_HINTS } from "./constants"
 import type { ResolvedServer, ServerLookupResult } from "./types"
 import type { ResolvedServer, ServerLookupResult } from "./types"
 
 
+/**
+ * Finds a suitable LSP server for a given file extension.
+ * @param ext - The file extension (including dot).
+ * @returns A result indicating if a server was found, not installed, or not configured.
+ */
 export function findServerForExtension(ext: string): ServerLookupResult {
 export function findServerForExtension(ext: string): ServerLookupResult {
   // Find matching server
   // Find matching server
   for (const [id, config] of Object.entries(BUILTIN_SERVERS)) {
   for (const [id, config] of Object.entries(BUILTIN_SERVERS)) {
@@ -33,10 +38,21 @@ export function findServerForExtension(ext: string): ServerLookupResult {
   return { status: "not_configured", extension: ext }
   return { status: "not_configured", extension: ext }
 }
 }
 
 
+/**
+ * Maps a file extension to its corresponding LSP language identifier.
+ * @param ext - The file extension (including dot).
+ * @returns The language identifier (e.g., 'typescript', 'python').
+ */
 export function getLanguageId(ext: string): string {
 export function getLanguageId(ext: string): string {
   return EXT_TO_LANG[ext] || "plaintext"
   return EXT_TO_LANG[ext] || "plaintext"
 }
 }
 
 
+/**
+ * Checks if an LSP server command is available on the system.
+ * Looks in PATH, local node_modules/.bin, and global opencode bin.
+ * @param command - The command array (e.g., ['typescript-language-server', '--stdio']).
+ * @returns True if the server is installed and executable.
+ */
 export function isServerInstalled(command: string[]): boolean {
 export function isServerInstalled(command: string[]): boolean {
   if (command.length === 0) return false
   if (command.length === 0) return false
 
 

+ 87 - 0
src/tools/lsp/protocol-parser.ts

@@ -0,0 +1,87 @@
+/**
+ * Utility for parsing LSP (Language Server Protocol) messages from a raw byte buffer.
+ * LSP uses a header-based protocol similar to HTTP.
+ */
+
+const decoder = new TextDecoder();
+const CONTENT_LENGTH = [67, 111, 110, 116, 101, 110, 116, 45, 76, 101, 110, 103, 116, 104, 58]; // "Content-Length:"
+const CRLF_CRLF = [13, 10, 13, 10]; // "\r\n\r\n"
+const LF_LF = [10, 10]; // "\n\n"
+
+/**
+ * Finds the first occurrence of a byte sequence in a Uint8Array.
+ * 
+ * @param haystack The buffer to search in.
+ * @param needle The byte sequence to find.
+ * @returns The index of the first occurrence, or -1 if not found.
+ */
+function findSequence(haystack: Uint8Array, needle: number[]): number {
+  outer: for (let i = 0; i <= haystack.length - needle.length; i++) {
+    for (let j = 0; j < needle.length; j++) {
+      if (haystack[i + j] !== needle[j]) continue outer;
+    }
+    return i;
+  }
+  return -1;
+}
+
+/**
+ * Parses LSP messages from a raw byte buffer.
+ * 
+ * @param buffer The raw bytes received from the LSP server.
+ * @returns An object containing the parsed messages and any remaining bytes.
+ */
+export function parseMessages(buffer: Uint8Array): { messages: any[]; remainingBuffer: Uint8Array } {
+  const messages: any[] = [];
+  let currentBuffer = buffer;
+
+  while (true) {
+    const headerStart = findSequence(currentBuffer, CONTENT_LENGTH);
+    if (headerStart === -1) break;
+    
+    // Discard any data before Content-Length
+    if (headerStart > 0) {
+      currentBuffer = currentBuffer.slice(headerStart);
+    }
+
+    let headerEnd = findSequence(currentBuffer, CRLF_CRLF);
+    let sepLen = 4;
+    if (headerEnd === -1) {
+      headerEnd = findSequence(currentBuffer, LF_LF);
+      sepLen = 2;
+    }
+    
+    if (headerEnd === -1) break;
+
+    const header = decoder.decode(currentBuffer.slice(0, headerEnd));
+    const match = header.match(/Content-Length:\s*(\d+)/i);
+    if (!match) {
+      // If we found something that looks like a header but doesn't have Content-Length,
+      // skip past it to avoid infinite loops.
+      currentBuffer = currentBuffer.slice(headerEnd + sepLen);
+      continue;
+    }
+
+    const len = parseInt(match[1], 10);
+    const start = headerEnd + sepLen;
+    const end = start + len;
+
+    if (currentBuffer.length < end) break;
+
+    const content = decoder.decode(currentBuffer.slice(start, end));
+    currentBuffer = currentBuffer.slice(end);
+
+    try {
+      const msg = JSON.parse(content);
+      messages.push(msg);
+    } catch (err) {
+      // We log but don't throw to avoid crashing the whole client on one malformed message
+      console.error(`[protocol-parser] Failed to parse LSP message: ${err instanceof Error ? err.message : String(err)}`);
+    }
+  }
+
+  return {
+    messages,
+    remainingBuffer: currentBuffer,
+  };
+}

+ 49 - 0
src/tools/lsp/text-editor.ts

@@ -0,0 +1,49 @@
+import { readFileSync, writeFileSync } from "fs"
+import type { TextEdit } from "./types"
+
+/**
+ * Applies a list of text edits to a file.
+ *
+ * @param filePath - The absolute path to the file to modify.
+ * @param edits - An array of TextEdit objects to apply.
+ * @returns An object indicating success, the number of edits applied, and an optional error message.
+ */
+export function applyTextEditsToFile(
+  filePath: string,
+  edits: TextEdit[]
+): { success: boolean; editCount: number; error?: string } {
+  try {
+    const content = readFileSync(filePath, "utf-8")
+    const lines = content.split("\n")
+
+    const sortedEdits = [...edits].sort((a, b) => {
+      if (b.range.start.line !== a.range.start.line) {
+        return b.range.start.line - a.range.start.line
+      }
+      return b.range.start.character - a.range.start.character
+    })
+
+    for (const edit of sortedEdits) {
+      const startLine = edit.range.start.line
+      const startChar = edit.range.start.character
+      const endLine = edit.range.end.line
+      const endChar = edit.range.end.character
+
+      if (startLine === endLine) {
+        const line = lines[startLine] || ""
+        lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar)
+      } else {
+        const firstLine = lines[startLine] || ""
+        const lastLine = lines[endLine] || ""
+        const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar)
+        lines.splice(startLine, endLine - startLine + 1, ...newContent.split("\n"))
+      }
+    }
+
+    writeFileSync(filePath, lines.join("\n"), "utf-8")
+    return { success: true, editCount: edits.length }
+  } catch (err) {
+    const message = err instanceof Error ? err.message : String(err)
+    return { success: false, editCount: 0, error: `[lsp-utils] applyTextEdits: ${message}` }
+  }
+}

+ 52 - 49
src/tools/lsp/utils.ts

@@ -6,7 +6,8 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync, statSync } from "f
 import { lspManager } from "./client"
 import { lspManager } from "./client"
 import type { LSPClient } from "./client"
 import type { LSPClient } from "./client"
 import { findServerForExtension } from "./config"
 import { findServerForExtension } from "./config"
-import { SYMBOL_KIND_MAP, SEVERITY_MAP } from "./constants"
+import { SEVERITY_MAP } from "./constants"
+import { applyTextEditsToFile } from "./text-editor"
 import type {
 import type {
   Location,
   Location,
   LocationLink,
   LocationLink,
@@ -16,6 +17,11 @@ import type {
   ServerLookupResult,
   ServerLookupResult,
 } from "./types"
 } from "./types"
 
 
+/**
+ * Finds the workspace root for a given file by looking for common markers like .git or package.json.
+ * @param filePath - The path to the file.
+ * @returns The resolved workspace root directory.
+ */
 export function findWorkspaceRoot(filePath: string): string {
 export function findWorkspaceRoot(filePath: string): string {
   let dir = resolve(filePath)
   let dir = resolve(filePath)
 
 
@@ -43,6 +49,11 @@ export function findWorkspaceRoot(filePath: string): string {
   return dirname(resolve(filePath))
   return dirname(resolve(filePath))
 }
 }
 
 
+/**
+ * Converts a file URI to a local filesystem path.
+ * @param uri - The file URI (e.g., 'file:///path/to/file').
+ * @returns The local filesystem path.
+ */
 export function uriToPath(uri: string): string {
 export function uriToPath(uri: string): string {
   return fileURLToPath(uri)
   return fileURLToPath(uri)
 }
 }
@@ -50,7 +61,7 @@ export function uriToPath(uri: string): string {
 export function formatServerLookupError(result: Exclude<ServerLookupResult, { status: "found" }>): string {
 export function formatServerLookupError(result: Exclude<ServerLookupResult, { status: "found" }>): string {
   if (result.status === "not_installed") {
   if (result.status === "not_installed") {
     return [
     return [
-      `LSP server '${result.server.id}' is NOT INSTALLED.`,
+      `[lsp-utils] findServer: LSP server '${result.server.id}' is NOT INSTALLED.`,
       ``,
       ``,
       `Command not found: ${result.server.command[0]}`,
       `Command not found: ${result.server.command[0]}`,
       ``,
       ``,
@@ -58,9 +69,17 @@ export function formatServerLookupError(result: Exclude<ServerLookupResult, { st
     ].join("\n")
     ].join("\n")
   }
   }
 
 
-  return `No LSP server configured for extension: ${result.extension}`
+  return `[lsp-utils] findServer: No LSP server configured for extension: ${result.extension}`
 }
 }
 
 
+/**
+ * Executes a callback function with an LSP client for the given file.
+ * Manages client acquisition and release automatically.
+ * @param filePath - The path to the file to get a client for.
+ * @param fn - The callback function to execute with the client.
+ * @returns The result of the callback function.
+ * @throws Error if no suitable LSP server is found or if the client times out.
+ */
 export async function withLspClient<T>(filePath: string, fn: (client: LSPClient) => Promise<T>): Promise<T> {
 export async function withLspClient<T>(filePath: string, fn: (client: LSPClient) => Promise<T>): Promise<T> {
   const absPath = resolve(filePath)
   const absPath = resolve(filePath)
   const ext = extname(absPath)
   const ext = extname(absPath)
@@ -80,7 +99,7 @@ export async function withLspClient<T>(filePath: string, fn: (client: LSPClient)
     if (e instanceof Error && e.message.includes("timeout")) {
     if (e instanceof Error && e.message.includes("timeout")) {
       const isInitializing = lspManager.isServerInitializing(root, server.id)
       const isInitializing = lspManager.isServerInitializing(root, server.id)
       if (isInitializing) {
       if (isInitializing) {
-        throw new Error(`LSP server is still initializing. Please retry in a few seconds.`)
+        throw new Error(`[lsp-utils] withLspClient: LSP server is still initializing. Please retry in a few seconds.`)
       }
       }
     }
     }
     throw e
     throw e
@@ -89,6 +108,11 @@ export async function withLspClient<T>(filePath: string, fn: (client: LSPClient)
   }
   }
 }
 }
 
 
+/**
+ * Formats an LSP location or location link into a human-readable string (path:line:char).
+ * @param loc - The LSP location or location link.
+ * @returns A formatted string representation.
+ */
 export function formatLocation(loc: Location | LocationLink): string {
 export function formatLocation(loc: Location | LocationLink): string {
   if ("targetUri" in loc) {
   if ("targetUri" in loc) {
     const uri = uriToPath(loc.targetUri)
     const uri = uriToPath(loc.targetUri)
@@ -103,15 +127,16 @@ export function formatLocation(loc: Location | LocationLink): string {
   return `${uri}:${line}:${char}`
   return `${uri}:${line}:${char}`
 }
 }
 
 
-export function formatSymbolKind(kind: number): string {
-  return SYMBOL_KIND_MAP[kind] || `Unknown(${kind})`
-}
-
 export function formatSeverity(severity: number | undefined): string {
 export function formatSeverity(severity: number | undefined): string {
   if (!severity) return "unknown"
   if (!severity) return "unknown"
   return SEVERITY_MAP[severity] || `unknown(${severity})`
   return SEVERITY_MAP[severity] || `unknown(${severity})`
 }
 }
 
 
+/**
+ * Formats an LSP diagnostic into a human-readable string.
+ * @param diag - The LSP diagnostic.
+ * @returns A formatted string representation.
+ */
 export function formatDiagnostic(diag: Diagnostic): string {
 export function formatDiagnostic(diag: Diagnostic): string {
   const severity = formatSeverity(diag.severity)
   const severity = formatSeverity(diag.severity)
   const line = diag.range.start.line + 1
   const line = diag.range.start.line + 1
@@ -142,42 +167,6 @@ export function filterDiagnosticsBySeverity(
 
 
 // WorkspaceEdit application
 // WorkspaceEdit application
 
 
-function applyTextEditsToFile(filePath: string, edits: TextEdit[]): { success: boolean; editCount: number; error?: string } {
-  try {
-    const content = readFileSync(filePath, "utf-8")
-    const lines = content.split("\n")
-
-    const sortedEdits = [...edits].sort((a, b) => {
-      if (b.range.start.line !== a.range.start.line) {
-        return b.range.start.line - a.range.start.line
-      }
-      return b.range.start.character - a.range.start.character
-    })
-
-    for (const edit of sortedEdits) {
-      const startLine = edit.range.start.line
-      const startChar = edit.range.start.character
-      const endLine = edit.range.end.line
-      const endChar = edit.range.end.character
-
-      if (startLine === endLine) {
-        const line = lines[startLine] || ""
-        lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar)
-      } else {
-        const firstLine = lines[startLine] || ""
-        const lastLine = lines[endLine] || ""
-        const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar)
-        lines.splice(startLine, endLine - startLine + 1, ...newContent.split("\n"))
-      }
-    }
-
-    writeFileSync(filePath, lines.join("\n"), "utf-8")
-    return { success: true, editCount: edits.length }
-  } catch (err) {
-    return { success: false, editCount: 0, error: err instanceof Error ? err.message : String(err) }
-  }
-}
-
 export interface ApplyResult {
 export interface ApplyResult {
   success: boolean
   success: boolean
   filesModified: string[]
   filesModified: string[]
@@ -185,9 +174,15 @@ export interface ApplyResult {
   errors: string[]
   errors: string[]
 }
 }
 
 
+/**
+ * Applies an LSP workspace edit to the local filesystem.
+ * Supports both 'changes' (TextEdits) and 'documentChanges' (Create/Rename/Delete/Edit).
+ * @param edit - The workspace edit to apply.
+ * @returns An object containing the success status, modified files, and any errors.
+ */
 export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
 export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
   if (!edit) {
   if (!edit) {
-    return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] }
+    return { success: false, filesModified: [], totalEdits: 0, errors: ["[lsp-utils] applyWorkspaceEdit: No edit provided"] }
   }
   }
 
 
   const result: ApplyResult = { success: true, filesModified: [], totalEdits: 0, errors: [] }
   const result: ApplyResult = { success: true, filesModified: [], totalEdits: 0, errors: [] }
@@ -202,7 +197,7 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
         result.totalEdits += applyResult.editCount
         result.totalEdits += applyResult.editCount
       } else {
       } else {
         result.success = false
         result.success = false
-        result.errors.push(`${filePath}: ${applyResult.error}`)
+        result.errors.push(`[lsp-utils] applyWorkspaceEdit: ${filePath}: ${applyResult.error?.replace("[lsp-utils] applyTextEdits: ", "")}`)
       }
       }
     }
     }
   }
   }
@@ -217,7 +212,8 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
             result.filesModified.push(filePath)
             result.filesModified.push(filePath)
           } catch (err) {
           } catch (err) {
             result.success = false
             result.success = false
-            result.errors.push(`Create ${change.uri}: ${err}`)
+            const message = err instanceof Error ? err.message : String(err)
+            result.errors.push(`[lsp-utils] applyWorkspaceEdit: Create ${change.uri}: ${message}`)
           }
           }
         } else if (change.kind === "rename") {
         } else if (change.kind === "rename") {
           try {
           try {
@@ -229,7 +225,8 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
             result.filesModified.push(newPath)
             result.filesModified.push(newPath)
           } catch (err) {
           } catch (err) {
             result.success = false
             result.success = false
-            result.errors.push(`Rename ${change.oldUri}: ${err}`)
+            const message = err instanceof Error ? err.message : String(err)
+            result.errors.push(`[lsp-utils] applyWorkspaceEdit: Rename ${change.oldUri}: ${message}`)
           }
           }
         } else if (change.kind === "delete") {
         } else if (change.kind === "delete") {
           try {
           try {
@@ -238,7 +235,8 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
             result.filesModified.push(filePath)
             result.filesModified.push(filePath)
           } catch (err) {
           } catch (err) {
             result.success = false
             result.success = false
-            result.errors.push(`Delete ${change.uri}: ${err}`)
+            const message = err instanceof Error ? err.message : String(err)
+            result.errors.push(`[lsp-utils] applyWorkspaceEdit: Delete ${change.uri}: ${message}`)
           }
           }
         }
         }
       } else {
       } else {
@@ -259,6 +257,11 @@ export function applyWorkspaceEdit(edit: WorkspaceEdit | null): ApplyResult {
   return result
   return result
 }
 }
 
 
+/**
+ * Formats the result of a workspace edit application into a human-readable summary.
+ * @param result - The apply result from applyWorkspaceEdit.
+ * @returns A formatted summary string.
+ */
 export function formatApplyResult(result: ApplyResult): string {
 export function formatApplyResult(result: ApplyResult): string {
   const lines: string[] = []
   const lines: string[] = []
 
 

+ 2 - 1
src/tools/quota/api.ts

@@ -1,6 +1,7 @@
 import * as path from "path";
 import * as path from "path";
 import * as os from "os";
 import * as os from "os";
 import * as fs from "fs";
 import * as fs from "fs";
+import { sleep } from "../../utils/polling";
 import type {
 import type {
   Account,
   Account,
   AccountsConfig,
   AccountsConfig,
@@ -177,7 +178,7 @@ export async function fetchAccountQuota(account: Account): Promise<AccountQuotaR
 export async function fetchAllQuotas(accounts: Account[]): Promise<AccountQuotaResult[]> {
 export async function fetchAllQuotas(accounts: Account[]): Promise<AccountQuotaResult[]> {
   const results: AccountQuotaResult[] = [];
   const results: AccountQuotaResult[] = [];
   for (let i = 0; i < accounts.length; i++) {
   for (let i = 0; i < accounts.length; i++) {
-    if (i > 0) await new Promise((r) => setTimeout(r, ACCOUNT_FETCH_DELAY_MS));
+    if (i > 0) await sleep(ACCOUNT_FETCH_DELAY_MS);
     results.push(await fetchAccountQuota(accounts[i]));
     results.push(await fetchAccountQuota(accounts[i]));
   }
   }
   return results;
   return results;

+ 3 - 2
src/tools/quota/command.ts

@@ -1,6 +1,7 @@
 import * as path from "path";
 import * as path from "path";
 import * as os from "os";
 import * as os from "os";
 import * as fs from "fs";
 import * as fs from "fs";
+import { log } from "../../shared/logger";
 
 
 // Define base configuration directory based on OS
 // Define base configuration directory based on OS
 const isWindows = os.platform() === "win32";
 const isWindows = os.platform() === "win32";
@@ -44,6 +45,6 @@ try {
     }
     }
   }
   }
 } catch (error) {
 } catch (error) {
-  console.error("Failed to create command file/directory:", error);
+  log("Failed to create command file/directory:", error);
   // Continue execution, as this might not be fatal for the plugin's core function
   // Continue execution, as this might not be fatal for the plugin's core function
-}
+}

+ 162 - 0
src/tools/shared/downloader-utils.ts

@@ -0,0 +1,162 @@
+import { existsSync, mkdirSync, chmodSync, unlinkSync, readdirSync, renameSync } from "node:fs"
+import { join } from "node:path"
+import { homedir } from "node:os"
+import { spawn } from "bun"
+import { extractZip } from "../../shared"
+import { log } from "../../shared/logger"
+
+export interface DownloadOptions {
+  binaryName: string
+  version: string
+  url: string
+  installDir: string
+  archiveName: string
+  isZip: boolean
+  tarInclude?: string
+}
+
+/**
+ * Downloads a file from a URL to a local destination path.
+ * Supports redirects.
+ * @param url - The URL to download from.
+ * @param destPath - The local path to save the file to.
+ * @returns A promise that resolves when the download is complete.
+ * @throws Error if the download fails.
+ */
+export async function downloadFile(url: string, destPath: string): Promise<void> {
+  const response = await fetch(url, { redirect: "follow" })
+  if (!response.ok) {
+    throw new Error(`Download failed: HTTP ${response.status} ${response.statusText}`)
+  }
+  const buffer = await response.arrayBuffer()
+  await Bun.write(destPath, buffer)
+}
+
+/**
+ * Extracts a .tar.gz archive using the system 'tar' command.
+ * @param archivePath - Path to the archive file.
+ * @param destDir - Directory to extract into.
+ * @param include - Optional pattern of files to include.
+ * @returns A promise that resolves when extraction is complete.
+ * @throws Error if extraction fails.
+ */
+export async function extractTarGz(archivePath: string, destDir: string, include?: string): Promise<void> {
+  const args = ["tar", "-xzf", archivePath, "--strip-components=1"]
+  
+  if (include) {
+    if (process.platform === "darwin") {
+      args.push(`--include=${include}`)
+    } else if (process.platform === "linux") {
+      args.push("--wildcards", include)
+    }
+  }
+
+  const proc = spawn(args, {
+    cwd: destDir,
+    stdout: "pipe",
+    stderr: "pipe",
+  })
+
+  const exitCode = await proc.exited
+  if (exitCode !== 0) {
+    const stderr = await new Response(proc.stderr).text()
+    throw new Error(`Failed to extract tar.gz: ${stderr}`)
+  }
+}
+
+/**
+ * Recursively searches for a file with a specific name within a directory.
+ * @param dir - The directory to search in.
+ * @param filename - The name of the file to find.
+ * @returns The full path to the found file, or null if not found.
+ */
+export function findFileRecursive(dir: string, filename: string): string | null {
+  try {
+    const entries = readdirSync(dir, { withFileTypes: true, recursive: true })
+    for (const entry of entries) {
+      if (entry.isFile() && entry.name === filename) {
+        // Bun 1.1+ supports entry.parentPath, fallback to dir if not available
+        return join((entry as any).parentPath ?? dir, entry.name)
+      }
+    }
+  } catch {
+    return null
+  }
+  return null
+}
+
+/**
+ * Gets the default installation directory for binaries based on the OS.
+ * @returns The absolute path to the default installation directory.
+ */
+export function getDefaultInstallDir(): string {
+  const home = homedir()
+  if (process.platform === "win32") {
+    const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA
+    const base = localAppData || join(home, "AppData", "Local")
+    return join(base, "oh-my-opencode-slim", "bin")
+  }
+
+  const xdgCache = process.env.XDG_CACHE_HOME
+  const base = xdgCache || join(home, ".cache")
+  return join(base, "oh-my-opencode-slim", "bin")
+}
+
+/**
+ * Ensures a binary is available locally, downloading and extracting it if necessary.
+ * @param options - Download and installation options.
+ * @returns A promise resolving to the absolute path of the binary.
+ * @throws Error if download or extraction fails, or if the binary is not found.
+ */
+export async function ensureBinary(options: DownloadOptions): Promise<string> {
+  const { binaryName, url, installDir, archiveName, isZip, tarInclude } = options
+  const isWindows = process.platform === "win32"
+  const fullBinaryName = isWindows ? `${binaryName}.exe` : binaryName
+  const binaryPath = join(installDir, fullBinaryName)
+
+  if (existsSync(binaryPath)) {
+    return binaryPath
+  }
+
+  if (!existsSync(installDir)) {
+    mkdirSync(installDir, { recursive: true })
+  }
+
+  const archivePath = join(installDir, archiveName)
+
+  try {
+    log(`[oh-my-opencode-slim] Downloading ${binaryName}...`)
+    await downloadFile(url, archivePath)
+
+    if (isZip) {
+      await extractZip(archivePath, installDir)
+      
+      // Some zips have nested structures, try to find the binary and move it to the root of installDir
+      const foundPath = findFileRecursive(installDir, fullBinaryName)
+      if (foundPath && foundPath !== binaryPath) {
+        renameSync(foundPath, binaryPath)
+      }
+    } else {
+      await extractTarGz(archivePath, installDir, tarInclude)
+    }
+
+    if (!isWindows && existsSync(binaryPath)) {
+      chmodSync(binaryPath, 0o755)
+    }
+
+    if (!existsSync(binaryPath)) {
+      throw new Error(`${binaryName} binary not found after extraction`)
+    }
+
+    log(`[oh-my-opencode-slim] ${binaryName} ready.`)
+    return binaryPath
+  } finally {
+    if (existsSync(archivePath)) {
+      try {
+        unlinkSync(archivePath)
+      } catch {
+        // Cleanup failures are non-critical
+      }
+    }
+  }
+}

+ 6 - 81
src/tools/skill/builtin.ts

@@ -1,3 +1,5 @@
+import { readFileSync } from "fs";
+import { fileURLToPath } from "url";
 import type { SkillDefinition } from "./types";
 import type { SkillDefinition } from "./types";
 import type { PluginConfig, AgentName } from "../../config/schema";
 import type { PluginConfig, AgentName } from "../../config/schema";
 
 
@@ -17,87 +19,10 @@ export const DEFAULT_AGENT_SKILLS: Record<AgentName, string[]> = {
   fixer: [],
   fixer: [],
 };
 };
 
 
-const YAGNI_TEMPLATE = `# YAGNI Enforcement Skill
-
-You are a code simplicity expert specializing in minimalism and the YAGNI (You Aren't Gonna Need It) principle. Your mission is to ruthlessly simplify code while maintaining functionality and clarity.
-
-When reviewing code, you will:
-
-1. **Analyze Every Line**: Question the necessity of each line of code. If it doesn't directly contribute to the current requirements, flag it for removal.
-
-2. **Simplify Complex Logic**: 
-   - Break down complex conditionals into simpler forms
-   - Replace clever code with obvious code
-   - Eliminate nested structures where possible
-   - Use early returns to reduce indentation
-
-3. **Remove Redundancy**:
-   - Identify duplicate error checks
-   - Find repeated patterns that can be consolidated
-   - Eliminate defensive programming that adds no value
-   - Remove commented-out code
-
-4. **Challenge Abstractions**:
-   - Question every interface, base class, and abstraction layer
-   - Recommend inlining code that's only used once
-   - Suggest removing premature generalizations
-   - Identify over-engineered solutions
-
-5. **Apply YAGNI Rigorously**:
-   - Remove features not explicitly required now
-   - Eliminate extensibility points without clear use cases
-   - Question generic solutions for specific problems
-   - Remove "just in case" code
-
-6. **Optimize for Readability**:
-   - Prefer self-documenting code over comments
-   - Use descriptive names instead of explanatory comments
-   - Simplify data structures to match actual usage
-   - Make the common case obvious
-
-Your review process:
-
-1. First, identify the core purpose of the code
-2. List everything that doesn't directly serve that purpose
-3. For each complex section, propose a simpler alternative
-4. Create a prioritized list of simplification opportunities
-5. Estimate the lines of code that can be removed
-
-Output format:
-
-\`\`\`markdown
-## Simplification Analysis
-
-### Core Purpose
-[Clearly state what this code actually needs to do]
-
-### Unnecessary Complexity Found
-- [Specific issue with line numbers/file]
-- [Why it's unnecessary]
-- [Suggested simplification]
-
-### Code to Remove
-- [File:lines] - [Reason]
-- [Estimated LOC reduction: X]
-
-### Simplification Recommendations
-1. [Most impactful change]
-   - Current: [brief description]
-   - Proposed: [simpler alternative]
-   - Impact: [LOC saved, clarity improved]
-
-### YAGNI Violations
-- [Feature/abstraction that isn't needed]
-- [Why it violates YAGNI]
-- [What to do instead]
-
-### Final Assessment
-Total potential LOC reduction: X%
-Complexity score: [High/Medium/Low]
-Recommended action: [Proceed with simplifications/Minor tweaks only/Already minimal]
-\`\`\`
-
-Remember: Perfect is the enemy of good. The simplest code that works is often the best code. Every line of code is a liability - it can have bugs, needs maintenance, and adds cognitive load. Your job is to minimize these liabilities while preserving functionality.`;
+const YAGNI_TEMPLATE = readFileSync(
+  fileURLToPath(new URL("./templates/yagni.md", import.meta.url)),
+  "utf-8"
+);
 
 
 const PLAYWRIGHT_TEMPLATE = `# Playwright Browser Automation Skill
 const PLAYWRIGHT_TEMPLATE = `# Playwright Browser Automation Skill
 
 

+ 4 - 4
src/tools/skill/mcp-manager.ts

@@ -85,7 +85,7 @@ export class SkillMcpManager {
   ): Promise<Client> {
   ): Promise<Client> {
     if (!("url" in config)) {
     if (!("url" in config)) {
       throw new Error(
       throw new Error(
-        `MCP server "${info.serverName}" missing url for HTTP connection.`
+        `[mcp-manager] init: MCP server "${info.serverName}" missing url for HTTP connection.`
       );
       );
     }
     }
 
 
@@ -114,7 +114,7 @@ export class SkillMcpManager {
       }
       }
       const errorMessage = error instanceof Error ? error.message : String(error);
       const errorMessage = error instanceof Error ? error.message : String(error);
       throw new Error(
       throw new Error(
-        `Failed to connect to MCP server "${info.serverName}". ${errorMessage}`
+        `[mcp-manager] connect: failed to connect to MCP server "${info.serverName}": ${errorMessage}`
       );
       );
     }
     }
 
 
@@ -137,7 +137,7 @@ export class SkillMcpManager {
   ): Promise<Client> {
   ): Promise<Client> {
     if (!("command" in config)) {
     if (!("command" in config)) {
       throw new Error(
       throw new Error(
-        `MCP server "${info.serverName}" missing command for stdio connection.`
+        `[mcp-manager] init: MCP server "${info.serverName}" missing command for stdio connection.`
       );
       );
     }
     }
 
 
@@ -163,7 +163,7 @@ export class SkillMcpManager {
       }
       }
       const errorMessage = error instanceof Error ? error.message : String(error);
       const errorMessage = error instanceof Error ? error.message : String(error);
       throw new Error(
       throw new Error(
-        `Failed to connect to MCP server "${info.serverName}". ${errorMessage}`
+        `[mcp-manager] connect: failed to connect to MCP server "${info.serverName}": ${errorMessage}`
       );
       );
     }
     }
 
 

+ 81 - 0
src/tools/skill/templates/yagni.md

@@ -0,0 +1,81 @@
+# YAGNI Enforcement Skill
+
+You are a code simplicity expert specializing in minimalism and the YAGNI (You Aren't Gonna Need It) principle. Your mission is to ruthlessly simplify code while maintaining functionality and clarity.
+
+When reviewing code, you will:
+
+1. **Analyze Every Line**: Question the necessity of each line of code. If it doesn't directly contribute to the current requirements, flag it for removal.
+
+2. **Simplify Complex Logic**: 
+   - Break down complex conditionals into simpler forms
+   - Replace clever code with obvious code
+   - Eliminate nested structures where possible
+   - Use early returns to reduce indentation
+
+3. **Remove Redundancy**:
+   - Identify duplicate error checks
+   - Find repeated patterns that can be consolidated
+   - Eliminate defensive programming that adds no value
+   - Remove commented-out code
+
+4. **Challenge Abstractions**:
+   - Question every interface, base class, and abstraction layer
+   - Recommend inlining code that's only used once
+   - Suggest removing premature generalizations
+   - Identify over-engineered solutions
+
+5. **Apply YAGNI Rigorously**:
+   - Remove features not explicitly required now
+   - Eliminate messaging points without clear use cases
+   - Question generic solutions for specific problems
+   - Remove "just in case" code
+
+6. **Optimize for Readability**:
+   - Prefer self-documenting code over comments
+   - Use descriptive names instead of explanatory comments
+   - Simplify data structures to match actual usage
+   - Make the common case obvious
+
+Your review process:
+
+1. First, identify the core purpose of the code
+2. List everything that doesn't directly serve that purpose
+3. For each complex section, propose a simpler alternative
+4. Create a prioritized list of simplification opportunities
+5. Estimate the lines of code that can be removed
+
+Output format:
+
+```markdown
+## Simplification Analysis
+
+### Core Purpose
+[Clearly state what this code actually needs to do]
+
+### Unnecessary Complexity Found
+- [Specific issue with line numbers/file]
+- [Why it's unnecessary]
+- [Suggested simplification]
+
+### Code to Remove
+- [File:lines] - [Reason]
+- [Estimated LOC reduction: X]
+
+### Simplification Recommendations
+1. [Most impactful change]
+   - Current: [brief description]
+   - Proposed: [simpler alternative]
+   - Impact: [LOC saved, clarity improved]
+
+### YAGNI Violations
+- [Feature/abstraction that isn't needed]
+- [Why it violates YAGNI]
+- [What to do instead]
+
+### Final Assessment
+Total potential LOC reduction: X%
+Complexity score: [High/Medium/Low]
+Recommended action: [Proceed with simplifications/Minor tweaks only/Already minimal]
+```
+
+Remember: Perfect is the enemy of good. The simplest code that works is often the best code. Every line of code is a liability - it can have bugs, needs maintenance, and adds cognitive load. Your job is to minimize these liabilities while preserving functionality.

+ 6 - 0
src/tools/skill/tools.ts

@@ -108,6 +108,12 @@ async function formatMcpCapabilities(
   return sections.join("\n");
   return sections.join("\n");
 }
 }
 
 
+/**
+ * Creates skill tools for agent use, including 'omos_skill' and 'omos_skill_mcp'.
+ * @param manager - The SkillMcpManager to use for MCP operations.
+ * @param pluginConfig - Optional plugin configuration for skill permissions.
+ * @returns An object containing the created tool definitions.
+ */
 export function createSkillTools(
 export function createSkillTools(
   manager: SkillMcpManager,
   manager: SkillMcpManager,
   pluginConfig?: PluginConfig
   pluginConfig?: PluginConfig

+ 17 - 0
src/utils/agent-variant.ts

@@ -1,11 +1,22 @@
 import type { PluginConfig } from "../config";
 import type { PluginConfig } from "../config";
 import { log } from "../shared/logger";
 import { log } from "../shared/logger";
 
 
+/**
+ * Normalizes an agent name by trimming whitespace and removing leading '@'.
+ * @param agentName - The agent name to normalize.
+ * @returns The normalized agent name.
+ */
 export function normalizeAgentName(agentName: string): string {
 export function normalizeAgentName(agentName: string): string {
   const trimmed = agentName.trim();
   const trimmed = agentName.trim();
   return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
   return trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
 }
 }
 
 
+/**
+ * Resolves the configured variant for a given agent.
+ * @param config - The plugin configuration.
+ * @param agentName - The name of the agent.
+ * @returns The resolved variant name, or undefined if not found/invalid.
+ */
 export function resolveAgentVariant(
 export function resolveAgentVariant(
   config: PluginConfig | undefined,
   config: PluginConfig | undefined,
   agentName: string
   agentName: string
@@ -28,6 +39,12 @@ export function resolveAgentVariant(
   return trimmed;
   return trimmed;
 }
 }
 
 
+/**
+ * Applies a variant to a prompt body if one is provided and not already present.
+ * @param variant - The variant to apply.
+ * @param body - The prompt body to modify.
+ * @returns The updated prompt body with the variant applied.
+ */
 export function applyAgentVariant<T extends { variant?: string }>(
 export function applyAgentVariant<T extends { variant?: string }>(
   variant: string | undefined,
   variant: string | undefined,
   body: T
   body: T

+ 5 - 3
src/utils/polling.ts

@@ -40,7 +40,7 @@ export async function pollUntilStable<T>(
       return { success: false, aborted: true };
       return { success: false, aborted: true };
     }
     }
 
 
-    await new Promise((r) => setTimeout(r, pollInterval));
+    await sleep(pollInterval);
 
 
     const currentData = await fetchFn();
     const currentData = await fetchFn();
 
 
@@ -60,8 +60,10 @@ export async function pollUntilStable<T>(
 }
 }
 
 
 /**
 /**
- * Simple delay utility
+ * Delays execution for a specified number of milliseconds.
+ * @param ms - The number of milliseconds to sleep.
+ * @returns A promise that resolves after the delay.
  */
  */
-export function delay(ms: number): Promise<void> {
+export function sleep(ms: number): Promise<void> {
   return new Promise((r) => setTimeout(r, ms));
   return new Promise((r) => setTimeout(r, ms));
 }
 }

+ 39 - 28
src/utils/tmux.ts

@@ -1,6 +1,7 @@
 import { spawn } from "bun";
 import { spawn } from "bun";
 import { log } from "../shared/logger";
 import { log } from "../shared/logger";
 import type { TmuxConfig, TmuxLayout } from "../config/schema";
 import type { TmuxConfig, TmuxLayout } from "../config/schema";
+import { sleep } from "./polling";
 
 
 let tmuxPath: string | null = null;
 let tmuxPath: string | null = null;
 let tmuxChecked = false;
 let tmuxChecked = false;
@@ -41,22 +42,25 @@ async function isServerRunning(serverUrl: string): Promise<boolean> {
     if (available) {
     if (available) {
       serverCheckUrl = serverUrl;
       serverCheckUrl = serverUrl;
       serverAvailable = true;
       serverAvailable = true;
-      log("[tmux] isServerRunning: checked", { serverUrl, available, attempt });
+      log("[tmux] health-check: success", { serverUrl, available, attempt });
       return true;
       return true;
     }
     }
 
 
     if (attempt < maxAttempts) {
     if (attempt < maxAttempts) {
-      await new Promise((r) => setTimeout(r, 250));
+      await sleep(250);
     }
     }
   }
   }
 
 
-  log("[tmux] isServerRunning: checked", { serverUrl, available: false });
+  log("[tmux] health-check: failed", { serverUrl, available: false });
   return false;
   return false;
 }
 }
 
 
 /**
 /**
  * Reset the server availability cache (useful when server might have started)
  * Reset the server availability cache (useful when server might have started)
  */
  */
+/**
+ * Resets the server availability cache.
+ */
 export function resetServerCheck(): void {
 export function resetServerCheck(): void {
   serverAvailable = null;
   serverAvailable = null;
   serverCheckUrl = null;
   serverCheckUrl = null;
@@ -77,14 +81,14 @@ async function findTmuxPath(): Promise<string | null> {
 
 
     const exitCode = await proc.exited;
     const exitCode = await proc.exited;
     if (exitCode !== 0) {
     if (exitCode !== 0) {
-      log("[tmux] findTmuxPath: 'which tmux' failed", { exitCode });
+      log("[tmux] find: 'which tmux' failed", { exitCode });
       return null;
       return null;
     }
     }
 
 
     const stdout = await new Response(proc.stdout).text();
     const stdout = await new Response(proc.stdout).text();
     const path = stdout.trim().split("\n")[0];
     const path = stdout.trim().split("\n")[0];
     if (!path) {
     if (!path) {
-      log("[tmux] findTmuxPath: no path in output");
+      log("[tmux] find: no path in output");
       return null;
       return null;
     }
     }
 
 
@@ -95,14 +99,14 @@ async function findTmuxPath(): Promise<string | null> {
     });
     });
     const verifyExit = await verifyProc.exited;
     const verifyExit = await verifyProc.exited;
     if (verifyExit !== 0) {
     if (verifyExit !== 0) {
-      log("[tmux] findTmuxPath: tmux -V failed", { path, verifyExit });
+      log("[tmux] find: tmux -V failed", { path, verifyExit });
       return null;
       return null;
     }
     }
 
 
-    log("[tmux] findTmuxPath: found tmux", { path });
+    log("[tmux] find: found tmux", { path });
     return path;
     return path;
   } catch (err) {
   } catch (err) {
-    log("[tmux] findTmuxPath: exception", { error: String(err) });
+    log("[tmux] find: exception", { error: err instanceof Error ? err.message : String(err) });
     return null;
     return null;
   }
   }
 }
 }
@@ -117,13 +121,17 @@ export async function getTmuxPath(): Promise<string | null> {
 
 
   tmuxPath = await findTmuxPath();
   tmuxPath = await findTmuxPath();
   tmuxChecked = true;
   tmuxChecked = true;
-  log("[tmux] getTmuxPath: initialized", { tmuxPath });
+  log("[tmux] init: initialized", { tmuxPath });
   return tmuxPath;
   return tmuxPath;
 }
 }
 
 
 /**
 /**
  * Check if we're running inside tmux
  * Check if we're running inside tmux
  */
  */
+/**
+ * Checks if the current process is running inside a tmux session.
+ * @returns True if running inside tmux, false otherwise.
+ */
 export function isInsideTmux(): boolean {
 export function isInsideTmux(): boolean {
   return !!process.env.TMUX;
   return !!process.env.TMUX;
 }
 }
@@ -160,9 +168,9 @@ async function applyLayout(tmux: string, layout: TmuxLayout, mainPaneSize: numbe
       await reapplyProc.exited;
       await reapplyProc.exited;
     }
     }
 
 
-    log("[tmux] applyLayout: applied", { layout, mainPaneSize });
+    log("[tmux] apply-layout: applied", { layout, mainPaneSize });
   } catch (err) {
   } catch (err) {
-    log("[tmux] applyLayout: exception", { error: String(err) });
+    log("[tmux] apply-layout: exception", { error: err instanceof Error ? err.message : String(err) });
   }
   }
 }
 }
 
 
@@ -183,15 +191,15 @@ export async function spawnTmuxPane(
   config: TmuxConfig,
   config: TmuxConfig,
   serverUrl: string
   serverUrl: string
 ): Promise<SpawnPaneResult> {
 ): Promise<SpawnPaneResult> {
-  log("[tmux] spawnTmuxPane called", { sessionId, description, config, serverUrl });
+  log("[tmux] spawn: start", { sessionId, description, config, serverUrl });
 
 
   if (!config.enabled) {
   if (!config.enabled) {
-    log("[tmux] spawnTmuxPane: config.enabled is false, skipping");
+    log("[tmux] spawn: disabled in config");
     return { success: false };
     return { success: false };
   }
   }
 
 
   if (!isInsideTmux()) {
   if (!isInsideTmux()) {
-    log("[tmux] spawnTmuxPane: not inside tmux, skipping");
+    log("[tmux] spawn: not inside tmux");
     return { success: false };
     return { success: false };
   }
   }
 
 
@@ -199,7 +207,7 @@ export async function spawnTmuxPane(
   // This is needed because serverUrl may be a fallback even when no server is running
   // This is needed because serverUrl may be a fallback even when no server is running
   const serverRunning = await isServerRunning(serverUrl);
   const serverRunning = await isServerRunning(serverUrl);
   if (!serverRunning) {
   if (!serverRunning) {
-    log("[tmux] spawnTmuxPane: OpenCode server not running, skipping", {
+    log("[tmux] spawn: server not running", {
       serverUrl,
       serverUrl,
       hint: "Start opencode with --port 4096"
       hint: "Start opencode with --port 4096"
     });
     });
@@ -208,7 +216,7 @@ export async function spawnTmuxPane(
 
 
   const tmux = await getTmuxPath();
   const tmux = await getTmuxPath();
   if (!tmux) {
   if (!tmux) {
-    log("[tmux] spawnTmuxPane: tmux binary not found, skipping");
+    log("[tmux] spawn: binary not found");
     return { success: false };
     return { success: false };
   }
   }
 
 
@@ -231,7 +239,7 @@ export async function spawnTmuxPane(
       opencodeCmd,
       opencodeCmd,
     ];
     ];
 
 
-    log("[tmux] spawnTmuxPane: executing", { tmux, args, opencodeCmd });
+    log("[tmux] spawn: executing", { tmux, args, opencodeCmd });
 
 
     const proc = spawn([tmux, ...args], {
     const proc = spawn([tmux, ...args], {
       stdout: "pipe",
       stdout: "pipe",
@@ -243,7 +251,7 @@ export async function spawnTmuxPane(
     const stderr = await new Response(proc.stderr).text();
     const stderr = await new Response(proc.stderr).text();
     const paneId = stdout.trim(); // e.g., "%42"
     const paneId = stdout.trim(); // e.g., "%42"
 
 
-    log("[tmux] spawnTmuxPane: split result", { exitCode, paneId, stderr: stderr.trim() });
+    log("[tmux] spawn: result", { exitCode, paneId, stderr: stderr.trim() });
 
 
     if (exitCode === 0 && paneId) {
     if (exitCode === 0 && paneId) {
       // Rename the pane for visibility
       // Rename the pane for visibility
@@ -258,13 +266,13 @@ export async function spawnTmuxPane(
       const mainPaneSize = config.main_pane_size ?? 60;
       const mainPaneSize = config.main_pane_size ?? 60;
       await applyLayout(tmux, layout, mainPaneSize);
       await applyLayout(tmux, layout, mainPaneSize);
 
 
-      log("[tmux] spawnTmuxPane: SUCCESS, pane created and layout applied", { paneId, layout });
+      log("[tmux] spawn: success", { paneId, layout });
       return { success: true, paneId };
       return { success: true, paneId };
     }
     }
 
 
     return { success: false };
     return { success: false };
   } catch (err) {
   } catch (err) {
-    log("[tmux] spawnTmuxPane: exception", { error: String(err) });
+    log("[tmux] spawn: exception", { error: err instanceof Error ? err.message : String(err) });
     return { success: false };
     return { success: false };
   }
   }
 }
 }
@@ -273,16 +281,16 @@ export async function spawnTmuxPane(
  * Close a tmux pane by its ID and reapply layout to rebalance remaining panes
  * Close a tmux pane by its ID and reapply layout to rebalance remaining panes
  */
  */
 export async function closeTmuxPane(paneId: string): Promise<boolean> {
 export async function closeTmuxPane(paneId: string): Promise<boolean> {
-  log("[tmux] closeTmuxPane called", { paneId });
+  log("[tmux] close: start", { paneId });
 
 
   if (!paneId) {
   if (!paneId) {
-    log("[tmux] closeTmuxPane: no paneId provided");
+    log("[tmux] close: no paneId provided");
     return false;
     return false;
   }
   }
 
 
   const tmux = await getTmuxPath();
   const tmux = await getTmuxPath();
   if (!tmux) {
   if (!tmux) {
-    log("[tmux] closeTmuxPane: tmux binary not found");
+    log("[tmux] close: binary not found");
     return false;
     return false;
   }
   }
 
 
@@ -295,27 +303,27 @@ export async function closeTmuxPane(paneId: string): Promise<boolean> {
     const exitCode = await proc.exited;
     const exitCode = await proc.exited;
     const stderr = await new Response(proc.stderr).text();
     const stderr = await new Response(proc.stderr).text();
 
 
-    log("[tmux] closeTmuxPane: result", { exitCode, stderr: stderr.trim() });
+    log("[tmux] close: result", { exitCode, stderr: stderr.trim() });
 
 
     if (exitCode === 0) {
     if (exitCode === 0) {
-      log("[tmux] closeTmuxPane: SUCCESS, pane closed", { paneId });
+      log("[tmux] close: success", { paneId });
 
 
       // Reapply layout to rebalance remaining panes
       // Reapply layout to rebalance remaining panes
       if (storedConfig) {
       if (storedConfig) {
         const layout = storedConfig.layout ?? "main-vertical";
         const layout = storedConfig.layout ?? "main-vertical";
         const mainPaneSize = storedConfig.main_pane_size ?? 60;
         const mainPaneSize = storedConfig.main_pane_size ?? 60;
         await applyLayout(tmux, layout, mainPaneSize);
         await applyLayout(tmux, layout, mainPaneSize);
-        log("[tmux] closeTmuxPane: layout reapplied", { layout });
+        log("[tmux] close: layout reapplied", { layout });
       }
       }
 
 
       return true;
       return true;
     }
     }
 
 
     // Pane might already be closed (user closed it manually, or process exited)
     // Pane might already be closed (user closed it manually, or process exited)
-    log("[tmux] closeTmuxPane: failed (pane may already be closed)", { paneId });
+    log("[tmux] close: failed (pane may already be closed)", { paneId });
     return false;
     return false;
   } catch (err) {
   } catch (err) {
-    log("[tmux] closeTmuxPane: exception", { error: String(err) });
+    log("[tmux] close: exception", { error: err instanceof Error ? err.message : String(err) });
     return false;
     return false;
   }
   }
 }
 }
@@ -323,6 +331,9 @@ export async function closeTmuxPane(paneId: string): Promise<boolean> {
 /**
 /**
  * Start background check for tmux availability
  * Start background check for tmux availability
  */
  */
+/**
+ * Starts a background check for tmux availability by looking for the binary path.
+ */
 export function startTmuxCheck(): void {
 export function startTmuxCheck(): void {
   if (!tmuxChecked) {
   if (!tmuxChecked) {
     getTmuxPath().catch(() => {});
     getTmuxPath().catch(() => {});