From fc5f10618e617ed0a3a9b8d13ddcbabf9b378f88 Mon Sep 17 00:00:00 2001 From: IMvision12 Date: Thu, 26 Feb 2026 21:34:17 -0800 Subject: [PATCH] Fixes --- .gitignore | 4 + .npmignore | 8 ++ README.md | 149 +++++++++++++++++++------------------- package.json | 27 ++++--- src/cli/commands/auth.ts | 10 ++- src/cli/commands/start.ts | 2 +- src/core/router.ts | 8 +- src/tools/cron.ts | 26 ++++++- src/tools/git.ts | 4 +- src/tools/http.ts | 45 ++++++++++-- src/tools/mcp-bridge.ts | 23 +++++- src/tools/process.ts | 27 +++++-- src/tools/registry.ts | 8 +- src/tools/search.ts | 15 +++- src/tools/terminal.ts | 4 - 15 files changed, 243 insertions(+), 117 deletions(-) diff --git a/.gitignore b/.gitignore index df1a79a..b0efed4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ node_modules/ dist/ .env +.env.* .wacli_auth/ .wwebjs_auth/ .wwebjs_cache/ *.log +*.tsbuildinfo +coverage/ .DS_Store +Thumbs.db diff --git a/.npmignore b/.npmignore index fe43d5f..aa1e651 100644 --- a/.npmignore +++ b/.npmignore @@ -10,6 +10,7 @@ tsconfig.json .env.* # Test files +test/ tests/ __tests__/ *.test.ts @@ -20,6 +21,13 @@ __tests__/ .oxlintrc.json .detect-secrets.cfg .secrets.baseline +vitest.config.ts + +# Assets (README images) +assets/ + +# Coverage +coverage/ # Git .git/ diff --git a/README.md b/README.md index 4882c51..5c9495d 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ Send a WhatsApp message. Watch your IDE write code. --- - ## Why txtcode? You're on the couch, on the train, or away from your desk but you need to fix a bug, run tests, or scaffold a feature. With **txtcode**, your phone becomes a remote control for your IDE: @@ -49,24 +48,30 @@ No port forwarding. No VPN. Just message and code. ### Messaging-First + Connect via **6 platforms** : WhatsApp, Telegram, Discord, Slack, Microsoft Teams, and Signal. First user to message is auto-authorized. ### 9 AI Providers + Anthropic, OpenAI, Google Gemini, Mistral, Moonshot, MiniMax, xAI Grok, HuggingFace, and OpenRouter. Hot-switch between them with `/switch`. ### 7 Coding Adapters + Claude Code, Cursor CLI, OpenAI Codex, Gemini CLI, Kiro CLI, OpenCode, and Ollama (local/free). Full IDE control in `/code` mode. ### 9 Built-in Tools + Terminal, process manager, git, file search, HTTP client, environment variables, network diagnostics, cron jobs, and system info all callable by the LLM. ### 13 MCP Servers + Connect GitHub, Brave Search, Puppeteer, PostgreSQL, MongoDB, Redis, Elasticsearch, AWS, GCP, Cloudflare, Vercel, Atlassian, and Supabase as external tools via the Model Context Protocol. ### Session Logging + Per-session logs accessible from the TUI. Follow live, view by index, auto-pruned after 7 days. @@ -99,14 +104,14 @@ That's it. The interactive menu guides you through everything authentication, co ## Supported Platforms -| Platform | Transport | Setup | -|:---|:---|:---| -| **WhatsApp** | QR code pairing | Scan QR in terminal on first run | -| **Telegram** | Bot API | Create bot via [@BotFather](https://t.me/BotFather), paste token | -| **Discord** | Bot gateway | Create app at [discord.com/developers](https://discord.com/developers), paste bot token | -| **Slack** | Socket Mode | Create app at [api.slack.com](https://api.slack.com/apps), enable Socket Mode | -| **Microsoft Teams** | Bot Framework | Register bot at [dev.teams.microsoft.com](https://dev.teams.microsoft.com/bots) | -| **Signal** | signal-cli REST | Run [signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) via Docker | +| Platform | Transport | Setup | +| :------------------ | :-------------- | :-------------------------------------------------------------------------------------- | +| **WhatsApp** | QR code pairing | Scan QR in terminal on first run | +| **Telegram** | Bot API | Create bot via [@BotFather](https://t.me/BotFather), paste token | +| **Discord** | Bot gateway | Create app at [discord.com/developers](https://discord.com/developers), paste bot token | +| **Slack** | Socket Mode | Create app at [api.slack.com](https://api.slack.com/apps), enable Socket Mode | +| **Microsoft Teams** | Bot Framework | Register bot at [dev.teams.microsoft.com](https://dev.teams.microsoft.com/bots) | +| **Signal** | signal-cli REST | Run [signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) via Docker | --- @@ -114,17 +119,17 @@ That's it. The interactive menu guides you through everything authentication, co txtcode supports **9 LLM providers** for chat mode. Configure one or more during setup and hot-switch with `/switch`. -| Provider | Example Models | Notes | -|:---|:---|:---| -| **Anthropic** | `claude-sonnet-4-6`, `claude-opus-4-6` | Claude family | -| **OpenAI** | `gpt-5.2`, `o4-mini`, `gpt-4o` | GPT and o-series | -| **Google Gemini** | `gemini-2.5-pro`, `gemini-2.5-flash` | Gemini family | -| **Mistral** | `mistral-large-latest`, `codestral-latest` | Mistral + Codestral | -| **Moonshot (Kimi)** | `kimi-k2.5`, `moonshot-v1-128k` | Long-context models | -| **MiniMax** | `MiniMax-M2.5`, `MiniMax-M2.1` | MiniMax family | -| **xAI (Grok)** | `grok-4`, `grok-3-fast` | Grok family | -| **HuggingFace** | *Discovered at runtime* | Inference Providers API | -| **OpenRouter** | *Discovered at runtime* | Unified API for 100+ models | +| Provider | Example Models | Notes | +| :------------------ | :----------------------------------------- | :-------------------------- | +| **Anthropic** | `claude-sonnet-4-6`, `claude-opus-4-6` | Claude family | +| **OpenAI** | `gpt-5.2`, `o4-mini`, `gpt-4o` | GPT and o-series | +| **Google Gemini** | `gemini-2.5-pro`, `gemini-2.5-flash` | Gemini family | +| **Mistral** | `mistral-large-latest`, `codestral-latest` | Mistral + Codestral | +| **Moonshot (Kimi)** | `kimi-k2.5`, `moonshot-v1-128k` | Long-context models | +| **MiniMax** | `MiniMax-M2.5`, `MiniMax-M2.1` | MiniMax family | +| **xAI (Grok)** | `grok-4`, `grok-3-fast` | Grok family | +| **HuggingFace** | _Discovered at runtime_ | Inference Providers API | +| **OpenRouter** | _Discovered at runtime_ | Unified API for 100+ models | All providers support tool calling and LLM can invoke any built-in tool or connected MCP server. @@ -134,15 +139,15 @@ All providers support tool calling and LLM can invoke any built-in tool or conne Use `/code` mode to route messages directly to a coding adapter with full IDE control. -| Adapter | Backend | CLI Required | Notes | -|:---|:---|:---|:---| -| **Claude Code** | Anthropic API | `claude` | Official Claude CLI | -| **Cursor CLI** | Cursor | `cursor` | Headless Cursor | -| **OpenAI Codex** | OpenAI API | `codex` | OpenAI's coding agent | -| **Gemini CLI** | Google AI API | `gemini` | Google's CLI | -| **Kiro CLI** | AWS | `kiro-cli` | AWS Kiro subscription | -| **OpenCode** | Multi-provider | `opencode` | Open-source, multi-provider | -| **Ollama Claude Code** | Local (Ollama) | `ollama` | Free, no API key needed | +| Adapter | Backend | CLI Required | Notes | +| :--------------------- | :------------- | :----------- | :-------------------------- | +| **Claude Code** | Anthropic API | `claude` | Official Claude CLI | +| **Cursor CLI** | Cursor | `cursor` | Headless Cursor | +| **OpenAI Codex** | OpenAI API | `codex` | OpenAI's coding agent | +| **Gemini CLI** | Google AI API | `gemini` | Google's CLI | +| **Kiro CLI** | AWS | `kiro-cli` | AWS Kiro subscription | +| **OpenCode** | Multi-provider | `opencode` | Open-source, multi-provider | +| **Ollama Claude Code** | Local (Ollama) | `ollama` | Free, no API key needed | --- @@ -150,17 +155,17 @@ Use `/code` mode to route messages directly to a coding adapter with full IDE co The primary LLM in chat mode has access to **9 built-in tools** that it can call autonomously: -| Tool | Capabilities | -|:---|:---| -| **Terminal** | Execute shell commands with timeout and output capture | -| **Process** | Manage background processes: list, poll, stream logs, kill, send input | -| **Git** | Full git operations (blocks force-push and credential config for safety) | -| **Search** | File and content search across the project | -| **HTTP** | Make HTTP requests (GET, POST, PUT, DELETE, PATCH, HEAD). Blocks cloud metadata endpoints | -| **Env** | Get, set, list, and delete environment variables. Masks sensitive values | -| **Network** | Ping, DNS lookup, reachability checks, port scanning | -| **Cron** | Create, list, and manage cron jobs | -| **Sysinfo** | CPU, memory, disk, uptime, OS details | +| Tool | Capabilities | +| :----------- | :---------------------------------------------------------------------------------------- | +| **Terminal** | Execute shell commands with timeout and output capture | +| **Process** | Manage background processes: list, poll, stream logs, kill, send input | +| **Git** | Full git operations (blocks force-push and credential config for safety) | +| **Search** | File and content search across the project | +| **HTTP** | Make HTTP requests (GET, POST, PUT, DELETE, PATCH, HEAD). Blocks cloud metadata endpoints | +| **Env** | Get, set, list, and delete environment variables. Masks sensitive values | +| **Network** | Ping, DNS lookup, reachability checks, port scanning | +| **Cron** | Create, list, and manage cron jobs | +| **Sysinfo** | CPU, memory, disk, uptime, OS details | --- @@ -170,36 +175,36 @@ txtcode integrates with the **Model Context Protocol** to connect external tool ### Developer Tools -| Server | Transport | Description | -|:---|:---|:---| -| **GitHub** | stdio | Repos, issues, PRs, code search, Actions | -| **Brave Search** | stdio | Web, image, video, and news search | -| **Puppeteer** | stdio | Browser automation, screenshots, form filling | +| Server | Transport | Description | +| :--------------- | :-------- | :-------------------------------------------- | +| **GitHub** | stdio | Repos, issues, PRs, code search, Actions | +| **Brave Search** | stdio | Web, image, video, and news search | +| **Puppeteer** | stdio | Browser automation, screenshots, form filling | ### Databases -| Server | Transport | Description | -|:---|:---|:---| -| **PostgreSQL** | stdio | Read-only SQL queries and schema inspection | -| **MongoDB** | stdio | CRUD, indexes, vector search, Atlas management | -| **Redis** | stdio | Data structures, caching, vectors, pub/sub | -| **Elasticsearch** | stdio | Index management, search queries, cluster ops | -| **Supabase** | HTTP | Postgres, Auth, Storage, Edge Functions | +| Server | Transport | Description | +| :---------------- | :-------- | :--------------------------------------------- | +| **PostgreSQL** | stdio | Read-only SQL queries and schema inspection | +| **MongoDB** | stdio | CRUD, indexes, vector search, Atlas management | +| **Redis** | stdio | Data structures, caching, vectors, pub/sub | +| **Elasticsearch** | stdio | Index management, search queries, cluster ops | +| **Supabase** | HTTP | Postgres, Auth, Storage, Edge Functions | ### Cloud -| Server | Transport | Description | -|:---|:---|:---| -| **AWS** | stdio | S3, Lambda, EKS, CDK, CloudFormation, 60+ services | -| **Google Cloud** | HTTP | BigQuery, GKE, Compute, Storage, Firebase | -| **Cloudflare** | HTTP | Workers, R2, DNS, Zero Trust, 2500+ endpoints | -| **Vercel** | HTTP | Deployments, domains, env vars, logs | +| Server | Transport | Description | +| :--------------- | :-------- | :------------------------------------------------- | +| **AWS** | stdio | S3, Lambda, EKS, CDK, CloudFormation, 60+ services | +| **Google Cloud** | HTTP | BigQuery, GKE, Compute, Storage, Firebase | +| **Cloudflare** | HTTP | Workers, R2, DNS, Zero Trust, 2500+ endpoints | +| **Vercel** | HTTP | Deployments, domains, env vars, logs | ### Productivity -| Server | Transport | Description | -|:---|:---|:---| -| **Atlassian** | HTTP | Jira issues, Confluence pages, Compass components | +| Server | Transport | Description | +| :------------ | :-------- | :------------------------------------------------ | +| **Atlassian** | HTTP | Jira issues, Confluence pages, Compass components | > **stdio** = local process, **HTTP** = remote Streamable HTTP endpoint. You can also add custom MCP servers via **Configuration** → **Manage MCP Servers**. @@ -209,19 +214,18 @@ txtcode integrates with the **Model Context Protocol** to connect external tool Send these commands in any messaging app while connected: -| Command | Description | -|:---|:---| -| `/chat` | Switch to **Chat mode** to send messages to primary LLM with tools *(default)* | -| `/code` | Switch to **Code mode** to send messages to coding adapter (full IDE control) | -| `/switch` | Switch primary LLM provider or coding adapter on the fly | -| `/cli-model` | Change the model used by the current coding adapter | -| `/cancel` | Cancel the currently running command | -| `/status` | Show adapter connection and current configuration | -| `/help` | Show available commands | +| Command | Description | +| :----------- | :----------------------------------------------------------------------------- | +| `/chat` | Switch to **Chat mode** to send messages to primary LLM with tools _(default)_ | +| `/code` | Switch to **Code mode** to send messages to coding adapter (full IDE control) | +| `/switch` | Switch primary LLM provider or coding adapter on the fly | +| `/cli-model` | Change the model used by the current coding adapter | +| `/cancel` | Cancel the currently running command | +| `/status` | Show adapter connection and current configuration | +| `/help` | Show available commands | --- - ## Configuration Config is stored at **`~/.txtcode/config.json`**. API keys and tokens are stored in your OS keychain (via `keytar`), never in the config file. @@ -298,9 +302,6 @@ Verbose and debug output goes to the log file; the terminal shows only key statu --- - - ## License Apache-2.0 - diff --git a/package.json b/package.json index e3da82a..a5ee5b0 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,25 @@ "text-based-coding", "whatsapp" ], - "license": "MIT", - "author": "", + "homepage": "https://github.com/IMvision12/txtcode#readme", + "bugs": { + "url": "https://github.com/IMvision12/txtcode/issues" + }, + "license": "Apache-2.0", + "author": "Gitesh Chawda (IMvision12)", + "repository": { + "type": "git", + "url": "https://github.com/IMvision12/txtcode.git" + }, "bin": { "txtcode": "dist/cli/index.js" }, - "main": "dist/index.js", + "files": [ + "dist/", + "LICENSE", + "README.md" + ], + "main": "dist/cli/index.js", "scripts": { "build": "tsc && npm run copy-data", "copy-data": "node -e \"require('fs').cpSync('src/data', 'dist/data', {recursive: true})\"", @@ -37,7 +50,7 @@ "lint": "oxlint", "lint:fix": "oxlint --fix", "prepublishOnly": "npm run build", - "start": "node dist/cli.js", + "start": "node dist/cli/index.js", "test": "vitest run", "test:watch": "vitest" }, @@ -51,9 +64,7 @@ "chalk": "^4.1.2", "discord.js": "^14.25.1", "dotenv": "^16.3.1", - "gradient-string": "^3.0.0", "keytar": "^7.9.0", - "link-preview-js": "^3.2.0", "openai": "^6.19.0", "qrcode-terminal": "^0.12.0", "telegraf": "^4.15.0" @@ -66,9 +77,7 @@ "typescript": "^5.3.3", "vitest": "^4.0.18" }, - "preferGlobal": true, "engines": { - "node": ">=20.0.0", - "npm": ">=10.0.0" + "node": ">=20.0.0" } } diff --git a/src/cli/commands/auth.ts b/src/cli/commands/auth.ts index 1e6c089..28d3140 100644 --- a/src/cli/commands/auth.ts +++ b/src/cli/commands/auth.ts @@ -813,9 +813,15 @@ export async function authCommand() { await setBotToken("signal-phone", signalPhoneNumber); await setBotToken("signal-api-url", signalCliRestUrl); } - } catch { + } catch (keychainError) { console.log(chalk.red("\n[ERROR] Failed to store credentials in keychain")); - console.log(chalk.yellow("Falling back to encrypted file storage...\n")); + console.log(chalk.yellow("Credentials will be unavailable until keychain access is restored.")); + console.log( + chalk.gray( + `Details: ${keychainError instanceof Error ? keychainError.message : String(keychainError)}`, + ), + ); + console.log(chalk.gray("Re-run 'txtcode' → 'Authenticate' to retry.\n")); } // Build providers object dynamically diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts index b78b1fa..cc74187 100644 --- a/src/cli/commands/start.ts +++ b/src/cli/commands/start.ts @@ -129,7 +129,7 @@ export async function startCommand(_options: { daemon?: boolean }) { process.env.IDE_PORT = String(config.idePort); process.env.AI_API_KEY = apiKey; process.env.AI_PROVIDER = config.aiProvider; - process.env.AI_MODEL = config.aiModel; + process.env.AI_MODEL = config.aiModel || ""; process.env.PROJECT_PATH = config.projectPath || process.cwd(); process.env.OLLAMA_MODEL = config.ollamaModel || "gpt-oss:20b"; process.env.CLAUDE_MODEL = config.claudeModel || "sonnet"; diff --git a/src/core/router.ts b/src/core/router.ts index a78346d..74b3d75 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -186,6 +186,9 @@ export class Router { } async shutdownMCP(): Promise { + for (const serverId of this.mcpBridge.getConnectedServerIds()) { + this.toolRegistry.removeMCPTools(serverId); + } await this.mcpBridge.disconnectAll(); } @@ -313,10 +316,7 @@ export class Router { return result; } finally { - // Clear abort controller after command completes - if (this.currentAbortController && !signal.aborted) { - this.currentAbortController = null; - } + this.currentAbortController = null; } } diff --git a/src/tools/cron.ts b/src/tools/cron.ts index 4327ac4..e13cd74 100644 --- a/src/tools/cron.ts +++ b/src/tools/cron.ts @@ -13,7 +13,7 @@ function runCommand( resolve({ stdout: stdout?.toString() ?? "", stderr: stderr?.toString() ?? "", - code: err ? ((err as { code?: number }).code ?? 1) : 0, + code: err ? ((err as { status?: number }).status ?? 1) : 0, }); }); }); @@ -259,8 +259,27 @@ export class CronTool implements Tool { } if (isWindows) { - const schtasksArgs = ["/create", "/tn", name, "/tr", command]; + const ALLOWED_SCHTASKS_FLAGS = new Set([ + "/sc", + "/st", + "/sd", + "/ed", + "/mo", + "/d", + "/m", + "/ri", + ]); const scheduleParts = schedule.split(/\s+/); + for (const part of scheduleParts) { + if (part.startsWith("/") && !ALLOWED_SCHTASKS_FLAGS.has(part.toLowerCase())) { + return { + toolCallId: "", + output: `Blocked: schedule flag "${part}" is not allowed. Allowed flags: ${[...ALLOWED_SCHTASKS_FLAGS].join(", ")}`, + isError: true, + }; + } + } + const schtasksArgs = ["/create", "/tn", name, "/tr", command]; schtasksArgs.push(...scheduleParts); schtasksArgs.push("/f"); @@ -331,7 +350,8 @@ export class CronTool implements Tool { } const lines = existing.stdout.split("\n"); - const filtered = lines.filter((line) => !line.includes(name)); + const commentTag = `# ${name}`; + const filtered = lines.filter((line) => !line.endsWith(commentTag)); if (filtered.length === lines.length) { return { toolCallId: "", output: `No crontab entry matching "${name}".`, isError: false }; diff --git a/src/tools/git.ts b/src/tools/git.ts index 41fff61..672941e 100644 --- a/src/tools/git.ts +++ b/src/tools/git.ts @@ -25,7 +25,7 @@ function runGit( resolve({ stdout: stdout?.toString() ?? "", stderr: stderr?.toString() ?? "", - code: err ? ((err as { code?: number }).code ?? 1) : 0, + code: err ? ((err as { status?: number }).status ?? 1) : 0, }); }, ); @@ -138,7 +138,7 @@ export class GitTool implements Tool { } } - if (action === "reset" && extraArgs.includes("--hard") && !force) { + if (action === "reset" && extraArgs.includes("--hard")) { return { toolCallId: "", output: `Blocked: "reset --hard" will discard changes. Set force=true to proceed.`, diff --git a/src/tools/http.ts b/src/tools/http.ts index 6746767..94d1bec 100644 --- a/src/tools/http.ts +++ b/src/tools/http.ts @@ -3,11 +3,43 @@ import { Tool, ToolDefinition, ToolResult } from "./types"; const DEFAULT_TIMEOUT_MS = 30_000; const MAX_BODY_SIZE = 50_000; -const BLOCKED_HOSTS = new Set([ - "169.254.169.254", // AWS/GCP/Azure metadata - "metadata.google.internal", - "metadata.internal", -]); +const BLOCKED_HOSTS = new Set(["169.254.169.254", "metadata.google.internal", "metadata.internal"]); + +function isBlockedHost(hostname: string): boolean { + if (BLOCKED_HOSTS.has(hostname)) { + return true; + } + + const lower = hostname.toLowerCase(); + + // Block localhost variants + if ( + lower === "localhost" || + lower === "127.0.0.1" || + lower === "[::1]" || + lower === "0.0.0.0" || + lower === "[::ffff:127.0.0.1]" + ) { + return true; + } + + // Block link-local / metadata IP ranges + if (lower.startsWith("169.254.") || lower.includes("169.254.169.254")) { + return true; + } + + // Block IPv6-mapped metadata + if (lower.includes("::ffff:169.254.") || lower.includes("::ffff:a9fe")) { + return true; + } + + // Block cloud metadata hostnames via subdomain + if (lower.endsWith(".internal") || lower.includes("metadata")) { + return true; + } + + return false; +} export class HttpTool implements Tool { name = "http"; @@ -69,7 +101,7 @@ export class HttpTool implements Tool { return { toolCallId: "", output: `Error: invalid URL: ${url}`, isError: true }; } - if (BLOCKED_HOSTS.has(parsedUrl.hostname)) { + if (isBlockedHost(parsedUrl.hostname)) { return { toolCallId: "", output: `Blocked: requests to ${parsedUrl.hostname} are not allowed (cloud metadata security).`, @@ -96,6 +128,7 @@ export class HttpTool implements Tool { method, headers, signal: controller.signal, + redirect: "manual", }; if (body && !["GET", "HEAD", "OPTIONS"].includes(method)) { diff --git a/src/tools/mcp-bridge.ts b/src/tools/mcp-bridge.ts index 9688b61..2c2076a 100644 --- a/src/tools/mcp-bridge.ts +++ b/src/tools/mcp-bridge.ts @@ -92,7 +92,23 @@ export class MCPBridge { await client.connect(transport); - const toolsResult = await client.listTools(); + let toolsResult: { tools: MCPToolSchema[] }; + try { + toolsResult = await client.listTools(); + } catch (error) { + try { + await client.close(); + } catch { + // Best-effort cleanup + } + try { + await transport.close(); + } catch { + // Best-effort cleanup + } + throw error; + } + const tools: MCPToolAdapter[] = toolsResult.tools.map( (mcpTool) => new MCPToolAdapter(config.id, mcpTool, client), ); @@ -126,6 +142,11 @@ export class MCPBridge { return; } + try { + await conn.client.close(); + } catch { + // Best-effort client cleanup + } try { await conn.transport.close(); } catch (error) { diff --git a/src/tools/process.ts b/src/tools/process.ts index a541337..9ba96af 100644 --- a/src/tools/process.ts +++ b/src/tools/process.ts @@ -158,10 +158,10 @@ export class ProcessTool implements Tool { const running = getSession(sessionId); if (running) { - const _truncNote = running.truncated ? "\n(output was truncated)" : ""; + const truncNote = running.truncated ? "\n(output was truncated)" : ""; return { toolCallId: "", - output: running.aggregated || "(no output yet)", + output: (running.aggregated || "(no output yet)") + truncNote, isError: false, metadata: { totalChars: running.totalOutputChars, truncated: running.truncated }, }; @@ -195,14 +195,21 @@ export class ProcessTool implements Tool { } if (running.child) { + const childRef = running.child; try { - killProcessTree(running.child, 3000); + killProcessTree(childRef, 3000); setTimeout(() => { - if (!running.exited && running.child) { - forceKillProcess(running.child); + try { + if (!running.exited && childRef.pid) { + forceKillProcess(childRef); + } + } catch { + // Process may have already exited } }, 3000); - } catch {} + } catch { + // Best-effort kill + } } const killMsg = @@ -272,6 +279,14 @@ export class ProcessTool implements Tool { if (!sessionId) { return { toolCallId: "", output: "Error: session_id is required for remove.", isError: true }; } + const running = getSession(sessionId); + if (running?.child) { + try { + killProcessTree(running.child, 3000); + } catch { + // Best-effort kill before removal + } + } deleteSession(sessionId); return { toolCallId: "", output: `Session ${sessionId} removed.`, isError: false }; } diff --git a/src/tools/registry.ts b/src/tools/registry.ts index 9a4c908..fe2d9bd 100644 --- a/src/tools/registry.ts +++ b/src/tools/registry.ts @@ -16,12 +16,16 @@ export class ToolRegistry { } removeMCPTools(prefix: string): void { + const toRemove: string[] = []; for (const name of this.mcpToolNames) { if (name.startsWith(prefix + "_")) { - this.tools.delete(name); - this.mcpToolNames.delete(name); + toRemove.push(name); } } + for (const name of toRemove) { + this.tools.delete(name); + this.mcpToolNames.delete(name); + } } getMCPToolCount(): number { diff --git a/src/tools/search.ts b/src/tools/search.ts index 4388614..e6ea8ce 100644 --- a/src/tools/search.ts +++ b/src/tools/search.ts @@ -175,13 +175,22 @@ export class SearchTool implements Tool { include: string, ): ToolResult { let regex: RegExp; - try { - regex = new RegExp(pattern, caseSensitive ? "g" : "gi"); - } catch { + const hasNestedQuantifiers = + /(\+|\*|\{)\??[^)]*(\+|\*|\{)/.test(pattern) || /\(([^)]*\|){10,}/.test(pattern); + if (hasNestedQuantifiers) { regex = new RegExp( pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), caseSensitive ? "g" : "gi", ); + } else { + try { + regex = new RegExp(pattern, caseSensitive ? "g" : "gi"); + } catch { + regex = new RegExp( + pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), + caseSensitive ? "g" : "gi", + ); + } } const matches: string[] = []; diff --git a/src/tools/terminal.ts b/src/tools/terminal.ts index 5a25279..7599f01 100644 --- a/src/tools/terminal.ts +++ b/src/tools/terminal.ts @@ -39,10 +39,6 @@ function resolveShell(): { shell: string; buildArgs: (cmd: string) => string[] } const isWindows = process.platform === "win32"; if (isWindows) { - const _pwsh = process.env.COMSPEC?.toLowerCase().includes("powershell") - ? process.env.COMSPEC - : null; - // Prefer PowerShell (pwsh/powershell), fall back to cmd.exe for (const candidate of ["pwsh.exe", "powershell.exe"]) { try {