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 {