From 43545c105cb3610cba544b4f53cf0bad4a68f3b1 Mon Sep 17 00:00:00 2001
From: steveluc
Date: Fri, 16 Jan 2026 23:19:53 -0800
Subject: [PATCH 01/21] Add command executor MCP server
Introduces a new MCP (Model Context Protocol) server that enables Claude Code to execute TypeAgent commands for music playback, list management, calendar operations, and other natural language tasks.
Key features:
- Connects Claude Code to TypeAgent dispatcher via WebSocket
- Automatic reconnection with 5-second retry interval
- Comprehensive logging to temp files for debugging
- Supports natural language commands (e.g., "play bohemian rhapsody", "add milk to grocery list")
- Clean output formatting with HTML image tags stripped
Includes example configuration for .mcp.json and detailed installation instructions in README.
Co-Authored-By: Claude Sonnet 4.5
---
ts/.mcp.json | 11 +
ts/packages/commandExecutor/README.md | 179 ++
.../commandExecutor/dist-test/testClient.d.ts | 2 +
.../commandExecutor/dist-test/testClient.js | 61 +
.../examples/claude_desktop_config.json | 10 +
.../commandExecutor/examples/example_usage.md | 76 +
ts/packages/commandExecutor/package-lock.json | 1501 +++++++++++++++++
ts/packages/commandExecutor/package.json | 39 +
.../commandExecutor/src/commandServer.ts | 370 ++++
ts/packages/commandExecutor/src/index.ts | 4 +
ts/packages/commandExecutor/src/server.ts | 15 +
ts/packages/commandExecutor/src/tsconfig.json | 6 +
.../commandExecutor/test/testClient.ts | 84 +
.../commandExecutor/test/tsconfig.json | 6 +
ts/packages/commandExecutor/tsconfig.json | 11 +
ts/packages/commandExecutor/view-logs.sh | 29 +
ts/pnpm-lock.yaml | 34 +
17 files changed, 2438 insertions(+)
create mode 100644 ts/.mcp.json
create mode 100644 ts/packages/commandExecutor/README.md
create mode 100644 ts/packages/commandExecutor/dist-test/testClient.d.ts
create mode 100644 ts/packages/commandExecutor/dist-test/testClient.js
create mode 100644 ts/packages/commandExecutor/examples/claude_desktop_config.json
create mode 100644 ts/packages/commandExecutor/examples/example_usage.md
create mode 100644 ts/packages/commandExecutor/package-lock.json
create mode 100644 ts/packages/commandExecutor/package.json
create mode 100644 ts/packages/commandExecutor/src/commandServer.ts
create mode 100644 ts/packages/commandExecutor/src/index.ts
create mode 100644 ts/packages/commandExecutor/src/server.ts
create mode 100644 ts/packages/commandExecutor/src/tsconfig.json
create mode 100644 ts/packages/commandExecutor/test/testClient.ts
create mode 100644 ts/packages/commandExecutor/test/tsconfig.json
create mode 100644 ts/packages/commandExecutor/tsconfig.json
create mode 100644 ts/packages/commandExecutor/view-logs.sh
diff --git a/ts/.mcp.json b/ts/.mcp.json
new file mode 100644
index 000000000..ed02dc794
--- /dev/null
+++ b/ts/.mcp.json
@@ -0,0 +1,11 @@
+{
+ "mcpServers": {
+ "command-executor": {
+ "command": "node",
+ "args": ["packages/commandExecutor/dist/server.js"],
+ "env": {
+ "DEBUG": "*"
+ }
+ }
+ }
+}
diff --git a/ts/packages/commandExecutor/README.md b/ts/packages/commandExecutor/README.md
new file mode 100644
index 000000000..9e33477b1
--- /dev/null
+++ b/ts/packages/commandExecutor/README.md
@@ -0,0 +1,179 @@
+# Command Executor MCP Server
+
+An MCP (Model Context Protocol) server that connects to the TypeAgent dispatcher to execute user commands like playing music, managing lists, working with calendars, and more.
+
+## Overview
+
+This MCP server acts as a bridge between Claude Code (or other MCP clients) and the TypeAgent system. It accepts natural language commands and forwards them to the TypeAgent dispatcher for execution.
+
+## Prerequisites
+
+1. **Built Package**: Build this package before using:
+ ```bash
+ pnpm run build
+ ```
+
+2. **TypeAgent Server** (optional at startup): The TypeAgent dispatcher server at `ws://localhost:8999`. The MCP server will automatically connect when the TypeAgent server becomes available and reconnect if the connection is lost.
+
+ Start the TypeAgent server with:
+ ```bash
+ pnpm run start:agent-server
+ ```
+
+## Configuration
+
+The server can be configured via environment variables or constructor parameters:
+
+- **AGENT_SERVER_URL**: WebSocket URL of the TypeAgent dispatcher (default: `ws://localhost:8999`)
+
+You can set this in the `.env` file at the root of the TypeAgent repository.
+
+## Installation
+
+### For Claude Code Users
+
+1. **Build the package** from the TypeAgent repository root:
+ ```bash
+ cd ts
+ pnpm run build
+ ```
+
+2. **Configure Claude Code** to use the MCP server. Add the following to your `.mcp.json` file in the TypeAgent repository root (create it if it doesn't exist):
+ ```json
+ {
+ "mcpServers": {
+ "command-executor": {
+ "command": "node",
+ "args": [
+ "packages/commandExecutor/dist/server.js"
+ ]
+ }
+ }
+ }
+ ```
+
+3. **Restart Claude Code** to load the MCP server configuration.
+
+4. **Start the TypeAgent server** (can be done before or after starting Claude Code):
+ ```bash
+ pnpm run start:agent-server
+ ```
+
+5. **Test it** by sending commands through Claude Code:
+ - "play bohemian rhapsody by queen"
+ - "what's on my grocery list"
+ - "add milk to my shopping list"
+
+### For Other MCP Clients
+
+The server is configured in `.mcp.json`:
+
+```json
+{
+ "mcpServers": {
+ "command-executor": {
+ "command": "node",
+ "args": [
+ "packages/commandExecutor/dist/server.js"
+ ]
+ }
+ }
+}
+```
+
+### Available Tools
+
+#### execute_command
+Execute user commands such as playing music, managing lists, or working with calendars.
+
+**Parameters:**
+- `request` (string): The natural language command to execute
+
+**Examples:**
+- "play sweet emotion by aerosmith"
+- "add jelly beans to my grocery list"
+- "schedule a meeting for tomorrow at 2pm"
+
+#### ping (debug mode)
+Test server connectivity.
+
+**Parameters:**
+- `message` (string): Message to echo back
+
+## Architecture
+
+```
+Claude Code (MCP Client)
+ ↓
+Command Executor MCP Server
+ ↓
+TypeAgent Dispatcher (WebSocket)
+ ↓
+TypeAgent Agents (Music, Lists, Calendar, etc.)
+```
+
+The MCP server:
+1. Receives commands from the MCP client
+2. Connects to the TypeAgent dispatcher via WebSocket
+3. Forwards commands to the dispatcher's `processCommand` method
+4. Returns results back to the client
+
+## Connection & Reconnection
+
+The MCP server includes automatic reconnection capabilities:
+
+- **Startup**: The server starts immediately, even if the TypeAgent dispatcher is not running
+- **Lazy Connection**: When you send the first command, it will attempt to connect if not already connected
+- **Auto-Reconnect**: Every 5 seconds, the server checks the connection and reconnects if needed
+- **Error Recovery**: If a command fails due to connection loss, the dispatcher is marked as disconnected and will automatically reconnect
+
+**Recommended workflow:**
+1. Start Claude Code (the MCP server starts automatically)
+2. Start the TypeAgent server: `pnpm run start:agent-server`
+3. Send commands - the MCP server will connect automatically
+
+You can also start the TypeAgent server first, or restart it at any time without restarting the MCP server.
+
+## Debugging and Logs
+
+The MCP server automatically logs all activity to both console and a log file for debugging.
+
+### Log File Location
+Logs are written to: `/tmp/typeagent-mcp/mcp-server-.log`
+
+### Viewing Logs
+Use the provided helper script to view the most recent log file:
+
+```bash
+# View the entire log
+./packages/commandExecutor/view-logs.sh
+
+# Follow the log in real-time
+./packages/commandExecutor/view-logs.sh -f
+```
+
+### What Gets Logged
+- Server initialization and configuration
+- Connection attempts to TypeAgent dispatcher
+- Connection success/failure with error details
+- Reconnection attempts
+- All incoming user requests
+- Command execution results
+- Errors with stack traces
+
+This is particularly useful for debugging connection issues between the MCP server and the TypeAgent dispatcher.
+
+## Development
+
+### Building
+```bash
+pnpm run build
+```
+
+### Running Standalone
+```bash
+pnpm run start
+```
+
+### Testing
+Use the MCP client (like Claude Code) to test commands, or use the TypeAgent CLI to verify the dispatcher is working.
diff --git a/ts/packages/commandExecutor/dist-test/testClient.d.ts b/ts/packages/commandExecutor/dist-test/testClient.d.ts
new file mode 100644
index 000000000..9e721977d
--- /dev/null
+++ b/ts/packages/commandExecutor/dist-test/testClient.d.ts
@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=testClient.d.ts.map
\ No newline at end of file
diff --git a/ts/packages/commandExecutor/dist-test/testClient.js b/ts/packages/commandExecutor/dist-test/testClient.js
new file mode 100644
index 000000000..b3587ca7a
--- /dev/null
+++ b/ts/packages/commandExecutor/dist-test/testClient.js
@@ -0,0 +1,61 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
+async function testCommandServer() {
+ console.log("Creating MCP client...");
+ const client = new Client({
+ name: "Test-Client",
+ version: "1.0.0",
+ });
+ const transport = new StdioClientTransport({
+ command: "node",
+ args: ["../dist/server.js"],
+ });
+ await client.connect(transport);
+ console.log("Connected to Command Executor Server\n");
+ // Test 1: Execute a music command
+ console.log("Test 1: Play music command");
+ const musicResult = (await client.callTool({
+ name: "execute_command",
+ arguments: { request: "play shake it off by taylor swift" },
+ }));
+ console.log("Response:", musicResult.content[0].type === "text"
+ ? musicResult.content[0].text
+ : "No text response");
+ console.log();
+ // Test 2: Execute a list command
+ console.log("Test 2: Add to list command");
+ const listResult = (await client.callTool({
+ name: "execute_command",
+ arguments: { request: "add ham to my grocery list" },
+ }));
+ console.log("Response:", listResult.content[0].type === "text"
+ ? listResult.content[0].text
+ : "No text response");
+ console.log();
+ // Test 3: Execute a calendar command
+ console.log("Test 3: Calendar command");
+ const calendarResult = (await client.callTool({
+ name: "execute_command",
+ arguments: { request: "add meeting tomorrow at 3pm" },
+ }));
+ console.log("Response:", calendarResult.content[0].type === "text"
+ ? calendarResult.content[0].text
+ : "No text response");
+ console.log();
+ // Test 4: Ping diagnostic tool
+ console.log("Test 4: Ping diagnostic");
+ const pingResult = (await client.callTool({
+ name: "ping",
+ arguments: { message: "test connection" },
+ }));
+ console.log("Response:", pingResult.content[0].type === "text"
+ ? pingResult.content[0].text
+ : "No text response");
+ console.log();
+ await client.close();
+ console.log("All tests completed successfully!");
+}
+testCommandServer().catch(console.error);
+//# sourceMappingURL=testClient.js.map
\ No newline at end of file
diff --git a/ts/packages/commandExecutor/examples/claude_desktop_config.json b/ts/packages/commandExecutor/examples/claude_desktop_config.json
new file mode 100644
index 000000000..3be97d654
--- /dev/null
+++ b/ts/packages/commandExecutor/examples/claude_desktop_config.json
@@ -0,0 +1,10 @@
+{
+ "mcpServers": {
+ "command-executor": {
+ "command": "node",
+ "args": [
+ "C:/Users/stevenlucco/src/TypeAgent/ts/packages/commandExecutor/dist/server.js"
+ ]
+ }
+ }
+}
diff --git a/ts/packages/commandExecutor/examples/example_usage.md b/ts/packages/commandExecutor/examples/example_usage.md
new file mode 100644
index 000000000..395028622
--- /dev/null
+++ b/ts/packages/commandExecutor/examples/example_usage.md
@@ -0,0 +1,76 @@
+# Command Executor MCP Server - Example Usage
+
+## Configuration
+
+To use this MCP server with Claude Desktop, add the following to your Claude Desktop configuration:
+
+### Windows
+Edit `%APPDATA%\Claude\claude_desktop_config.json`:
+
+```json
+{
+ "mcpServers": {
+ "command-executor": {
+ "command": "node",
+ "args": [
+ "C:/Users/YOUR_USERNAME/src/TypeAgent/ts/packages/commandExecutor/dist/server.js"
+ ]
+ }
+ }
+}
+```
+
+### macOS
+Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
+
+```json
+{
+ "mcpServers": {
+ "command-executor": {
+ "command": "node",
+ "args": [
+ "/Users/YOUR_USERNAME/src/TypeAgent/ts/packages/commandExecutor/dist/server.js"
+ ]
+ }
+ }
+}
+```
+
+## Example Commands
+
+Once configured, you can ask Claude to execute various commands:
+
+### Music Commands
+- "Play shake it off by taylor swift"
+- "Skip to the next song"
+- "Pause the music"
+- "Set volume to 50%"
+
+### List Management
+- "Add ham to my grocery list"
+- "Add milk, eggs, and bread to shopping list"
+- "Remove bananas from grocery list"
+- "Show me my grocery list"
+
+### Calendar Operations
+- "Add meeting tomorrow at 3pm"
+- "Schedule dentist appointment for next Tuesday at 10am"
+- "What's on my calendar today?"
+- "Cancel my 2pm meeting"
+
+## Testing
+
+The server will log all incoming requests to the console. You can verify it's working by:
+
+1. Restart Claude Desktop after updating the configuration
+2. Send a command like "Add ham to my grocery list"
+3. Claude will use the `execute_command` tool and receive: `Finished add ham to my grocery list`
+
+## Current Behavior
+
+The MCP server currently:
+- Accepts command requests via the `execute_command` tool
+- Logs the request to the console
+- Returns a success message to Claude
+
+Future versions will integrate with an actual command execution service to perform the requested actions.
diff --git a/ts/packages/commandExecutor/package-lock.json b/ts/packages/commandExecutor/package-lock.json
new file mode 100644
index 000000000..6461246fd
--- /dev/null
+++ b/ts/packages/commandExecutor/package-lock.json
@@ -0,0 +1,1501 @@
+{
+ "name": "command-executor-mcp",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "command-executor-mcp",
+ "version": "0.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.25.2",
+ "dotenv": "^16.3.1",
+ "zod": "^4.1.13"
+ },
+ "devDependencies": {
+ "prettier": "^3.2.5",
+ "rimraf": "^5.0.5",
+ "typescript": "~5.4.5"
+ }
+ },
+ "node_modules/@hono/node-server": {
+ "version": "1.19.9",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
+ "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
+ "engines": {
+ "node": ">=18.14.1"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.25.2",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz",
+ "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==",
+ "dependencies": {
+ "@hono/node-server": "^1.19.7",
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.0.1",
+ "express-rate-limit": "^7.5.0",
+ "jose": "^6.1.1",
+ "json-schema-typed": "^8.0.2",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.25 || ^4.0",
+ "zod-to-json-schema": "^3.25.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "@cfworker/json-schema": {
+ "optional": true
+ },
+ "zod": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventsource": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
+ "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
+ "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": ">= 4.11"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ]
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "dev": true,
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hono": {
+ "version": "4.11.4",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz",
+ "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==",
+ "peer": true,
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jose": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz",
+ "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
+ },
+ "node_modules/json-schema-typed": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
+ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/pkce-challenge": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
+ "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.0.tgz",
+ "integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "5.0.10",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
+ "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^10.3.7"
+ },
+ "bin": {
+ "rimraf": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
+ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "node_modules/zod": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
+ "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
+ "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
+ "peerDependencies": {
+ "zod": "^3.25 || ^4"
+ }
+ }
+ }
+}
diff --git a/ts/packages/commandExecutor/package.json b/ts/packages/commandExecutor/package.json
new file mode 100644
index 000000000..71a04e9cc
--- /dev/null
+++ b/ts/packages/commandExecutor/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "command-executor-mcp",
+ "version": "0.0.1",
+ "private": true,
+ "description": "MCP server for executing user commands like playing music, managing lists, and working with calendars.",
+ "homepage": "https://github.com/microsoft/TypeAgent#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/microsoft/TypeAgent.git",
+ "directory": "ts/packages/commandExecutor"
+ },
+ "license": "MIT",
+ "author": "Microsoft",
+ "type": "module",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "scripts": {
+ "build": "npm run tsc",
+ "clean": "rimraf --glob dist *.tsbuildinfo *.done.build.log",
+ "prettier": "prettier --check . --ignore-path ../../.prettierignore",
+ "prettier:fix": "prettier --write . --ignore-path ../../.prettierignore",
+ "tsc": "tsc -b",
+ "start": "node dist/server.js"
+ },
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.25.2",
+ "@typeagent/agent-sdk": "workspace:*",
+ "@typeagent/agent-server-client": "workspace:*",
+ "@typeagent/dispatcher-types": "workspace:*",
+ "dotenv": "^16.3.1",
+ "isomorphic-ws": "^5.0.0",
+ "zod": "^4.1.13"
+ },
+ "devDependencies": {
+ "prettier": "^3.2.5",
+ "rimraf": "^5.0.5",
+ "typescript": "~5.4.5"
+ }
+}
diff --git a/ts/packages/commandExecutor/src/commandServer.ts b/ts/packages/commandExecutor/src/commandServer.ts
new file mode 100644
index 000000000..93dfc123b
--- /dev/null
+++ b/ts/packages/commandExecutor/src/commandServer.ts
@@ -0,0 +1,370 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { z } from "zod/v4";
+import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { connectDispatcher } from "@typeagent/agent-server-client";
+import type {
+ ClientIO,
+ IAgentMessage,
+ RequestId,
+ TemplateEditConfig,
+} from "@typeagent/dispatcher-types";
+import type { Dispatcher } from "@typeagent/dispatcher-types";
+import { DisplayAppendMode } from "@typeagent/agent-sdk";
+import * as fs from "fs";
+import * as path from "path";
+import * as os from "os";
+
+function executeCommandRequestSchema() {
+ return {
+ request: z.string(),
+ };
+}
+const ExecuteCommandRequestSchema = z.object(executeCommandRequestSchema());
+
+export type ExecuteCommandRequest = z.infer<
+ typeof ExecuteCommandRequestSchema
+>;
+
+function toolResult(result: string): CallToolResult {
+ return {
+ content: [{ type: "text", text: result }],
+ };
+}
+
+/**
+ * Logger utility that writes to both console and a log file
+ */
+class Logger {
+ private logFilePath: string;
+ private logStream: fs.WriteStream;
+
+ constructor() {
+ const logDir = path.join(os.tmpdir(), "typeagent-mcp");
+ if (!fs.existsSync(logDir)) {
+ fs.mkdirSync(logDir, { recursive: true });
+ }
+ this.logFilePath = path.join(logDir, `mcp-server-${Date.now()}.log`);
+ this.logStream = fs.createWriteStream(this.logFilePath, { flags: "a" });
+ this.log(`Log file created at: ${this.logFilePath}`);
+ }
+
+ private formatMessage(level: string, message: string): string {
+ const timestamp = new Date().toISOString();
+ return `[${timestamp}] [${level}] ${message}`;
+ }
+
+ log(message: string): void {
+ const formatted = this.formatMessage("INFO", message);
+ console.log(formatted);
+ this.logStream.write(formatted + "\n");
+ }
+
+ error(message: string, error?: any): void {
+ const errorDetails = error ? ` - ${error instanceof Error ? error.message : String(error)}` : "";
+ const formatted = this.formatMessage("ERROR", message + errorDetails);
+ console.error(formatted);
+ this.logStream.write(formatted + "\n");
+ if (error?.stack) {
+ this.logStream.write(error.stack + "\n");
+ }
+ }
+
+ getLogFilePath(): string {
+ return this.logFilePath;
+ }
+
+ close(): void {
+ this.logStream.end();
+ }
+}
+
+/**
+ * Remove ANSI escape codes from a string
+ */
+function stripAnsi(text: string): string {
+ // eslint-disable-next-line no-control-regex
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
+}
+
+// downloadImage function removed - images are not downloaded or displayed
+
+/**
+ * Process HTML content to download images and replace img tags with file references
+ */
+async function processHtmlImages(content: string): Promise {
+ // Find all img tags with src attributes
+ const imgRegex = /
]+src="([^"]+)"[^>]*>/gi;
+ let processed = content;
+ const matches = [...content.matchAll(imgRegex)];
+
+ for (const match of matches) {
+ const fullTag = match[0];
+
+ // Just remove the img tag entirely - don't download or display artwork
+ processed = processed.replace(fullTag, '');
+ }
+
+ return processed;
+}
+
+/**
+ * Minimal ClientIO implementation for MCP server
+ * Most methods are no-ops since we just need to satisfy the interface
+ */
+function createMcpClientIO(logger: Logger, responseCollector: { messages: string[] }): ClientIO {
+ return {
+ clear(): void {
+ logger.log("ClientIO: clear() called");
+ },
+ exit(): void {
+ logger.log("ClientIO: exit() called");
+ },
+ setDisplayInfo(): void {},
+ setDisplay(message: IAgentMessage): void {
+ logger.log(`ClientIO: setDisplay() - ${JSON.stringify(message)}`);
+ if (typeof message === 'object' && 'message' in message) {
+ const msg = message.message;
+ // Filter out "info" kind messages (technical translation details)
+ if (typeof msg === 'object' && msg && 'kind' in msg && msg.kind === 'info') {
+ return;
+ }
+
+ if (typeof msg === 'string') {
+ responseCollector.messages.push(stripAnsi(msg));
+ } else if (typeof msg === 'object' && msg && 'content' in msg) {
+ responseCollector.messages.push(stripAnsi(String(msg.content)));
+ }
+ }
+ },
+ appendDisplay(message: IAgentMessage, mode: DisplayAppendMode): void {
+ logger.log(`ClientIO: appendDisplay(mode=${mode}) - ${JSON.stringify(message)}`);
+ // Only capture block mode messages (final results), not temporary status messages
+ if (mode === 'block' && typeof message === 'object' && 'message' in message) {
+ const msg = message.message;
+ // Filter out "info" kind messages (technical translation details)
+ if (typeof msg === 'object' && msg && 'kind' in msg && msg.kind === 'info') {
+ return;
+ }
+
+ if (typeof msg === 'string') {
+ responseCollector.messages.push(stripAnsi(msg));
+ } else if (typeof msg === 'object' && msg && 'content' in msg) {
+ responseCollector.messages.push(stripAnsi(String(msg.content)));
+ }
+ }
+ },
+ appendDiagnosticData(requestId: RequestId, data: any): void {
+ logger.log(`ClientIO: appendDiagnosticData(requestId=${requestId}) - ${JSON.stringify(data)}`);
+ },
+ setDynamicDisplay(): void {},
+ async askYesNo(
+ message: string,
+ requestId: RequestId,
+ defaultValue?: boolean,
+ ): Promise {
+ logger.log(`ClientIO: askYesNo(requestId=${requestId}) - "${message}" (defaulting to ${defaultValue ?? false})`);
+ return defaultValue ?? false;
+ },
+ async proposeAction(
+ actionTemplates: TemplateEditConfig,
+ requestId: RequestId,
+ source: string,
+ ): Promise {
+ logger.log(`ClientIO: proposeAction(requestId=${requestId}, source=${source}) - ${JSON.stringify(actionTemplates)}`);
+ return undefined;
+ },
+ async popupQuestion(
+ message: string,
+ choices: string[],
+ defaultId: number | undefined,
+ source: string,
+ ): Promise {
+ logger.log(`ClientIO: popupQuestion(source=${source}) - "${message}" choices=[${choices.join(", ")}] (defaulting to ${defaultId ?? 0})`);
+ return defaultId ?? 0;
+ },
+ notify(event: string, requestId: RequestId, data: any, source: string): void {
+ logger.log(`ClientIO: notify(event=${event}, requestId=${requestId}, source=${source}) - ${JSON.stringify(data)}`);
+ },
+ openLocalView(port: number): void {
+ logger.log(`ClientIO: openLocalView(port=${port})`);
+ },
+ closeLocalView(port: number): void {
+ logger.log(`ClientIO: closeLocalView(port=${port})`);
+ },
+ takeAction(action: string, data: unknown): void {
+ logger.log(`ClientIO: takeAction(action=${action}) - ${JSON.stringify(data)}`);
+ },
+ };
+}
+
+export class CommandServer {
+ public server: McpServer;
+ private dispatcher: Dispatcher | null = null;
+ private agentServerUrl: string;
+ private reconnectInterval: NodeJS.Timeout | null = null;
+ private isConnecting: boolean = false;
+ private reconnectDelayMs: number = 5000; // 5 seconds between reconnection attempts
+ private logger: Logger;
+ private responseCollector: { messages: string[] } = { messages: [] };
+
+ /**
+ * Creates a new CommandServer instance
+ * @param debugMode Enable debug mode for diagnostic tools
+ * @param agentServerUrl URL of the TypeAgent dispatcher server (default: ws://localhost:8999)
+ */
+ constructor(debugMode: boolean = true, agentServerUrl?: string) {
+ this.logger = new Logger();
+ this.server = new McpServer({
+ name: "Command-Executor-Server",
+ version: "1.0.0",
+ });
+ this.agentServerUrl = agentServerUrl ?? process.env.AGENT_SERVER_URL ?? "ws://localhost:8999";
+ this.logger.log(`CommandServer initializing with TypeAgent server URL: ${this.agentServerUrl}`);
+ this.addTools();
+ if (debugMode) {
+ this.addDiagnosticTools();
+ }
+ }
+
+ public async start(transport?: StdioServerTransport): Promise {
+ transport ??= new StdioServerTransport();
+ await this.server.connect(transport);
+
+ // Connect to the TypeAgent dispatcher
+ await this.connectToDispatcher();
+
+ // Start reconnection monitoring
+ this.startReconnectionMonitoring();
+ }
+
+ private async connectToDispatcher(): Promise {
+ if (this.isConnecting) {
+ return;
+ }
+
+ this.isConnecting = true;
+ try {
+ const clientIO = createMcpClientIO(this.logger, this.responseCollector);
+ this.dispatcher = await connectDispatcher(clientIO, this.agentServerUrl);
+ this.logger.log(`Connected to TypeAgent dispatcher at ${this.agentServerUrl}`);
+ } catch (error) {
+ this.logger.error(`Failed to connect to dispatcher at ${this.agentServerUrl}`, error);
+ this.logger.error("Will retry connection automatically. Make sure the TypeAgent server is running.");
+ this.dispatcher = null;
+ } finally {
+ this.isConnecting = false;
+ }
+ }
+
+ private startReconnectionMonitoring(): void {
+ // Check connection status periodically and reconnect if needed
+ this.reconnectInterval = setInterval(async () => {
+ if (!this.dispatcher && !this.isConnecting) {
+ this.logger.log("Attempting to reconnect to TypeAgent dispatcher...");
+ await this.connectToDispatcher();
+ }
+ }, this.reconnectDelayMs);
+ }
+
+ private stopReconnectionMonitoring(): void {
+ if (this.reconnectInterval) {
+ clearInterval(this.reconnectInterval);
+ this.reconnectInterval = null;
+ }
+ }
+
+ public async close(): Promise {
+ this.stopReconnectionMonitoring();
+ if (this.dispatcher) {
+ await this.dispatcher.close();
+ this.dispatcher = null;
+ }
+ this.logger.close();
+ }
+
+ private addTools() {
+ this.server.registerTool(
+ "execute_command",
+ {
+ inputSchema: executeCommandRequestSchema(),
+ description:
+ "Execute a user command such as playing music, managing lists, or working with calendars",
+ },
+ async (request: ExecuteCommandRequest) =>
+ this.executeCommand(request),
+ );
+ }
+
+ public async executeCommand(
+ request: ExecuteCommandRequest,
+ ): Promise {
+ this.logger.log(`User request: ${request.request}`);
+
+ // If not connected, try to connect now (lazy connection)
+ if (!this.dispatcher && !this.isConnecting) {
+ this.logger.log("Not connected to dispatcher, attempting to connect...");
+ await this.connectToDispatcher();
+ }
+
+ if (!this.dispatcher) {
+ const errorMsg = `Cannot execute command: not connected to TypeAgent dispatcher at ${this.agentServerUrl}. Make sure the TypeAgent server is running with: pnpm run start:agent-server`;
+ this.logger.error(errorMsg);
+ return toolResult(errorMsg);
+ }
+
+ try {
+ // Clear response collector before processing new command
+ this.responseCollector.messages = [];
+
+ // Process the command through the TypeAgent dispatcher
+ this.logger.log(`Sending command to dispatcher: ${request.request}`);
+ const result = await this.dispatcher.processCommand(request.request);
+
+ if (result?.lastError) {
+ this.logger.error(`Command execution error: ${result.lastError}`);
+ return toolResult(`Error executing command: ${result.lastError}`);
+ }
+
+ // Return the collected messages from the dispatcher
+ this.logger.log(`Successfully executed command: ${request.request}`);
+
+ if (this.responseCollector.messages.length > 0) {
+ const response = this.responseCollector.messages.join('\n\n');
+ // Process any HTML images in the response
+ const processedResponse = await processHtmlImages(response);
+ return toolResult(processedResponse);
+ }
+
+ // Fallback if no messages were collected
+ return toolResult(`Successfully executed: ${request.request}`);
+ } catch (error) {
+ const errorMsg = `Failed to execute command: ${error instanceof Error ? error.message : String(error)}`;
+ this.logger.error(errorMsg);
+
+ // Mark dispatcher as disconnected so we'll try to reconnect
+ this.dispatcher = null;
+
+ return toolResult(errorMsg);
+ }
+ }
+
+ private addDiagnosticTools() {
+ this.server.registerTool(
+ "ping",
+ {
+ inputSchema: { message: z.string() },
+ description: "Ping the server to test connectivity",
+ },
+ async (request: { message: string }) => {
+ const response = request.message
+ ? "PONG: " + request.message
+ : "pong";
+ return toolResult(response);
+ },
+ );
+ }
+}
diff --git a/ts/packages/commandExecutor/src/index.ts b/ts/packages/commandExecutor/src/index.ts
new file mode 100644
index 000000000..4ead999f7
--- /dev/null
+++ b/ts/packages/commandExecutor/src/index.ts
@@ -0,0 +1,4 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+export { CommandServer, ExecuteCommandRequest } from "./commandServer.js";
diff --git a/ts/packages/commandExecutor/src/server.ts b/ts/packages/commandExecutor/src/server.ts
new file mode 100644
index 000000000..b8584d0f4
--- /dev/null
+++ b/ts/packages/commandExecutor/src/server.ts
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { CommandServer } from "./commandServer.js";
+import dotenv from "dotenv";
+
+const envPath = new URL("../../../.env", import.meta.url);
+dotenv.config({ path: envPath });
+
+console.log("Starting Command Executor Server");
+
+const commandServer = new CommandServer();
+await commandServer.start();
+
+console.log("Exit Command Executor Server");
diff --git a/ts/packages/commandExecutor/src/tsconfig.json b/ts/packages/commandExecutor/src/tsconfig.json
new file mode 100644
index 000000000..6e2a4fdd3
--- /dev/null
+++ b/ts/packages/commandExecutor/src/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "../dist"
+ }
+}
diff --git a/ts/packages/commandExecutor/test/testClient.ts b/ts/packages/commandExecutor/test/testClient.ts
new file mode 100644
index 000000000..26446c416
--- /dev/null
+++ b/ts/packages/commandExecutor/test/testClient.ts
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
+import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+
+async function testCommandServer() {
+ console.log("Creating MCP client...");
+
+ const client = new Client({
+ name: "Test-Client",
+ version: "1.0.0",
+ });
+
+ const transport = new StdioClientTransport({
+ command: "node",
+ args: ["../dist/server.js"],
+ });
+
+ await client.connect(transport);
+ console.log("Connected to Command Executor Server\n");
+
+ // Test 1: Execute a music command
+ console.log("Test 1: Play music command");
+ const musicResult = (await client.callTool({
+ name: "execute_command",
+ arguments: { request: "play shake it off by taylor swift" },
+ })) as CallToolResult;
+ console.log(
+ "Response:",
+ musicResult.content[0].type === "text"
+ ? musicResult.content[0].text
+ : "No text response",
+ );
+ console.log();
+
+ // Test 2: Execute a list command
+ console.log("Test 2: Add to list command");
+ const listResult = (await client.callTool({
+ name: "execute_command",
+ arguments: { request: "add ham to my grocery list" },
+ })) as CallToolResult;
+ console.log(
+ "Response:",
+ listResult.content[0].type === "text"
+ ? listResult.content[0].text
+ : "No text response",
+ );
+ console.log();
+
+ // Test 3: Execute a calendar command
+ console.log("Test 3: Calendar command");
+ const calendarResult = (await client.callTool({
+ name: "execute_command",
+ arguments: { request: "add meeting tomorrow at 3pm" },
+ })) as CallToolResult;
+ console.log(
+ "Response:",
+ calendarResult.content[0].type === "text"
+ ? calendarResult.content[0].text
+ : "No text response",
+ );
+ console.log();
+
+ // Test 4: Ping diagnostic tool
+ console.log("Test 4: Ping diagnostic");
+ const pingResult = (await client.callTool({
+ name: "ping",
+ arguments: { message: "test connection" },
+ })) as CallToolResult;
+ console.log(
+ "Response:",
+ pingResult.content[0].type === "text"
+ ? pingResult.content[0].text
+ : "No text response",
+ );
+ console.log();
+
+ await client.close();
+ console.log("All tests completed successfully!");
+}
+
+testCommandServer().catch(console.error);
diff --git a/ts/packages/commandExecutor/test/tsconfig.json b/ts/packages/commandExecutor/test/tsconfig.json
new file mode 100644
index 000000000..2881fdbb9
--- /dev/null
+++ b/ts/packages/commandExecutor/test/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "../dist-test"
+ }
+}
diff --git a/ts/packages/commandExecutor/tsconfig.json b/ts/packages/commandExecutor/tsconfig.json
new file mode 100644
index 000000000..b6e1577e4
--- /dev/null
+++ b/ts/packages/commandExecutor/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "composite": true
+ },
+ "include": [],
+ "references": [{ "path": "./src" }],
+ "ts-node": {
+ "esm": true
+ }
+}
diff --git a/ts/packages/commandExecutor/view-logs.sh b/ts/packages/commandExecutor/view-logs.sh
new file mode 100644
index 000000000..5c1e76a0e
--- /dev/null
+++ b/ts/packages/commandExecutor/view-logs.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+# Helper script to view MCP server logs
+
+LOG_DIR="/tmp/typeagent-mcp"
+
+if [ ! -d "$LOG_DIR" ]; then
+ echo "No logs found yet. The log directory will be created when the MCP server starts."
+ echo "Expected location: $LOG_DIR"
+ exit 0
+fi
+
+# Find the most recent log file
+LATEST_LOG=$(ls -t "$LOG_DIR"/mcp-server-*.log 2>/dev/null | head -1)
+
+if [ -z "$LATEST_LOG" ]; then
+ echo "No log files found in $LOG_DIR"
+ exit 0
+fi
+
+echo "Viewing log file: $LATEST_LOG"
+echo "=================================================================================="
+echo ""
+
+# Check if we should tail (follow) the log
+if [ "$1" == "-f" ] || [ "$1" == "--follow" ]; then
+ tail -f "$LATEST_LOG"
+else
+ cat "$LATEST_LOG"
+fi
diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml
index 5e4f62a49..66fdcac77 100644
--- a/ts/pnpm-lock.yaml
+++ b/ts/pnpm-lock.yaml
@@ -2813,6 +2813,40 @@ importers:
specifier: ^6.0.1
version: 6.0.1
+ packages/commandExecutor:
+ dependencies:
+ '@modelcontextprotocol/sdk':
+ specifier: ^1.25.2
+ version: 1.25.2(hono@4.11.3)(zod@4.1.13)
+ '@typeagent/agent-sdk':
+ specifier: workspace:*
+ version: link:../agentSdk
+ '@typeagent/agent-server-client':
+ specifier: workspace:*
+ version: link:../agentServer/client
+ '@typeagent/dispatcher-types':
+ specifier: workspace:*
+ version: link:../dispatcher/types
+ dotenv:
+ specifier: ^16.3.1
+ version: 16.5.0
+ isomorphic-ws:
+ specifier: ^5.0.0
+ version: 5.0.0(ws@8.18.2)
+ zod:
+ specifier: ^4.1.13
+ version: 4.1.13
+ devDependencies:
+ prettier:
+ specifier: ^3.2.5
+ version: 3.5.3
+ rimraf:
+ specifier: ^5.0.5
+ version: 5.0.10
+ typescript:
+ specifier: ~5.4.5
+ version: 5.4.5
+
packages/defaultAgentProvider:
dependencies:
'@modelcontextprotocol/sdk':
From bf20673511a2149b33274bf768986b2a961c24c8 Mon Sep 17 00:00:00 2001
From: steveluc
Date: Fri, 16 Jan 2026 23:22:43 -0800
Subject: [PATCH 02/21] Add copyright header to view-logs.sh
Co-Authored-By: Claude Sonnet 4.5
---
ts/packages/commandExecutor/view-logs.sh | 3 +++
1 file changed, 3 insertions(+)
diff --git a/ts/packages/commandExecutor/view-logs.sh b/ts/packages/commandExecutor/view-logs.sh
index 5c1e76a0e..775d9b7a0 100644
--- a/ts/packages/commandExecutor/view-logs.sh
+++ b/ts/packages/commandExecutor/view-logs.sh
@@ -1,4 +1,7 @@
#!/bin/bash
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
# Helper script to view MCP server logs
LOG_DIR="/tmp/typeagent-mcp"
From 7c2a81a579f09e165ce6a697a40c3025245aad7d Mon Sep 17 00:00:00 2001
From: steveluc
Date: Fri, 16 Jan 2026 23:29:37 -0800
Subject: [PATCH 03/21] Fix repo policy issues
- Add trademark section to README
- Remove dist-test build outputs from git
- Fix package.json field ordering (use exports instead of main)
Co-Authored-By: Claude Sonnet 4.5
---
ts/packages/commandExecutor/README.md | 4 ++
.../commandExecutor/dist-test/testClient.d.ts | 2 -
.../commandExecutor/dist-test/testClient.js | 61 -------------------
ts/packages/commandExecutor/package.json | 6 +-
4 files changed, 8 insertions(+), 65 deletions(-)
delete mode 100644 ts/packages/commandExecutor/dist-test/testClient.d.ts
delete mode 100644 ts/packages/commandExecutor/dist-test/testClient.js
diff --git a/ts/packages/commandExecutor/README.md b/ts/packages/commandExecutor/README.md
index 9e33477b1..b7ff2173e 100644
--- a/ts/packages/commandExecutor/README.md
+++ b/ts/packages/commandExecutor/README.md
@@ -177,3 +177,7 @@ pnpm run start
### Testing
Use the MCP client (like Claude Code) to test commands, or use the TypeAgent CLI to verify the dispatcher is working.
+
+## Trademarks
+
+This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies.
diff --git a/ts/packages/commandExecutor/dist-test/testClient.d.ts b/ts/packages/commandExecutor/dist-test/testClient.d.ts
deleted file mode 100644
index 9e721977d..000000000
--- a/ts/packages/commandExecutor/dist-test/testClient.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export {};
-//# sourceMappingURL=testClient.d.ts.map
\ No newline at end of file
diff --git a/ts/packages/commandExecutor/dist-test/testClient.js b/ts/packages/commandExecutor/dist-test/testClient.js
deleted file mode 100644
index b3587ca7a..000000000
--- a/ts/packages/commandExecutor/dist-test/testClient.js
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-import { Client } from "@modelcontextprotocol/sdk/client/index.js";
-import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
-async function testCommandServer() {
- console.log("Creating MCP client...");
- const client = new Client({
- name: "Test-Client",
- version: "1.0.0",
- });
- const transport = new StdioClientTransport({
- command: "node",
- args: ["../dist/server.js"],
- });
- await client.connect(transport);
- console.log("Connected to Command Executor Server\n");
- // Test 1: Execute a music command
- console.log("Test 1: Play music command");
- const musicResult = (await client.callTool({
- name: "execute_command",
- arguments: { request: "play shake it off by taylor swift" },
- }));
- console.log("Response:", musicResult.content[0].type === "text"
- ? musicResult.content[0].text
- : "No text response");
- console.log();
- // Test 2: Execute a list command
- console.log("Test 2: Add to list command");
- const listResult = (await client.callTool({
- name: "execute_command",
- arguments: { request: "add ham to my grocery list" },
- }));
- console.log("Response:", listResult.content[0].type === "text"
- ? listResult.content[0].text
- : "No text response");
- console.log();
- // Test 3: Execute a calendar command
- console.log("Test 3: Calendar command");
- const calendarResult = (await client.callTool({
- name: "execute_command",
- arguments: { request: "add meeting tomorrow at 3pm" },
- }));
- console.log("Response:", calendarResult.content[0].type === "text"
- ? calendarResult.content[0].text
- : "No text response");
- console.log();
- // Test 4: Ping diagnostic tool
- console.log("Test 4: Ping diagnostic");
- const pingResult = (await client.callTool({
- name: "ping",
- arguments: { message: "test connection" },
- }));
- console.log("Response:", pingResult.content[0].type === "text"
- ? pingResult.content[0].text
- : "No text response");
- console.log();
- await client.close();
- console.log("All tests completed successfully!");
-}
-testCommandServer().catch(console.error);
-//# sourceMappingURL=testClient.js.map
\ No newline at end of file
diff --git a/ts/packages/commandExecutor/package.json b/ts/packages/commandExecutor/package.json
index 71a04e9cc..25eb2f9e7 100644
--- a/ts/packages/commandExecutor/package.json
+++ b/ts/packages/commandExecutor/package.json
@@ -12,8 +12,10 @@
"license": "MIT",
"author": "Microsoft",
"type": "module",
- "main": "dist/index.js",
- "types": "dist/index.d.ts",
+ "exports": {
+ ".": "./dist/index.js"
+ },
+ "types": "./dist/index.d.ts",
"scripts": {
"build": "npm run tsc",
"clean": "rimraf --glob dist *.tsbuildinfo *.done.build.log",
From 82a19109894364a6ed998925ab7b2c5e9f2b32ad Mon Sep 17 00:00:00 2001
From: steveluc
Date: Fri, 16 Jan 2026 23:30:52 -0800
Subject: [PATCH 04/21] Run prettier to format files
Co-Authored-By: Claude Sonnet 4.5
---
ts/packages/commandExecutor/README.md | 26 ++-
.../commandExecutor/examples/example_usage.md | 6 +
.../commandExecutor/src/commandServer.ts | 150 +++++++++++++-----
3 files changed, 137 insertions(+), 45 deletions(-)
diff --git a/ts/packages/commandExecutor/README.md b/ts/packages/commandExecutor/README.md
index b7ff2173e..be504ede3 100644
--- a/ts/packages/commandExecutor/README.md
+++ b/ts/packages/commandExecutor/README.md
@@ -9,6 +9,7 @@ This MCP server acts as a bridge between Claude Code (or other MCP clients) and
## Prerequisites
1. **Built Package**: Build this package before using:
+
```bash
pnpm run build
```
@@ -16,6 +17,7 @@ This MCP server acts as a bridge between Claude Code (or other MCP clients) and
2. **TypeAgent Server** (optional at startup): The TypeAgent dispatcher server at `ws://localhost:8999`. The MCP server will automatically connect when the TypeAgent server becomes available and reconnect if the connection is lost.
Start the TypeAgent server with:
+
```bash
pnpm run start:agent-server
```
@@ -33,20 +35,20 @@ You can set this in the `.env` file at the root of the TypeAgent repository.
### For Claude Code Users
1. **Build the package** from the TypeAgent repository root:
+
```bash
cd ts
pnpm run build
```
2. **Configure Claude Code** to use the MCP server. Add the following to your `.mcp.json` file in the TypeAgent repository root (create it if it doesn't exist):
+
```json
{
"mcpServers": {
"command-executor": {
"command": "node",
- "args": [
- "packages/commandExecutor/dist/server.js"
- ]
+ "args": ["packages/commandExecutor/dist/server.js"]
}
}
}
@@ -55,6 +57,7 @@ You can set this in the `.env` file at the root of the TypeAgent repository.
3. **Restart Claude Code** to load the MCP server configuration.
4. **Start the TypeAgent server** (can be done before or after starting Claude Code):
+
```bash
pnpm run start:agent-server
```
@@ -73,9 +76,7 @@ The server is configured in `.mcp.json`:
"mcpServers": {
"command-executor": {
"command": "node",
- "args": [
- "packages/commandExecutor/dist/server.js"
- ]
+ "args": ["packages/commandExecutor/dist/server.js"]
}
}
}
@@ -84,20 +85,25 @@ The server is configured in `.mcp.json`:
### Available Tools
#### execute_command
+
Execute user commands such as playing music, managing lists, or working with calendars.
**Parameters:**
+
- `request` (string): The natural language command to execute
**Examples:**
+
- "play sweet emotion by aerosmith"
- "add jelly beans to my grocery list"
- "schedule a meeting for tomorrow at 2pm"
#### ping (debug mode)
+
Test server connectivity.
**Parameters:**
+
- `message` (string): Message to echo back
## Architecture
@@ -113,6 +119,7 @@ TypeAgent Agents (Music, Lists, Calendar, etc.)
```
The MCP server:
+
1. Receives commands from the MCP client
2. Connects to the TypeAgent dispatcher via WebSocket
3. Forwards commands to the dispatcher's `processCommand` method
@@ -128,6 +135,7 @@ The MCP server includes automatic reconnection capabilities:
- **Error Recovery**: If a command fails due to connection loss, the dispatcher is marked as disconnected and will automatically reconnect
**Recommended workflow:**
+
1. Start Claude Code (the MCP server starts automatically)
2. Start the TypeAgent server: `pnpm run start:agent-server`
3. Send commands - the MCP server will connect automatically
@@ -139,9 +147,11 @@ You can also start the TypeAgent server first, or restart it at any time without
The MCP server automatically logs all activity to both console and a log file for debugging.
### Log File Location
+
Logs are written to: `/tmp/typeagent-mcp/mcp-server-.log`
### Viewing Logs
+
Use the provided helper script to view the most recent log file:
```bash
@@ -153,6 +163,7 @@ Use the provided helper script to view the most recent log file:
```
### What Gets Logged
+
- Server initialization and configuration
- Connection attempts to TypeAgent dispatcher
- Connection success/failure with error details
@@ -166,16 +177,19 @@ This is particularly useful for debugging connection issues between the MCP serv
## Development
### Building
+
```bash
pnpm run build
```
### Running Standalone
+
```bash
pnpm run start
```
### Testing
+
Use the MCP client (like Claude Code) to test commands, or use the TypeAgent CLI to verify the dispatcher is working.
## Trademarks
diff --git a/ts/packages/commandExecutor/examples/example_usage.md b/ts/packages/commandExecutor/examples/example_usage.md
index 395028622..a94f4b2bc 100644
--- a/ts/packages/commandExecutor/examples/example_usage.md
+++ b/ts/packages/commandExecutor/examples/example_usage.md
@@ -5,6 +5,7 @@
To use this MCP server with Claude Desktop, add the following to your Claude Desktop configuration:
### Windows
+
Edit `%APPDATA%\Claude\claude_desktop_config.json`:
```json
@@ -21,6 +22,7 @@ Edit `%APPDATA%\Claude\claude_desktop_config.json`:
```
### macOS
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
@@ -41,18 +43,21 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
Once configured, you can ask Claude to execute various commands:
### Music Commands
+
- "Play shake it off by taylor swift"
- "Skip to the next song"
- "Pause the music"
- "Set volume to 50%"
### List Management
+
- "Add ham to my grocery list"
- "Add milk, eggs, and bread to shopping list"
- "Remove bananas from grocery list"
- "Show me my grocery list"
### Calendar Operations
+
- "Add meeting tomorrow at 3pm"
- "Schedule dentist appointment for next Tuesday at 10am"
- "What's on my calendar today?"
@@ -69,6 +74,7 @@ The server will log all incoming requests to the console. You can verify it's wo
## Current Behavior
The MCP server currently:
+
- Accepts command requests via the `execute_command` tool
- Logs the request to the console
- Returns a success message to Claude
diff --git a/ts/packages/commandExecutor/src/commandServer.ts b/ts/packages/commandExecutor/src/commandServer.ts
index 93dfc123b..4e0dca2c4 100644
--- a/ts/packages/commandExecutor/src/commandServer.ts
+++ b/ts/packages/commandExecutor/src/commandServer.ts
@@ -25,9 +25,7 @@ function executeCommandRequestSchema() {
}
const ExecuteCommandRequestSchema = z.object(executeCommandRequestSchema());
-export type ExecuteCommandRequest = z.infer<
- typeof ExecuteCommandRequestSchema
->;
+export type ExecuteCommandRequest = z.infer;
function toolResult(result: string): CallToolResult {
return {
@@ -64,7 +62,9 @@ class Logger {
}
error(message: string, error?: any): void {
- const errorDetails = error ? ` - ${error instanceof Error ? error.message : String(error)}` : "";
+ const errorDetails = error
+ ? ` - ${error instanceof Error ? error.message : String(error)}`
+ : "";
const formatted = this.formatMessage("ERROR", message + errorDetails);
console.error(formatted);
this.logStream.write(formatted + "\n");
@@ -105,7 +105,7 @@ async function processHtmlImages(content: string): Promise {
const fullTag = match[0];
// Just remove the img tag entirely - don't download or display artwork
- processed = processed.replace(fullTag, '');
+ processed = processed.replace(fullTag, "");
}
return processed;
@@ -115,7 +115,10 @@ async function processHtmlImages(content: string): Promise {
* Minimal ClientIO implementation for MCP server
* Most methods are no-ops since we just need to satisfy the interface
*/
-function createMcpClientIO(logger: Logger, responseCollector: { messages: string[] }): ClientIO {
+function createMcpClientIO(
+ logger: Logger,
+ responseCollector: { messages: string[] },
+): ClientIO {
return {
clear(): void {
logger.log("ClientIO: clear() called");
@@ -126,39 +129,61 @@ function createMcpClientIO(logger: Logger, responseCollector: { messages: string
setDisplayInfo(): void {},
setDisplay(message: IAgentMessage): void {
logger.log(`ClientIO: setDisplay() - ${JSON.stringify(message)}`);
- if (typeof message === 'object' && 'message' in message) {
+ if (typeof message === "object" && "message" in message) {
const msg = message.message;
// Filter out "info" kind messages (technical translation details)
- if (typeof msg === 'object' && msg && 'kind' in msg && msg.kind === 'info') {
+ if (
+ typeof msg === "object" &&
+ msg &&
+ "kind" in msg &&
+ msg.kind === "info"
+ ) {
return;
}
- if (typeof msg === 'string') {
+ if (typeof msg === "string") {
responseCollector.messages.push(stripAnsi(msg));
- } else if (typeof msg === 'object' && msg && 'content' in msg) {
- responseCollector.messages.push(stripAnsi(String(msg.content)));
+ } else if (typeof msg === "object" && msg && "content" in msg) {
+ responseCollector.messages.push(
+ stripAnsi(String(msg.content)),
+ );
}
}
},
appendDisplay(message: IAgentMessage, mode: DisplayAppendMode): void {
- logger.log(`ClientIO: appendDisplay(mode=${mode}) - ${JSON.stringify(message)}`);
+ logger.log(
+ `ClientIO: appendDisplay(mode=${mode}) - ${JSON.stringify(message)}`,
+ );
// Only capture block mode messages (final results), not temporary status messages
- if (mode === 'block' && typeof message === 'object' && 'message' in message) {
+ if (
+ mode === "block" &&
+ typeof message === "object" &&
+ "message" in message
+ ) {
const msg = message.message;
// Filter out "info" kind messages (technical translation details)
- if (typeof msg === 'object' && msg && 'kind' in msg && msg.kind === 'info') {
+ if (
+ typeof msg === "object" &&
+ msg &&
+ "kind" in msg &&
+ msg.kind === "info"
+ ) {
return;
}
- if (typeof msg === 'string') {
+ if (typeof msg === "string") {
responseCollector.messages.push(stripAnsi(msg));
- } else if (typeof msg === 'object' && msg && 'content' in msg) {
- responseCollector.messages.push(stripAnsi(String(msg.content)));
+ } else if (typeof msg === "object" && msg && "content" in msg) {
+ responseCollector.messages.push(
+ stripAnsi(String(msg.content)),
+ );
}
}
},
appendDiagnosticData(requestId: RequestId, data: any): void {
- logger.log(`ClientIO: appendDiagnosticData(requestId=${requestId}) - ${JSON.stringify(data)}`);
+ logger.log(
+ `ClientIO: appendDiagnosticData(requestId=${requestId}) - ${JSON.stringify(data)}`,
+ );
},
setDynamicDisplay(): void {},
async askYesNo(
@@ -166,7 +191,9 @@ function createMcpClientIO(logger: Logger, responseCollector: { messages: string
requestId: RequestId,
defaultValue?: boolean,
): Promise {
- logger.log(`ClientIO: askYesNo(requestId=${requestId}) - "${message}" (defaulting to ${defaultValue ?? false})`);
+ logger.log(
+ `ClientIO: askYesNo(requestId=${requestId}) - "${message}" (defaulting to ${defaultValue ?? false})`,
+ );
return defaultValue ?? false;
},
async proposeAction(
@@ -174,7 +201,9 @@ function createMcpClientIO(logger: Logger, responseCollector: { messages: string
requestId: RequestId,
source: string,
): Promise {
- logger.log(`ClientIO: proposeAction(requestId=${requestId}, source=${source}) - ${JSON.stringify(actionTemplates)}`);
+ logger.log(
+ `ClientIO: proposeAction(requestId=${requestId}, source=${source}) - ${JSON.stringify(actionTemplates)}`,
+ );
return undefined;
},
async popupQuestion(
@@ -183,11 +212,20 @@ function createMcpClientIO(logger: Logger, responseCollector: { messages: string
defaultId: number | undefined,
source: string,
): Promise {
- logger.log(`ClientIO: popupQuestion(source=${source}) - "${message}" choices=[${choices.join(", ")}] (defaulting to ${defaultId ?? 0})`);
+ logger.log(
+ `ClientIO: popupQuestion(source=${source}) - "${message}" choices=[${choices.join(", ")}] (defaulting to ${defaultId ?? 0})`,
+ );
return defaultId ?? 0;
},
- notify(event: string, requestId: RequestId, data: any, source: string): void {
- logger.log(`ClientIO: notify(event=${event}, requestId=${requestId}, source=${source}) - ${JSON.stringify(data)}`);
+ notify(
+ event: string,
+ requestId: RequestId,
+ data: any,
+ source: string,
+ ): void {
+ logger.log(
+ `ClientIO: notify(event=${event}, requestId=${requestId}, source=${source}) - ${JSON.stringify(data)}`,
+ );
},
openLocalView(port: number): void {
logger.log(`ClientIO: openLocalView(port=${port})`);
@@ -196,7 +234,9 @@ function createMcpClientIO(logger: Logger, responseCollector: { messages: string
logger.log(`ClientIO: closeLocalView(port=${port})`);
},
takeAction(action: string, data: unknown): void {
- logger.log(`ClientIO: takeAction(action=${action}) - ${JSON.stringify(data)}`);
+ logger.log(
+ `ClientIO: takeAction(action=${action}) - ${JSON.stringify(data)}`,
+ );
},
};
}
@@ -222,8 +262,13 @@ export class CommandServer {
name: "Command-Executor-Server",
version: "1.0.0",
});
- this.agentServerUrl = agentServerUrl ?? process.env.AGENT_SERVER_URL ?? "ws://localhost:8999";
- this.logger.log(`CommandServer initializing with TypeAgent server URL: ${this.agentServerUrl}`);
+ this.agentServerUrl =
+ agentServerUrl ??
+ process.env.AGENT_SERVER_URL ??
+ "ws://localhost:8999";
+ this.logger.log(
+ `CommandServer initializing with TypeAgent server URL: ${this.agentServerUrl}`,
+ );
this.addTools();
if (debugMode) {
this.addDiagnosticTools();
@@ -248,12 +293,25 @@ export class CommandServer {
this.isConnecting = true;
try {
- const clientIO = createMcpClientIO(this.logger, this.responseCollector);
- this.dispatcher = await connectDispatcher(clientIO, this.agentServerUrl);
- this.logger.log(`Connected to TypeAgent dispatcher at ${this.agentServerUrl}`);
+ const clientIO = createMcpClientIO(
+ this.logger,
+ this.responseCollector,
+ );
+ this.dispatcher = await connectDispatcher(
+ clientIO,
+ this.agentServerUrl,
+ );
+ this.logger.log(
+ `Connected to TypeAgent dispatcher at ${this.agentServerUrl}`,
+ );
} catch (error) {
- this.logger.error(`Failed to connect to dispatcher at ${this.agentServerUrl}`, error);
- this.logger.error("Will retry connection automatically. Make sure the TypeAgent server is running.");
+ this.logger.error(
+ `Failed to connect to dispatcher at ${this.agentServerUrl}`,
+ error,
+ );
+ this.logger.error(
+ "Will retry connection automatically. Make sure the TypeAgent server is running.",
+ );
this.dispatcher = null;
} finally {
this.isConnecting = false;
@@ -264,7 +322,9 @@ export class CommandServer {
// Check connection status periodically and reconnect if needed
this.reconnectInterval = setInterval(async () => {
if (!this.dispatcher && !this.isConnecting) {
- this.logger.log("Attempting to reconnect to TypeAgent dispatcher...");
+ this.logger.log(
+ "Attempting to reconnect to TypeAgent dispatcher...",
+ );
await this.connectToDispatcher();
}
}, this.reconnectDelayMs);
@@ -306,7 +366,9 @@ export class CommandServer {
// If not connected, try to connect now (lazy connection)
if (!this.dispatcher && !this.isConnecting) {
- this.logger.log("Not connected to dispatcher, attempting to connect...");
+ this.logger.log(
+ "Not connected to dispatcher, attempting to connect...",
+ );
await this.connectToDispatcher();
}
@@ -321,19 +383,29 @@ export class CommandServer {
this.responseCollector.messages = [];
// Process the command through the TypeAgent dispatcher
- this.logger.log(`Sending command to dispatcher: ${request.request}`);
- const result = await this.dispatcher.processCommand(request.request);
+ this.logger.log(
+ `Sending command to dispatcher: ${request.request}`,
+ );
+ const result = await this.dispatcher.processCommand(
+ request.request,
+ );
if (result?.lastError) {
- this.logger.error(`Command execution error: ${result.lastError}`);
- return toolResult(`Error executing command: ${result.lastError}`);
+ this.logger.error(
+ `Command execution error: ${result.lastError}`,
+ );
+ return toolResult(
+ `Error executing command: ${result.lastError}`,
+ );
}
// Return the collected messages from the dispatcher
- this.logger.log(`Successfully executed command: ${request.request}`);
+ this.logger.log(
+ `Successfully executed command: ${request.request}`,
+ );
if (this.responseCollector.messages.length > 0) {
- const response = this.responseCollector.messages.join('\n\n');
+ const response = this.responseCollector.messages.join("\n\n");
// Process any HTML images in the response
const processedResponse = await processHtmlImages(response);
return toolResult(processedResponse);
From ea66752a7ec5d899a81309bd9cfcfadf757a507d Mon Sep 17 00:00:00 2001
From: steveluc
Date: Fri, 16 Jan 2026 23:33:40 -0800
Subject: [PATCH 05/21] Fix repo policy issues - trademark format and
package.json script order
- Use exact trademark text format with proper line breaks
- Sort package.json scripts alphabetically
Co-Authored-By: Claude Sonnet 4.5
---
ts/packages/commandExecutor/README.md | 6 +++++-
ts/packages/commandExecutor/package.json | 4 ++--
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/ts/packages/commandExecutor/README.md b/ts/packages/commandExecutor/README.md
index be504ede3..39adee334 100644
--- a/ts/packages/commandExecutor/README.md
+++ b/ts/packages/commandExecutor/README.md
@@ -194,4 +194,8 @@ Use the MCP client (like Claude Code) to test commands, or use the TypeAgent CLI
## Trademarks
-This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies.
+This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
+trademarks or logos is subject to and must follow
+[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
+Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
+Any use of third-party trademarks or logos are subject to those third-party's policies.
diff --git a/ts/packages/commandExecutor/package.json b/ts/packages/commandExecutor/package.json
index 25eb2f9e7..9778f8854 100644
--- a/ts/packages/commandExecutor/package.json
+++ b/ts/packages/commandExecutor/package.json
@@ -21,8 +21,8 @@
"clean": "rimraf --glob dist *.tsbuildinfo *.done.build.log",
"prettier": "prettier --check . --ignore-path ../../.prettierignore",
"prettier:fix": "prettier --write . --ignore-path ../../.prettierignore",
- "tsc": "tsc -b",
- "start": "node dist/server.js"
+ "start": "node dist/server.js",
+ "tsc": "tsc -b"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
From e4672de7382895676781c1cbaa90331819908a5d Mon Sep 17 00:00:00 2001
From: steveluc
Date: Sun, 18 Jan 2026 21:24:52 -0800
Subject: [PATCH 06/21] Enhance VSCode split editor functionality
This commit enhances the split editor command in the Coda extension to support more flexible editor selection and improves TypeAgent schema disambiguation.
**Bug Fixes:**
- Fix off-by-one error when selecting "first" or "last" editor by sorting by viewColumn
- Fix multiple split issue by adding conditionals to focus changes
- Remove unnecessary focus restoration logic for voice command scenarios
**Features:**
- Add support for splitting editors by position: "first", "last", "active", or numeric index
- Add support for splitting editors by file name: "split app.tsx to the right"
- Search all open tabs using tabGroups API, not just visible editors
- Automatically open and focus files found in background tabs before splitting
**Schema Improvements:**
- Add clear disambiguation between splitEditor and moveCursorInFile actions
- Add "USE THIS for" and "DO NOT USE for" guidance in schema comments
- Add concrete examples to help LLM choose correct action
- Remove moveCursorInFile reference from main code schema (not useful for voice)
**Documentation:**
- Add VSCODE_CAPABILITIES.md documenting all VSCode automation features
- Update split editor examples to show new position and file-based splitting
Co-Authored-By: Claude Sonnet 4.5
---
.../agents/code/src/codeActionsSchema.ts | 20 +-
.../src/vscode/editorCodeActionsSchema.ts | 13 +-
ts/packages/coda/src/handleVSCodeActions.ts | 166 ++++++++-
ts/packages/commandExecutor/README.md | 24 +-
.../commandExecutor/VSCODE_CAPABILITIES.md | 318 ++++++++++++++++++
.../commandExecutor/src/commandServer.ts | 6 +-
6 files changed, 531 insertions(+), 16 deletions(-)
create mode 100644 ts/packages/commandExecutor/VSCODE_CAPABILITIES.md
diff --git a/ts/packages/agents/code/src/codeActionsSchema.ts b/ts/packages/agents/code/src/codeActionsSchema.ts
index 176655ff2..b38f25ce4 100644
--- a/ts/packages/agents/code/src/codeActionsSchema.ts
+++ b/ts/packages/agents/code/src/codeActionsSchema.ts
@@ -41,13 +41,29 @@ export type ChangeColorThemeAction = {
};
export type SplitDirection = "right" | "left" | "up" | "down";
+export type EditorPosition = "first" | "last" | "active";
-// Split to update the current editor window into a new editor pane to the left, right, above, or below
+// ACTION: Split an editor window into multiple panes showing the same file or different files side-by-side.
+// This creates a new editor pane (split view) for working with multiple files simultaneously.
+// USE THIS for: "split editor", "split the editor with X", "duplicate this editor to the right", "split X"
+//
+// Examples:
+// - "split editor to the right" → splits active editor
+// - "split the first editor" → splits leftmost editor
+// - "split app.tsx to the left" → finds editor showing app.tsx and splits it
+// - "split the last editor down" → splits rightmost editor downward
+// - "split the editor with utils.ts" → finds editor showing utils.ts and splits it
export type SplitEditorAction = {
actionName: "splitEditor";
parameters: {
- // e.g., "right", "left", "up", "down", only if specified by the user
+ // Direction to split: "right", "left", "up", "down". Only include if user specifies direction.
direction?: SplitDirection;
+ // Which editor to split by position. Use "first" for leftmost editor, "last" for rightmost, "active" for current editor, or a number (0-based index).
+ editorPosition?: EditorPosition | number;
+ // Which editor to split by file name. Extract the file name or pattern from user request.
+ // Examples: "app.tsx", "main.py", "utils", "codeActionHandler"
+ // Use this when user says "split X" or "split the editor with X" where X is a file name.
+ fileName?: string;
};
};
diff --git a/ts/packages/agents/code/src/vscode/editorCodeActionsSchema.ts b/ts/packages/agents/code/src/vscode/editorCodeActionsSchema.ts
index 28e1c55fa..38126e23a 100644
--- a/ts/packages/agents/code/src/vscode/editorCodeActionsSchema.ts
+++ b/ts/packages/agents/code/src/vscode/editorCodeActionsSchema.ts
@@ -160,11 +160,20 @@ export type EditorActionFixProblem = {
};
};
-// Action to move the cursor in a file to a specified position.
+// ACTION: Move the cursor to a specific position within a file (for navigation or editing preparation).
+// This moves the cursor position, NOT split/duplicate the editor view.
+// USE THIS for: "go to file X", "jump to line 50", "go to function foo", "move cursor to X"
+// DO NOT USE for: "split editor", "split X", "duplicate editor" (use splitEditor action instead)
+//
+// Examples:
+// - "go to line 50" → { target: { type: "onLine", line: 50 } }
+// - "jump to function main" → { target: { type: "insideFunction", name: "main" } }
+// - "go to app.tsx" → { target: { type: "inFile", filePath: "app.tsx" } }
+// - "move cursor to the end of file" → { target: { type: "atEndOfFile" } }
export type EditorActionMoveCursor = {
actionName: "moveCursorInFile";
parameters: {
- //Target position for the cursor. Supports symbolic locations, line-based positions, or file-relative positions.
+ // Target position for the cursor. Supports symbolic locations, line-based positions, or file-relative positions.
target: CursorTarget;
// Optional file where the cursor should be moved. Defaults to the active editor if not provided.
file?: FileTarget;
diff --git a/ts/packages/coda/src/handleVSCodeActions.ts b/ts/packages/coda/src/handleVSCodeActions.ts
index 564063351..f8a6217f4 100644
--- a/ts/packages/coda/src/handleVSCodeActions.ts
+++ b/ts/packages/coda/src/handleVSCodeActions.ts
@@ -298,38 +298,186 @@ export async function handleBaseEditorActions(
}
case "splitEditor": {
- if (actionData && actionData.direction) {
- switch (actionData.direction) {
+ console.log(
+ `[splitEditor] Starting with actionData:`,
+ JSON.stringify(actionData),
+ );
+ // Find the target editor to split
+ let targetEditor: vscode.TextEditor | undefined;
+ const editorPosition = actionData?.editorPosition;
+ const fileName = actionData?.fileName;
+ console.log(
+ `[splitEditor] editorPosition=${editorPosition}, fileName=${fileName}`,
+ );
+
+ if (fileName || editorPosition !== undefined) {
+ // Find target editor by fileName or editorPosition
+ // Use visibleTextEditors to get all currently visible editors
+ const allEditors = vscode.window.visibleTextEditors;
+ console.log(
+ `[splitEditor] Found ${allEditors.length} visible editors:`,
+ allEditors.map((e) => e.document.fileName),
+ );
+
+ if (fileName) {
+ // Search by file name (case-insensitive, partial match)
+ const pattern = fileName.toLowerCase();
+ console.log(
+ `[splitEditor] Searching for pattern: ${pattern}`,
+ );
+
+ // First try visible editors
+ targetEditor = allEditors.find((editor) =>
+ editor.document.fileName
+ .toLowerCase()
+ .includes(pattern),
+ );
+
+ // If not found in visible editors, search all open tabs
+ if (!targetEditor) {
+ console.log(
+ `[splitEditor] Not found in visible editors, searching all tabs...`,
+ );
+ for (const tabGroup of vscode.window.tabGroups.all) {
+ for (const tab of tabGroup.tabs) {
+ const input = tab.input as any;
+ if (input?.uri) {
+ const filePath =
+ input.uri.fsPath || input.uri.path;
+ if (
+ filePath.toLowerCase().includes(pattern)
+ ) {
+ console.log(
+ `[splitEditor] Found tab with matching file: ${filePath}`,
+ );
+ // Open the document to make it an editor
+ const document =
+ await vscode.workspace.openTextDocument(
+ input.uri,
+ );
+ targetEditor =
+ await vscode.window.showTextDocument(
+ document,
+ {
+ viewColumn:
+ tabGroup.viewColumn,
+ preserveFocus: false,
+ },
+ );
+ break;
+ }
+ }
+ }
+ if (targetEditor) break;
+ }
+ }
+
+ if (!targetEditor) {
+ console.log(
+ `[splitEditor] No editor or tab found with pattern: ${pattern}`,
+ );
+ actionResult.handled = false;
+ actionResult.message = `No editor found with file: ${fileName}`;
+ break;
+ }
+ console.log(
+ `[splitEditor] Found target editor: ${targetEditor.document.fileName}`,
+ );
+ } else if (editorPosition !== undefined) {
+ // Search by position
+ if (typeof editorPosition === "number") {
+ targetEditor = allEditors[editorPosition];
+ if (!targetEditor) {
+ actionResult.handled = false;
+ actionResult.message = `No editor at position: ${editorPosition}`;
+ break;
+ }
+ } else if (editorPosition === "first") {
+ // Sort by viewColumn to get leftmost editor
+ const sortedEditors = [...allEditors].sort(
+ (a, b) => (a.viewColumn || 0) - (b.viewColumn || 0),
+ );
+ targetEditor = sortedEditors[0];
+ } else if (editorPosition === "last") {
+ // Sort by viewColumn to get rightmost editor
+ const sortedEditors = [...allEditors].sort(
+ (a, b) => (a.viewColumn || 0) - (b.viewColumn || 0),
+ );
+ targetEditor = sortedEditors[sortedEditors.length - 1];
+ } else if (editorPosition === "active") {
+ targetEditor = vscode.window.activeTextEditor;
+ }
+
+ if (!targetEditor) {
+ actionResult.handled = false;
+ actionResult.message = `No editor found at position: ${editorPosition}`;
+ break;
+ }
+ }
+
+ // Focus the target editor temporarily (only if it's not already active)
+ if (targetEditor !== vscode.window.activeTextEditor) {
+ console.log(
+ `[splitEditor] Focusing target editor: ${targetEditor!.document.fileName}`,
+ );
+ await vscode.window.showTextDocument(
+ targetEditor!.document,
+ {
+ viewColumn:
+ targetEditor!.viewColumn ??
+ vscode.ViewColumn.One,
+ preserveFocus: false,
+ },
+ );
+ }
+ }
+
+ // Execute the split command
+ const direction = actionData?.direction;
+ if (direction) {
+ switch (direction) {
case "right": {
- vscode.commands.executeCommand(
+ await vscode.commands.executeCommand(
"workbench.action.splitEditorRight",
);
break;
}
case "left": {
- vscode.commands.executeCommand(
+ await vscode.commands.executeCommand(
"workbench.action.splitEditorLeft",
);
break;
}
case "up": {
- vscode.commands.executeCommand(
+ await vscode.commands.executeCommand(
"workbench.action.splitEditorUp",
);
break;
}
case "down": {
- vscode.commands.executeCommand(
+ await vscode.commands.executeCommand(
"workbench.action.splitEditorDown",
);
break;
}
}
- actionResult.message = `Split editor ${actionData.direction}`;
} else {
- vscode.commands.executeCommand("workbench.action.splitEditor");
- actionResult.message = "Split editor";
+ await vscode.commands.executeCommand(
+ "workbench.action.splitEditor",
+ );
}
+
+ // Build result message
+ const targetInfo = fileName
+ ? ` (${fileName})`
+ : editorPosition !== undefined
+ ? ` (${editorPosition})`
+ : "";
+ actionResult.message =
+ `Split editor${targetInfo} ${direction || ""}`.trim();
+ console.log(
+ `[splitEditor] Completed successfully: ${actionResult.message}`,
+ );
break;
}
diff --git a/ts/packages/commandExecutor/README.md b/ts/packages/commandExecutor/README.md
index 39adee334..f02106f28 100644
--- a/ts/packages/commandExecutor/README.md
+++ b/ts/packages/commandExecutor/README.md
@@ -86,7 +86,7 @@ The server is configured in `.mcp.json`:
#### execute_command
-Execute user commands such as playing music, managing lists, or working with calendars.
+Execute user commands including music playback, list management, calendar operations, and VSCode automation.
**Parameters:**
@@ -94,10 +94,28 @@ Execute user commands such as playing music, managing lists, or working with cal
**Examples:**
+**Music & Media:**
- "play sweet emotion by aerosmith"
+- "play bohemian rhapsody by queen"
+
+**Lists & Tasks:**
- "add jelly beans to my grocery list"
+- "what's on my shopping list"
+
+**Calendar:**
- "schedule a meeting for tomorrow at 2pm"
+**VSCode Automation:**
+- "switch to monokai theme"
+- "change theme to dark+"
+- "open the explorer view"
+- "create a new folder called components"
+- "open file app.ts"
+- "split editor to the right"
+- "toggle zen mode"
+- "open integrated terminal"
+- "show output panel"
+
#### ping (debug mode)
Test server connectivity.
@@ -115,7 +133,9 @@ Command Executor MCP Server
↓
TypeAgent Dispatcher (WebSocket)
↓
-TypeAgent Agents (Music, Lists, Calendar, etc.)
+ ├─ TypeAgent Agents (Music, Lists, Calendar, etc.)
+ └─ Coda VSCode Extension (via WebSocket on port 8082)
+ └─ VSCode APIs (theme, editor, files, terminal, etc.)
```
The MCP server:
diff --git a/ts/packages/commandExecutor/VSCODE_CAPABILITIES.md b/ts/packages/commandExecutor/VSCODE_CAPABILITIES.md
new file mode 100644
index 000000000..c99ca05f6
--- /dev/null
+++ b/ts/packages/commandExecutor/VSCODE_CAPABILITIES.md
@@ -0,0 +1,318 @@
+
+
+# VSCode Capabilities Available Through Command Executor
+
+The Command Executor MCP server can control VSCode through the Coda extension. Below are the available capabilities organized by category.
+
+## How It Works
+
+```
+User → Claude Code → execute_command MCP tool →
+ → TypeAgent Dispatcher →
+ → Coda Extension (WebSocket on port 8082) →
+ → VSCode APIs
+```
+
+The Coda VSCode extension connects to TypeAgent's dispatcher and can execute various VSCode commands. Simply use natural language with the `execute_command` tool.
+
+## Available Commands
+
+### Theme & Appearance
+
+**Change Color Theme:**
+- "switch to monokai theme"
+- "change theme to dark+"
+- "change to light theme"
+- "set theme to solarized dark"
+
+**Display Controls:**
+- "toggle full screen"
+- "toggle zen mode"
+- "zoom in" (zooms in 5 levels)
+- "zoom out" (zooms out 5 levels)
+- "reset zoom"
+
+### Editor Layout
+
+**Split Editor:**
+- "split editor to the right" (splits currently focused editor)
+- "split editor to the left"
+- "split editor up"
+- "split editor down"
+- "split the first editor to the right" (splits leftmost editor)
+- "split the last editor" (splits rightmost editor)
+- "split app.tsx to the right" (splits editor with app.tsx file)
+- "split the typescript file" (splits editor with a .ts file)
+
+**Column Layout:**
+- "change editor to single column"
+- "change editor to double columns"
+- "change editor to three columns"
+- "toggle editor layout"
+
+**Editor Management:**
+- "close editor"
+
+### File & Folder Operations
+
+**Open Files:**
+- "open file app.ts"
+- "open main.py"
+- "goto file index.html"
+
+**Navigate:**
+- "goto line 42"
+- "goto file package.json"
+
+**Create Files:**
+- "create new file" (untitled)
+- "create file hello.ts in src folder"
+
+**Create Folders:**
+- "create folder called components"
+- "create folder utils in src"
+
+**Open Folders:**
+- "open folder src in explorer"
+- "reveal packages folder"
+
+### Views & Panels
+
+**Show Views:**
+- "show explorer"
+- "open explorer view"
+- "show search"
+- "show source control"
+- "show output panel"
+
+**Special Views:**
+- "toggle search details"
+- "replace in files"
+- "open markdown preview"
+- "open markdown preview to side"
+
+### Navigation & Commands
+
+**Command Palette:**
+- "show command palette"
+
+**Quick Open:**
+- "quick open file"
+
+**Settings:**
+- "open settings"
+- "show user settings"
+- "show keyboard shortcuts"
+
+### Terminal
+
+**Open Terminal:**
+- "open integrated terminal"
+- "open terminal in src folder"
+- "open terminal and run npm install"
+
+### Tasks & Build
+
+**Run Tasks:**
+- "build the project"
+- "clean the project"
+- "rebuild the project"
+- "run build task in packages folder"
+
+### Window Management
+
+**New Windows:**
+- "open new window"
+
+## Usage Examples
+
+### Example 1: Change Theme
+
+**User to Claude Code:**
+```
+switch to monokai theme
+```
+
+**Claude Code calls:**
+```json
+{
+ "tool": "execute_command",
+ "arguments": {
+ "request": "switch to monokai theme"
+ }
+}
+```
+
+**Result:**
+```
+Changed theme to Monokai
+```
+
+### Example 2: Open File and Split Editor
+
+**User to Claude Code:**
+```
+open app.ts and split the editor to the right
+```
+
+**Claude Code can:**
+1. Call execute_command with "open app.ts"
+2. Call execute_command with "split editor to the right"
+
+Or TypeAgent might handle both in one call.
+
+### Example 3: Create Project Structure
+
+**User to Claude Code:**
+```
+create folders called src, tests, and docs
+```
+
+**Claude Code calls:**
+```json
+{
+ "tool": "execute_command",
+ "arguments": {
+ "request": "create folders called src, tests, and docs"
+ }
+}
+```
+
+## Implementation Details
+
+### How Coda Extension Handles Commands
+
+The Coda extension listens for WebSocket messages from TypeAgent's Code Agent:
+
+```typescript
+// Message format from TypeAgent
+{
+ id: "123",
+ method: "code/changeColorScheme",
+ params: {
+ theme: "Monokai"
+ }
+}
+
+// Response from Coda
+{
+ id: "123",
+ result: "Changed theme to Monokai"
+}
+```
+
+### Action Handlers
+
+Commands are routed through several handlers:
+
+1. **handleBaseEditorActions**: Theme, split, layout, new file
+2. **handleGeneralKBActions**: Command palette, goto, settings
+3. **handleDisplayKBActions**: Views, panels, zoom, full screen
+4. **handleWorkbenchActions**: Files, folders, tasks, terminal
+5. **handleDebugActions**: Debugging operations
+6. **handleExtensionActions**: Extension management
+7. **handleEditorCodeActions**: Code editing, refactoring
+
+See source files in `packages/coda/src/handle*.ts` for full details.
+
+### Available Action Names
+
+Here are the internal action names (useful for understanding the code):
+
+**Base Editor:**
+- `changeColorScheme`
+- `splitEditor`
+- `changeEditorLayout`
+- `newFile`
+
+**Display:**
+- `toggleFullScreen`
+- `toggleEditorLayout`
+- `zoomIn`, `zoomOut`, `fontZoomReset`
+- `showExplorer`, `showSearch`, `showSourceControl`
+- `showOutputPanel`
+- `toggleSearchDetails`
+- `replaceInFiles`
+- `openMarkdownPreview`, `openMarkdownPreviewToSide`
+- `zenMode`
+- `closeEditor`
+- `openSettings`
+
+**General:**
+- `showCommandPalette`
+- `gotoFileOrLineOrSymbol`
+- `newWindowFromApp`
+- `showUserSettings`
+- `showKeyboardShortcuts`
+
+**Workbench:**
+- `workbenchOpenFile`
+- `workbenchOpenFolder`
+- `workbenchCreateFolderFromExplorer`
+- `workbenchBuildRelatedTask`
+- `openInIntegratedTerminal`
+
+## Prerequisites
+
+To use these VSCode capabilities:
+
+1. **TypeAgent Dispatcher** must be running:
+ ```bash
+ pnpm run start:agent-server
+ ```
+
+2. **Coda Extension** must be installed and activated in VSCode:
+ - Published as `aisystems.copilot-coda`
+ - Auto-connects to dispatcher on port 8082
+
+3. **Command Executor MCP Server** configured in `.mcp.json`:
+ ```json
+ {
+ "mcpServers": {
+ "command-executor": {
+ "command": "node",
+ "args": ["packages/commandExecutor/dist/server.js"]
+ }
+ }
+ }
+ ```
+
+## Limitations
+
+1. **Natural Language Translation**: Commands must be clear enough for TypeAgent to translate to the correct action
+2. **File/Folder Matching**: File and folder names are matched via search, so ambiguous names might require user selection
+3. **Terminal Commands**: High-risk terminal commands are blocked for security
+4. **Theme Names**: Theme names must match installed themes exactly
+
+## Testing Commands
+
+You can test these commands directly in Claude Code:
+
+```
+// Test theme changing
+switch to monokai theme
+
+// Test editor layout
+split editor to the right
+
+// Test file operations
+open package.json
+
+// Test views
+show explorer
+
+// Test terminal
+open integrated terminal
+```
+
+## Extending Capabilities
+
+To add new VSCode capabilities:
+
+1. Add handler in `packages/coda/src/handle*.ts`
+2. Register in `handleVSCodeActions` function
+3. Update this documentation
+4. No changes needed to MCP server (it forwards all commands to dispatcher)
+
+The beauty of this architecture is that new capabilities added to Coda are automatically available through the MCP server without any code changes to the command executor.
diff --git a/ts/packages/commandExecutor/src/commandServer.ts b/ts/packages/commandExecutor/src/commandServer.ts
index 4e0dca2c4..be2966bdf 100644
--- a/ts/packages/commandExecutor/src/commandServer.ts
+++ b/ts/packages/commandExecutor/src/commandServer.ts
@@ -352,7 +352,11 @@ export class CommandServer {
{
inputSchema: executeCommandRequestSchema(),
description:
- "Execute a user command such as playing music, managing lists, or working with calendars",
+ "Execute user commands including:\n" +
+ "- Music & media: play songs, control playback\n" +
+ "- Lists & tasks: manage shopping lists, todo lists\n" +
+ "- Calendar: schedule events, view calendar\n" +
+ "- VSCode automation: change theme (e.g. 'switch to monokai theme'), open files, create folders, run tasks, manage editor layout, open terminals, toggle settings",
},
async (request: ExecuteCommandRequest) =>
this.executeCommand(request),
From 7cf1b8089b0806bb60e8c43624ee8be6093c21b3 Mon Sep 17 00:00:00 2001
From: steveluc
Date: Sun, 18 Jan 2026 21:36:02 -0800
Subject: [PATCH 07/21] Fix prettier formatting for README and
VSCODE_CAPABILITIES
---
ts/packages/commandExecutor/README.md | 5 ++-
.../commandExecutor/VSCODE_CAPABILITIES.md | 31 +++++++++++++++++++
2 files changed, 35 insertions(+), 1 deletion(-)
diff --git a/ts/packages/commandExecutor/README.md b/ts/packages/commandExecutor/README.md
index bde1c9839..a510b40f3 100644
--- a/ts/packages/commandExecutor/README.md
+++ b/ts/packages/commandExecutor/README.md
@@ -95,17 +95,21 @@ Execute user commands including music playback, list management, calendar operat
**Examples:**
**Music & Media:**
+
- "play sweet emotion by aerosmith"
- "play bohemian rhapsody by queen"
**Lists & Tasks:**
+
- "add jelly beans to my grocery list"
- "what's on my shopping list"
**Calendar:**
+
- "schedule a meeting for tomorrow at 2pm"
**VSCode Automation:**
+
- "switch to monokai theme"
- "change theme to dark+"
- "open the explorer view"
@@ -116,7 +120,6 @@ Execute user commands including music playback, list management, calendar operat
- "open integrated terminal"
- "show output panel"
-
#### ping (debug mode)
Test server connectivity.
diff --git a/ts/packages/commandExecutor/VSCODE_CAPABILITIES.md b/ts/packages/commandExecutor/VSCODE_CAPABILITIES.md
index c99ca05f6..ecb97378c 100644
--- a/ts/packages/commandExecutor/VSCODE_CAPABILITIES.md
+++ b/ts/packages/commandExecutor/VSCODE_CAPABILITIES.md
@@ -21,12 +21,14 @@ The Coda VSCode extension connects to TypeAgent's dispatcher and can execute var
### Theme & Appearance
**Change Color Theme:**
+
- "switch to monokai theme"
- "change theme to dark+"
- "change to light theme"
- "set theme to solarized dark"
**Display Controls:**
+
- "toggle full screen"
- "toggle zen mode"
- "zoom in" (zooms in 5 levels)
@@ -36,6 +38,7 @@ The Coda VSCode extension connects to TypeAgent's dispatcher and can execute var
### Editor Layout
**Split Editor:**
+
- "split editor to the right" (splits currently focused editor)
- "split editor to the left"
- "split editor up"
@@ -46,40 +49,48 @@ The Coda VSCode extension connects to TypeAgent's dispatcher and can execute var
- "split the typescript file" (splits editor with a .ts file)
**Column Layout:**
+
- "change editor to single column"
- "change editor to double columns"
- "change editor to three columns"
- "toggle editor layout"
**Editor Management:**
+
- "close editor"
### File & Folder Operations
**Open Files:**
+
- "open file app.ts"
- "open main.py"
- "goto file index.html"
**Navigate:**
+
- "goto line 42"
- "goto file package.json"
**Create Files:**
+
- "create new file" (untitled)
- "create file hello.ts in src folder"
**Create Folders:**
+
- "create folder called components"
- "create folder utils in src"
**Open Folders:**
+
- "open folder src in explorer"
- "reveal packages folder"
### Views & Panels
**Show Views:**
+
- "show explorer"
- "open explorer view"
- "show search"
@@ -87,6 +98,7 @@ The Coda VSCode extension connects to TypeAgent's dispatcher and can execute var
- "show output panel"
**Special Views:**
+
- "toggle search details"
- "replace in files"
- "open markdown preview"
@@ -95,12 +107,15 @@ The Coda VSCode extension connects to TypeAgent's dispatcher and can execute var
### Navigation & Commands
**Command Palette:**
+
- "show command palette"
**Quick Open:**
+
- "quick open file"
**Settings:**
+
- "open settings"
- "show user settings"
- "show keyboard shortcuts"
@@ -108,6 +123,7 @@ The Coda VSCode extension connects to TypeAgent's dispatcher and can execute var
### Terminal
**Open Terminal:**
+
- "open integrated terminal"
- "open terminal in src folder"
- "open terminal and run npm install"
@@ -115,6 +131,7 @@ The Coda VSCode extension connects to TypeAgent's dispatcher and can execute var
### Tasks & Build
**Run Tasks:**
+
- "build the project"
- "clean the project"
- "rebuild the project"
@@ -123,6 +140,7 @@ The Coda VSCode extension connects to TypeAgent's dispatcher and can execute var
### Window Management
**New Windows:**
+
- "open new window"
## Usage Examples
@@ -130,11 +148,13 @@ The Coda VSCode extension connects to TypeAgent's dispatcher and can execute var
### Example 1: Change Theme
**User to Claude Code:**
+
```
switch to monokai theme
```
**Claude Code calls:**
+
```json
{
"tool": "execute_command",
@@ -145,6 +165,7 @@ switch to monokai theme
```
**Result:**
+
```
Changed theme to Monokai
```
@@ -152,11 +173,13 @@ Changed theme to Monokai
### Example 2: Open File and Split Editor
**User to Claude Code:**
+
```
open app.ts and split the editor to the right
```
**Claude Code can:**
+
1. Call execute_command with "open app.ts"
2. Call execute_command with "split editor to the right"
@@ -165,11 +188,13 @@ Or TypeAgent might handle both in one call.
### Example 3: Create Project Structure
**User to Claude Code:**
+
```
create folders called src, tests, and docs
```
**Claude Code calls:**
+
```json
{
"tool": "execute_command",
@@ -221,12 +246,14 @@ See source files in `packages/coda/src/handle*.ts` for full details.
Here are the internal action names (useful for understanding the code):
**Base Editor:**
+
- `changeColorScheme`
- `splitEditor`
- `changeEditorLayout`
- `newFile`
**Display:**
+
- `toggleFullScreen`
- `toggleEditorLayout`
- `zoomIn`, `zoomOut`, `fontZoomReset`
@@ -240,6 +267,7 @@ Here are the internal action names (useful for understanding the code):
- `openSettings`
**General:**
+
- `showCommandPalette`
- `gotoFileOrLineOrSymbol`
- `newWindowFromApp`
@@ -247,6 +275,7 @@ Here are the internal action names (useful for understanding the code):
- `showKeyboardShortcuts`
**Workbench:**
+
- `workbenchOpenFile`
- `workbenchOpenFolder`
- `workbenchCreateFolderFromExplorer`
@@ -258,11 +287,13 @@ Here are the internal action names (useful for understanding the code):
To use these VSCode capabilities:
1. **TypeAgent Dispatcher** must be running:
+
```bash
pnpm run start:agent-server
```
2. **Coda Extension** must be installed and activated in VSCode:
+
- Published as `aisystems.copilot-coda`
- Auto-connects to dispatcher on port 8082
From 0ebe52c51079f2cb4cca32c45976516dfb7eafd4 Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 00:08:16 -0800
Subject: [PATCH 08/21] Add support for creating playlists with song lists in
player agent
This enhancement allows the player agent to create playlists with songs
specified by title and artist, and to add lists of songs to existing
playlists, eliminating the need to search and build a track list first.
Changes:
- Add SongSpecification interface for specifying songs by track name,
optional artist, and optional album
- Extend CreatePlaylistAction with optional songs parameter to support
creating playlists with songs in one action
- Add new AddSongsToPlaylistAction for bulk adding songs to existing
playlists
- Implement searchSongsAndGetUris helper function to search Spotify
for songs and collect their URIs
- Update createPlaylist handler to search for and add songs when
creating playlists
- Add addSongsToPlaylist handler for the new action
- Add validation support for the new action in playerHandlers
Benefits:
- Users can create playlists with songs in a single request
- Supports requests like "create a playlist with the top 10 songs"
- Provides feedback on songs that couldn't be found
- Maintains backward compatibility with existing createPlaylist usage
Co-Authored-By: Claude Sonnet 4.5
---
.../agents/player/src/agent/playerHandlers.ts | 1 +
.../agents/player/src/agent/playerSchema.ts | 24 +++-
ts/packages/agents/player/src/client.ts | 125 +++++++++++++++++-
3 files changed, 145 insertions(+), 5 deletions(-)
diff --git a/ts/packages/agents/player/src/agent/playerHandlers.ts b/ts/packages/agents/player/src/agent/playerHandlers.ts
index 91aa3ae00..700fbb231 100644
--- a/ts/packages/agents/player/src/agent/playerHandlers.ts
+++ b/ts/packages/agents/player/src/agent/playerHandlers.ts
@@ -378,6 +378,7 @@ async function getPlayerActionCompletion(
case "deletePlaylist":
case "addCurrentTrackToPlaylist":
case "addToPlaylistFromCurrentTrackList":
+ case "addSongsToPlaylist":
if (propertyName === "parameters.name") {
if (userData.data.playlists === undefined) {
await getPlaylistsFromUserData(
diff --git a/ts/packages/agents/player/src/agent/playerSchema.ts b/ts/packages/agents/player/src/agent/playerSchema.ts
index 7f8338432..ed1edaced 100644
--- a/ts/packages/agents/player/src/agent/playerSchema.ts
+++ b/ts/packages/agents/player/src/agent/playerSchema.ts
@@ -32,11 +32,19 @@ export type PlayerActions =
| DeletePlaylistAction
| AddCurrentTrackToPlaylistAction
| AddToPlaylistFromCurrentTrackListAction
+ | AddSongsToPlaylistAction
| GetQueueAction;
export type PlayerEntities = MusicDevice;
export type MusicDevice = string;
+// Specification for a song by title and optional artist/album
+export interface SongSpecification {
+ trackName: string;
+ artist?: string;
+ albumName?: string;
+}
+
// Use playRandom when the user asks for some music to play
export interface PlayRandomAction {
actionName: "playRandom";
@@ -235,12 +243,14 @@ export interface GetFavoritesAction {
};
}
-// create a new empty playlist
+// create a new playlist, optionally with a list of songs specified by title and artist
export interface CreatePlaylistAction {
actionName: "createPlaylist";
parameters: {
// name of playlist to create
name: string;
+ // optional list of songs to add to the playlist when creating it
+ songs?: SongSpecification[];
};
}
@@ -276,6 +286,18 @@ export interface AddToPlaylistFromCurrentTrackListAction {
};
}
+// add songs to a playlist by specifying track names and optional artists
+// this action searches for each song and adds it to the playlist
+export interface AddSongsToPlaylistAction {
+ actionName: "addSongsToPlaylist";
+ parameters: {
+ // name of playlist to add songs to
+ name: string;
+ // list of songs to add, each specified by track name and optional artist/album
+ songs: SongSpecification[];
+ };
+}
+
// set the current track list to the queue of upcoming tracks
export interface GetQueueAction {
actionName: "getQueue";
diff --git a/ts/packages/agents/player/src/client.ts b/ts/packages/agents/player/src/client.ts
index 49f0949fd..b836761c6 100644
--- a/ts/packages/agents/player/src/client.ts
+++ b/ts/packages/agents/player/src/client.ts
@@ -18,6 +18,8 @@ import {
GetFromCurrentPlaylistListAction,
AddCurrentTrackToPlaylistAction,
AddToPlaylistFromCurrentTrackListAction,
+ AddSongsToPlaylistAction,
+ SongSpecification,
} from "./agent/playerSchema.js";
import { createTokenProvider } from "./defaultTokenProvider.js";
import chalk from "chalk";
@@ -536,6 +538,49 @@ export async function searchForPlaylists(
}
}
+// Search for tracks from a list of song specifications and return their URIs
+async function searchSongsAndGetUris(
+ songs: SongSpecification[],
+ context: IClientContext,
+): Promise<{ uris: string[]; notFound: string[] }> {
+ const uris: string[] = [];
+ const notFound: string[] = [];
+
+ for (const song of songs) {
+ // Build search query from track name and optional artist/album
+ let queryString = song.trackName;
+ if (song.artist) {
+ queryString += ` artist:${song.artist}`;
+ }
+ if (song.albumName) {
+ queryString += ` album:${song.albumName}`;
+ }
+
+ const trackCollection = await searchTracks(queryString, context);
+ if (trackCollection) {
+ const tracks = trackCollection.getTracks();
+ if (tracks.length > 0) {
+ // Take the first (best) match
+ uris.push(tracks[0].uri);
+ } else {
+ notFound.push(
+ song.artist
+ ? `${song.trackName} by ${song.artist}`
+ : song.trackName,
+ );
+ }
+ } else {
+ notFound.push(
+ song.artist
+ ? `${song.trackName} by ${song.artist}`
+ : song.trackName,
+ );
+ }
+ }
+
+ return { uris, notFound };
+}
+
async function playTrackCollection(
trackCollection: ITrackCollection,
clientContext: IClientContext,
@@ -1300,17 +1345,40 @@ export async function handleCall(
*/
case "createPlaylist": {
const name = action.parameters.name;
- // create empty playlist
+ const songs = action.parameters.songs;
+
+ let resultMessage = `playlist ${name} created`;
+ let uris: string[] = [];
+
+ // If songs are specified, search for them first
+ if (songs && songs.length > 0) {
+ const searchResult = await searchSongsAndGetUris(
+ songs,
+ clientContext,
+ );
+ uris = searchResult.uris;
+
+ if (uris.length > 0) {
+ resultMessage += ` with ${uris.length} song${uris.length > 1 ? "s" : ""}`;
+ }
+
+ if (searchResult.notFound.length > 0) {
+ resultMessage += `\nCouldn't find: ${searchResult.notFound.join(", ")}`;
+ }
+ }
+
+ // Create the playlist with the URIs (or empty if no songs)
await createPlaylist(
clientContext.service,
name,
clientContext.service.retrieveUser().id!,
- [],
+ uris,
name,
);
- console.log(`playlist ${name} created`);
+
+ console.log(resultMessage);
return createActionResultFromTextDisplay(
- chalk.magentaBright(`playlist ${name} created`),
+ chalk.magentaBright(resultMessage),
);
}
case "deletePlaylist": {
@@ -1421,6 +1489,55 @@ export async function handleCall(
chalk.magentaBright(resultString),
);
}
+ case "addSongsToPlaylist": {
+ const addAction = action as AddSongsToPlaylistAction;
+ const playlistName = addAction.parameters.name;
+ const songs = addAction.parameters.songs;
+
+ if (clientContext.userData === undefined) {
+ return createErrorActionResult("No user data found");
+ }
+
+ // Find the playlist
+ const playlists = await getPlaylistsFromUserData(
+ clientContext.service,
+ clientContext.userData!.data,
+ );
+ const playlist = playlists?.find((pl) => {
+ return pl.name
+ .toLowerCase()
+ .includes(playlistName.toLowerCase());
+ });
+ if (!playlist) {
+ return createErrorActionResult(
+ `playlist ${playlistName} not found`,
+ );
+ }
+
+ // Search for the songs and get their URIs
+ const { uris, notFound } = await searchSongsAndGetUris(
+ songs,
+ clientContext,
+ );
+
+ if (uris.length === 0) {
+ return createErrorActionResult(
+ `Could not find any of the specified songs`,
+ );
+ }
+
+ // Add the tracks to the playlist
+ await addTracksToPlaylist(clientContext.service, playlist.id, uris);
+
+ let resultMessage = `Added ${uris.length} song${uris.length > 1 ? "s" : ""} to playlist ${playlist.name}`;
+ if (notFound.length > 0) {
+ resultMessage += `\nCouldn't find: ${notFound.join(", ")}`;
+ }
+
+ return createActionResultFromTextDisplay(
+ chalk.magentaBright(resultMessage),
+ );
+ }
default:
return createErrorActionResult(
`Action not supported: ${(action as any).actionName}`,
From 3938cc652c31d5b8178f8f8ca704c3f2b0453dae Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 12:20:38 -0800
Subject: [PATCH 09/21] Add coderWrapper package for CLI assistant PTY wrapper
This package provides a pseudo terminal wrapper for CLI coding assistants
like Claude Code, with support for transparent I/O passthrough and future
caching capabilities.
Features:
- PTY wrapper using node-pty for proper terminal emulation
- Transparent passthrough of all stdin/stdout/stderr
- Support for multiple CLI assistants (Claude Code, Node REPL, Python, etc.)
- Windows compatibility with automatic .exe handling
- Graceful exit handling (Ctrl+C, SIGTERM)
- Terminal features: colors, cursor control, resizing
- Configurable assistant system for extensibility
Architecture:
- assistantConfig.ts: Pluggable configuration for different assistants
- ptyWrapper.ts: Core PTY wrapper with node-pty
- cli.ts: Command-line interface entry point
- index.ts: Public API exports
Usage:
coder-wrapper # Use Claude Code (default)
coder-wrapper -a node # Use Node REPL
coder-wrapper -a python # Use Python REPL
Future enhancements:
- Cache checking with TypeAgent dispatcher before forwarding requests
- Request/response logging and performance metrics
- Cache hit/miss statistics
Testing requires a real terminal (TTY) - see TESTING.md for manual test
procedures.
Co-Authored-By: Claude Sonnet 4.5
---
ts/packages/coderWrapper/README.md | 122 +++++++
ts/packages/coderWrapper/TESTING.md | 75 ++++
ts/packages/coderWrapper/package.json | 41 +++
ts/packages/coderWrapper/pnpm-lock.yaml | 344 ++++++++++++++++++
.../coderWrapper/src/assistantConfig.ts | 62 ++++
ts/packages/coderWrapper/src/cli.ts | 117 ++++++
ts/packages/coderWrapper/src/index.ts | 9 +
ts/packages/coderWrapper/src/ptyWrapper.ts | 143 ++++++++
ts/packages/coderWrapper/test-wrapper.js | 37 ++
ts/packages/coderWrapper/tsconfig.json | 8 +
10 files changed, 958 insertions(+)
create mode 100644 ts/packages/coderWrapper/README.md
create mode 100644 ts/packages/coderWrapper/TESTING.md
create mode 100644 ts/packages/coderWrapper/package.json
create mode 100644 ts/packages/coderWrapper/pnpm-lock.yaml
create mode 100644 ts/packages/coderWrapper/src/assistantConfig.ts
create mode 100644 ts/packages/coderWrapper/src/cli.ts
create mode 100644 ts/packages/coderWrapper/src/index.ts
create mode 100644 ts/packages/coderWrapper/src/ptyWrapper.ts
create mode 100644 ts/packages/coderWrapper/test-wrapper.js
create mode 100644 ts/packages/coderWrapper/tsconfig.json
diff --git a/ts/packages/coderWrapper/README.md b/ts/packages/coderWrapper/README.md
new file mode 100644
index 000000000..376d26a31
--- /dev/null
+++ b/ts/packages/coderWrapper/README.md
@@ -0,0 +1,122 @@
+# Coder Wrapper
+
+A pseudo terminal wrapper for CLI coding assistants (Claude Code, etc.) with caching support.
+
+## Overview
+
+The Coder Wrapper provides a transparent PTY (pseudo terminal) wrapper around CLI coding assistants. It:
+
+- **Spawns assistants** in a pseudo terminal for proper TTY support
+- **Transparently passes through** all I/O between the user and the assistant
+- **Supports multiple assistants** with a pluggable configuration system
+- **Will add caching** (future) to check TypeAgent cache before forwarding requests
+
+## Installation
+
+```bash
+cd packages/coderWrapper
+npm install
+npm run build
+```
+
+## Usage
+
+### Basic Usage
+
+```bash
+# Use Claude Code (default)
+npm start
+
+# Or using the built binary
+node dist/cli.js
+```
+
+### Command Line Options
+
+```bash
+coder-wrapper [options]
+
+Options:
+ -a, --assistant Specify the assistant to use (default: claude)
+ -h, --help Show this help message
+```
+
+### Examples
+
+```bash
+# Use Claude Code
+coder-wrapper
+
+# Explicitly specify Claude
+coder-wrapper -a claude
+```
+
+## How It Works
+
+1. **PTY Wrapper**: Uses `node-pty` to spawn the assistant in a pseudo terminal
+2. **Transparent I/O**: All stdin/stdout/stderr is passed through unchanged
+3. **Terminal Features**: Supports colors, cursor control, and terminal resizing
+4. **Clean Exit**: Handles Ctrl+C and process termination gracefully
+
+## Architecture
+
+```
+┌─────────────┐
+│ User │
+└──────┬──────┘
+ │ stdin/stdout
+┌──────▼──────────┐
+│ Coder Wrapper │
+│ (node-pty) │
+└──────┬──────────┘
+ │ PTY
+┌──────▼──────────┐
+│ Claude Code CLI │
+│ (or other) │
+└─────────────────┘
+```
+
+## Adding New Assistants
+
+Edit `src/assistantConfig.ts` to add new assistants:
+
+```typescript
+export const ASSISTANT_CONFIGS: Record = {
+ claude: {
+ name: "Claude Code",
+ command: "claude",
+ args: [],
+ },
+ aider: {
+ name: "Aider",
+ command: "aider",
+ args: [],
+ },
+ // Add more...
+};
+```
+
+## Future Enhancements
+
+- [ ] Cache checking before forwarding to assistant
+- [ ] Request/response logging
+- [ ] Performance metrics
+- [ ] Cache hit/miss statistics
+- [ ] Support for intercepting and modifying requests
+
+## Development
+
+```bash
+# Build
+npm run build
+
+# Format
+npm run prettier:fix
+
+# Clean
+npm run clean
+```
+
+## License
+
+MIT
diff --git a/ts/packages/coderWrapper/TESTING.md b/ts/packages/coderWrapper/TESTING.md
new file mode 100644
index 000000000..6e7accdbf
--- /dev/null
+++ b/ts/packages/coderWrapper/TESTING.md
@@ -0,0 +1,75 @@
+# Testing Coder Wrapper
+
+The coder wrapper uses `node-pty` which requires a real TTY (terminal) to function properly. It cannot be tested with automated scripts that pipe input.
+
+## Manual Testing
+
+### Test 1: Node REPL (Simple Test)
+
+```bash
+cd packages/coderWrapper
+node dist/cli.js -a node
+```
+
+Expected behavior:
+- Node REPL should start
+- You should see the `>` prompt
+- Type `1+1` and press Enter → should show `2`
+- Type `.exit` to quit
+- All colors and formatting should work
+
+### Test 2: Python REPL
+
+```bash
+node dist/cli.js -a python
+```
+
+Expected behavior:
+- Python REPL should start
+- You should see the `>>>` prompt
+- Type `print("Hello")` and press Enter → should show `Hello`
+- Type `exit()` to quit
+
+### Test 3: Claude Code (If Installed)
+
+```bash
+node dist/cli.js -a claude
+# or just
+node dist/cli.js
+```
+
+Expected behavior:
+- Claude Code CLI should start
+- All interactive features should work
+- Colors, prompts, and formatting preserved
+- Terminal resizing should work
+- Ctrl+C should exit gracefully
+
+## What to Verify
+
+✓ **Transparent Passthrough**: All output appears exactly as it would running the command directly
+✓ **Colors**: ANSI colors and formatting work correctly
+✓ **Interactivity**: Prompts, input, and responses work in real-time
+✓ **Terminal Resizing**: Resizing your terminal window updates the PTY size
+✓ **Clean Exit**: Ctrl+C or typing exit commands work properly
+✓ **Process Management**: No orphaned processes left behind
+
+## Known Limitations
+
+- **Requires Real TTY**: Cannot be tested with piped input/output
+- **Windows**: On Windows, commands must have `.exe` extension or be in PATH
+- **Exit Behavior**: Some commands may not exit cleanly (wrapper handles this)
+
+## Troubleshooting
+
+### "File not found" error
+- Command not in PATH
+- On Windows, ensure command ends with `.exe` or is fully qualified
+
+### "stdin.setRawMode is not a function"
+- You're not running in a real terminal
+- Run directly in terminal, not through a script with pipes
+
+### Process doesn't exit
+- Some commands may not handle stdin close properly
+- Use Ctrl+C to force exit - wrapper handles this gracefully
diff --git a/ts/packages/coderWrapper/package.json b/ts/packages/coderWrapper/package.json
new file mode 100644
index 000000000..11d9663a0
--- /dev/null
+++ b/ts/packages/coderWrapper/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "coder-wrapper",
+ "version": "0.0.1",
+ "private": true,
+ "description": "Pseudo terminal wrapper for CLI coding assistants (Claude Code, etc.) with caching support",
+ "homepage": "https://github.com/microsoft/TypeAgent#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/microsoft/TypeAgent.git",
+ "directory": "ts/packages/coderWrapper"
+ },
+ "license": "MIT",
+ "author": "Microsoft",
+ "type": "module",
+ "exports": {
+ ".": "./dist/index.js"
+ },
+ "types": "./dist/index.d.ts",
+ "bin": {
+ "coder-wrapper": "./dist/cli.js"
+ },
+ "scripts": {
+ "build": "npm run tsc",
+ "clean": "rimraf --glob dist *.tsbuildinfo *.done.build.log",
+ "prettier": "prettier --check . --ignore-path ../../.prettierignore",
+ "prettier:fix": "prettier --write . --ignore-path ../../.prettierignore",
+ "start": "node dist/cli.js",
+ "tsc": "tsc -b"
+ },
+ "dependencies": {
+ "@typeagent/agent-server-client": "workspace:*",
+ "@typeagent/dispatcher-types": "workspace:*",
+ "node-pty": "^1.0.0"
+ },
+ "devDependencies": {
+ "@types/node": "^18.19.3",
+ "prettier": "^3.2.5",
+ "rimraf": "^5.0.5",
+ "typescript": "~5.4.5"
+ }
+}
diff --git a/ts/packages/coderWrapper/pnpm-lock.yaml b/ts/packages/coderWrapper/pnpm-lock.yaml
new file mode 100644
index 000000000..e6e306bd1
--- /dev/null
+++ b/ts/packages/coderWrapper/pnpm-lock.yaml
@@ -0,0 +1,344 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@typeagent/agent-server-client':
+ specifier: workspace:*
+ version: link:../agentServer/client
+ '@typeagent/dispatcher-types':
+ specifier: workspace:*
+ version: link:../dispatcher/types
+ node-pty:
+ specifier: ^1.0.0
+ version: 1.1.0
+ devDependencies:
+ '@types/node':
+ specifier: ^18.19.3
+ version: 18.19.130
+ prettier:
+ specifier: ^3.2.5
+ version: 3.8.0
+ rimraf:
+ specifier: ^5.0.5
+ version: 5.0.10
+ typescript:
+ specifier: ~5.4.5
+ version: 5.4.5
+
+packages:
+
+ '@isaacs/cliui@8.0.2':
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+
+ '@pkgjs/parseargs@0.11.0':
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+
+ '@types/node@18.19.130':
+ resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ engines: {node: '>=12'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ ansi-styles@6.2.3:
+ resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
+ engines: {node: '>=12'}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ brace-expansion@2.0.2:
+ resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+
+ foreground-child@3.3.1:
+ resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
+ engines: {node: '>=14'}
+
+ glob@10.5.0:
+ resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
+ hasBin: true
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ jackspeak@3.4.3:
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
+
+ lru-cache@10.4.3:
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+
+ minimatch@9.0.5:
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minipass@7.1.2:
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ node-addon-api@7.1.1:
+ resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+
+ node-pty@1.1.0:
+ resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==}
+
+ package-json-from-dist@1.0.1:
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-scurry@1.11.1:
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
+ engines: {node: '>=16 || 14 >=14.18'}
+
+ prettier@3.8.0:
+ resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ rimraf@5.0.10:
+ resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-ansi@7.1.2:
+ resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
+ engines: {node: '>=12'}
+
+ typescript@5.4.5:
+ resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@5.26.5:
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+
+snapshots:
+
+ '@isaacs/cliui@8.0.2':
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: string-width@4.2.3
+ strip-ansi: 7.1.2
+ strip-ansi-cjs: strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: wrap-ansi@7.0.0
+
+ '@pkgjs/parseargs@0.11.0':
+ optional: true
+
+ '@types/node@18.19.130':
+ dependencies:
+ undici-types: 5.26.5
+
+ ansi-regex@5.0.1: {}
+
+ ansi-regex@6.2.2: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ ansi-styles@6.2.3: {}
+
+ balanced-match@1.0.2: {}
+
+ brace-expansion@2.0.2:
+ dependencies:
+ balanced-match: 1.0.2
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ eastasianwidth@0.2.0: {}
+
+ emoji-regex@8.0.0: {}
+
+ emoji-regex@9.2.2: {}
+
+ foreground-child@3.3.1:
+ dependencies:
+ cross-spawn: 7.0.6
+ signal-exit: 4.1.0
+
+ glob@10.5.0:
+ dependencies:
+ foreground-child: 3.3.1
+ jackspeak: 3.4.3
+ minimatch: 9.0.5
+ minipass: 7.1.2
+ package-json-from-dist: 1.0.1
+ path-scurry: 1.11.1
+
+ is-fullwidth-code-point@3.0.0: {}
+
+ isexe@2.0.0: {}
+
+ jackspeak@3.4.3:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+
+ lru-cache@10.4.3: {}
+
+ minimatch@9.0.5:
+ dependencies:
+ brace-expansion: 2.0.2
+
+ minipass@7.1.2: {}
+
+ node-addon-api@7.1.1: {}
+
+ node-pty@1.1.0:
+ dependencies:
+ node-addon-api: 7.1.1
+
+ package-json-from-dist@1.0.1: {}
+
+ path-key@3.1.1: {}
+
+ path-scurry@1.11.1:
+ dependencies:
+ lru-cache: 10.4.3
+ minipass: 7.1.2
+
+ prettier@3.8.0: {}
+
+ rimraf@5.0.10:
+ dependencies:
+ glob: 10.5.0
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ signal-exit@4.1.0: {}
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string-width@5.1.2:
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.1.2
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-ansi@7.1.2:
+ dependencies:
+ ansi-regex: 6.2.2
+
+ typescript@5.4.5: {}
+
+ undici-types@5.26.5: {}
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@8.1.0:
+ dependencies:
+ ansi-styles: 6.2.3
+ string-width: 5.1.2
+ strip-ansi: 7.1.2
diff --git a/ts/packages/coderWrapper/src/assistantConfig.ts b/ts/packages/coderWrapper/src/assistantConfig.ts
new file mode 100644
index 000000000..7af1d2def
--- /dev/null
+++ b/ts/packages/coderWrapper/src/assistantConfig.ts
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+/**
+ * Configuration for different CLI coding assistants
+ */
+export interface AssistantConfig {
+ name: string;
+ command: string;
+ args: string[];
+ env?: Record;
+}
+
+/**
+ * Predefined assistant configurations
+ */
+export const ASSISTANT_CONFIGS: Record = {
+ claude: {
+ name: "Claude Code",
+ command: "claude",
+ args: [],
+ },
+ node: {
+ name: "Node REPL",
+ command: "node",
+ args: [],
+ },
+ python: {
+ name: "Python REPL",
+ command: "python",
+ args: [],
+ },
+ // Add more assistants as needed
+ // aider: {
+ // name: "Aider",
+ // command: "aider",
+ // args: [],
+ // },
+ // cursor: {
+ // name: "Cursor",
+ // command: "cursor",
+ // args: [],
+ // },
+};
+
+/**
+ * Get assistant configuration by name or return default
+ */
+export function getAssistantConfig(
+ assistantName?: string,
+): AssistantConfig {
+ const name = assistantName || "claude";
+ const config = ASSISTANT_CONFIGS[name];
+
+ if (!config) {
+ throw new Error(
+ `Unknown assistant: ${name}. Available: ${Object.keys(ASSISTANT_CONFIGS).join(", ")}`,
+ );
+ }
+
+ return config;
+}
diff --git a/ts/packages/coderWrapper/src/cli.ts b/ts/packages/coderWrapper/src/cli.ts
new file mode 100644
index 000000000..63c448f67
--- /dev/null
+++ b/ts/packages/coderWrapper/src/cli.ts
@@ -0,0 +1,117 @@
+#!/usr/bin/env node
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { PtyWrapper } from "./ptyWrapper.js";
+import { getAssistantConfig } from "./assistantConfig.js";
+
+/**
+ * Parse command line arguments
+ */
+function parseArgs(): { assistant: string; help: boolean } {
+ const args = process.argv.slice(2);
+ let assistant = "claude";
+ let help = false;
+
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i];
+ if (arg === "--help" || arg === "-h") {
+ help = true;
+ } else if (arg === "--assistant" || arg === "-a") {
+ assistant = args[++i];
+ }
+ }
+
+ return { assistant, help };
+}
+
+/**
+ * Print usage information
+ */
+function printUsage(): void {
+ console.log(`
+Usage: coder-wrapper [options]
+
+Options:
+ -a, --assistant Specify the assistant to use (default: claude)
+ -h, --help Show this help message
+
+Available assistants:
+ claude Claude Code CLI
+
+Examples:
+ coder-wrapper # Use Claude Code (default)
+ coder-wrapper -a claude # Explicitly use Claude Code
+
+Description:
+ Wraps CLI coding assistants in a pseudo terminal with caching support.
+ The wrapper transparently passes through all I/O to/from the assistant.
+ Future versions will add TypeAgent cache checking before forwarding requests.
+`);
+}
+
+/**
+ * Main CLI entry point
+ */
+async function main() {
+ const { assistant, help } = parseArgs();
+
+ if (help) {
+ printUsage();
+ process.exit(0);
+ }
+
+ try {
+ // Get assistant configuration
+ const config = getAssistantConfig(assistant);
+
+ console.log(`[CoderWrapper] Starting ${config.name}...`);
+ console.log(`[CoderWrapper] Command: ${config.command} ${config.args.join(" ")}`);
+ console.log(
+ `[CoderWrapper] Press Ctrl+C to exit or type 'exit' in the assistant\n`,
+ );
+
+ // Create and spawn the PTY wrapper
+ const wrapper = new PtyWrapper(config, {
+ cols: process.stdout.columns,
+ rows: process.stdout.rows,
+ });
+
+ wrapper.spawn();
+
+ // Handle Ctrl+C gracefully
+ process.on("SIGINT", () => {
+ console.log("\n[CoderWrapper] Received SIGINT, shutting down...");
+ wrapper.kill();
+ process.exit(0);
+ });
+
+ // Handle SIGTERM
+ process.on("SIGTERM", () => {
+ console.log("\n[CoderWrapper] Received SIGTERM, shutting down...");
+ wrapper.kill();
+ process.exit(0);
+ });
+
+ // Keep process alive while wrapper is running
+ const checkInterval = setInterval(() => {
+ if (!wrapper.isRunning()) {
+ clearInterval(checkInterval);
+ process.exit(0);
+ }
+ }, 1000);
+ } catch (error) {
+ console.error(
+ `[CoderWrapper] Error: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ process.exit(1);
+ }
+}
+
+// Run the CLI
+main().catch((error) => {
+ console.error(
+ `[CoderWrapper] Fatal error: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ process.exit(1);
+});
diff --git a/ts/packages/coderWrapper/src/index.ts b/ts/packages/coderWrapper/src/index.ts
new file mode 100644
index 000000000..cfe705c9b
--- /dev/null
+++ b/ts/packages/coderWrapper/src/index.ts
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+export { PtyWrapper, PtyWrapperOptions } from "./ptyWrapper.js";
+export {
+ AssistantConfig,
+ ASSISTANT_CONFIGS,
+ getAssistantConfig,
+} from "./assistantConfig.js";
diff --git a/ts/packages/coderWrapper/src/ptyWrapper.ts b/ts/packages/coderWrapper/src/ptyWrapper.ts
new file mode 100644
index 000000000..0f7c4e013
--- /dev/null
+++ b/ts/packages/coderWrapper/src/ptyWrapper.ts
@@ -0,0 +1,143 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import * as pty from "node-pty";
+import { AssistantConfig } from "./assistantConfig.js";
+
+/**
+ * Options for the PTY wrapper
+ */
+export interface PtyWrapperOptions {
+ cols?: number;
+ rows?: number;
+ cwd?: string;
+ env?: Record;
+}
+
+/**
+ * Wraps a CLI coding assistant in a pseudo terminal for transparent I/O
+ */
+export class PtyWrapper {
+ private ptyProcess: pty.IPty | null = null;
+ private readonly config: AssistantConfig;
+ private readonly options: PtyWrapperOptions;
+
+ constructor(config: AssistantConfig, options: PtyWrapperOptions = {}) {
+ this.config = config;
+ // Filter out undefined values from process.env
+ const cleanEnv: Record = {};
+ for (const [key, value] of Object.entries(process.env)) {
+ if (value !== undefined) {
+ cleanEnv[key] = value;
+ }
+ }
+ this.options = {
+ cols: options.cols || 80,
+ rows: options.rows || 30,
+ cwd: options.cwd || process.cwd(),
+ env: { ...cleanEnv, ...config.env, ...options.env },
+ };
+ }
+
+ /**
+ * Spawn the assistant process in a pseudo terminal
+ */
+ spawn(): void {
+ if (this.ptyProcess) {
+ throw new Error("Process already spawned");
+ }
+
+ console.log(
+ `[CoderWrapper] Spawning ${this.config.name} (${this.config.command})`,
+ );
+
+ // On Windows, node-pty needs .exe extension or full path
+ let command = this.config.command;
+ if (process.platform === "win32" && !command.endsWith(".exe")) {
+ command = command + ".exe";
+ }
+
+ this.ptyProcess = pty.spawn(command, this.config.args, {
+ name: "xterm-256color",
+ cols: this.options.cols!,
+ rows: this.options.rows!,
+ cwd: this.options.cwd!,
+ env: this.options.env as any,
+ });
+
+ // Set up data handler for transparent passthrough
+ this.ptyProcess.onData((data: string) => {
+ process.stdout.write(data);
+ });
+
+ // Handle process exit
+ this.ptyProcess.onExit(({ exitCode, signal }) => {
+ console.log(
+ `\n[CoderWrapper] ${this.config.name} exited with code ${exitCode}${signal ? ` (signal: ${signal})` : ""}`,
+ );
+ this.ptyProcess = null;
+ });
+
+ // Handle stdin from the user
+ process.stdin.setRawMode(true);
+ process.stdin.on("data", (data: Buffer) => {
+ if (this.ptyProcess) {
+ this.ptyProcess.write(data.toString());
+ }
+ });
+
+ // Handle terminal resize
+ process.stdout.on("resize", () => {
+ if (this.ptyProcess) {
+ const cols = process.stdout.columns;
+ const rows = process.stdout.rows;
+ this.ptyProcess.resize(cols, rows);
+ }
+ });
+ }
+
+ /**
+ * Write data to the assistant's stdin
+ */
+ write(data: string): void {
+ if (!this.ptyProcess) {
+ throw new Error("Process not spawned");
+ }
+ this.ptyProcess.write(data);
+ }
+
+ /**
+ * Resize the pseudo terminal
+ */
+ resize(cols: number, rows: number): void {
+ if (!this.ptyProcess) {
+ throw new Error("Process not spawned");
+ }
+ this.ptyProcess.resize(cols, rows);
+ }
+
+ /**
+ * Kill the assistant process
+ */
+ kill(signal?: string): void {
+ if (this.ptyProcess) {
+ console.log(`\n[CoderWrapper] Killing ${this.config.name}...`);
+ this.ptyProcess.kill(signal);
+ this.ptyProcess = null;
+ }
+ }
+
+ /**
+ * Check if the process is running
+ */
+ isRunning(): boolean {
+ return this.ptyProcess !== null;
+ }
+
+ /**
+ * Get the process ID
+ */
+ getPid(): number | undefined {
+ return this.ptyProcess?.pid;
+ }
+}
diff --git a/ts/packages/coderWrapper/test-wrapper.js b/ts/packages/coderWrapper/test-wrapper.js
new file mode 100644
index 000000000..4c24e3af6
--- /dev/null
+++ b/ts/packages/coderWrapper/test-wrapper.js
@@ -0,0 +1,37 @@
+#!/usr/bin/env node
+// Simple test of the PTY wrapper with Node REPL
+
+import { PtyWrapper } from "./dist/index.js";
+
+console.log("[Test] Testing PTY wrapper with Node REPL...");
+console.log("[Test] Type 'console.log(\"Hello from Node!\")' to test");
+console.log("[Test] Type '.exit' to quit\n");
+
+const config = {
+ name: "Node REPL",
+ command: "node",
+ args: [],
+};
+
+const wrapper = new PtyWrapper(config, {
+ cols: process.stdout.columns,
+ rows: process.stdout.rows,
+});
+
+wrapper.spawn();
+
+// Handle Ctrl+C
+process.on("SIGINT", () => {
+ console.log("\n[Test] Shutting down...");
+ wrapper.kill();
+ process.exit(0);
+});
+
+// Keep process alive
+const checkInterval = setInterval(() => {
+ if (!wrapper.isRunning()) {
+ console.log("[Test] Node REPL exited");
+ clearInterval(checkInterval);
+ process.exit(0);
+ }
+}, 1000);
diff --git a/ts/packages/coderWrapper/tsconfig.json b/ts/packages/coderWrapper/tsconfig.json
new file mode 100644
index 000000000..a013e0c30
--- /dev/null
+++ b/ts/packages/coderWrapper/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src/**/*"]
+}
From b88be7b9461ad99bfbfa4876f0ba27567713857c Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 12:24:21 -0800
Subject: [PATCH 10/21] Fix prettier formatting for coderWrapper package
---
ts/packages/coderWrapper/README.md | 22 +++++++++----------
ts/packages/coderWrapper/TESTING.md | 6 +++++
.../coderWrapper/src/assistantConfig.ts | 4 +---
ts/packages/coderWrapper/src/cli.ts | 4 +++-
4 files changed, 21 insertions(+), 15 deletions(-)
diff --git a/ts/packages/coderWrapper/README.md b/ts/packages/coderWrapper/README.md
index 376d26a31..3f9a94250 100644
--- a/ts/packages/coderWrapper/README.md
+++ b/ts/packages/coderWrapper/README.md
@@ -82,17 +82,17 @@ Edit `src/assistantConfig.ts` to add new assistants:
```typescript
export const ASSISTANT_CONFIGS: Record = {
- claude: {
- name: "Claude Code",
- command: "claude",
- args: [],
- },
- aider: {
- name: "Aider",
- command: "aider",
- args: [],
- },
- // Add more...
+ claude: {
+ name: "Claude Code",
+ command: "claude",
+ args: [],
+ },
+ aider: {
+ name: "Aider",
+ command: "aider",
+ args: [],
+ },
+ // Add more...
};
```
diff --git a/ts/packages/coderWrapper/TESTING.md b/ts/packages/coderWrapper/TESTING.md
index 6e7accdbf..4c53bc962 100644
--- a/ts/packages/coderWrapper/TESTING.md
+++ b/ts/packages/coderWrapper/TESTING.md
@@ -12,6 +12,7 @@ node dist/cli.js -a node
```
Expected behavior:
+
- Node REPL should start
- You should see the `>` prompt
- Type `1+1` and press Enter → should show `2`
@@ -25,6 +26,7 @@ node dist/cli.js -a python
```
Expected behavior:
+
- Python REPL should start
- You should see the `>>>` prompt
- Type `print("Hello")` and press Enter → should show `Hello`
@@ -39,6 +41,7 @@ node dist/cli.js
```
Expected behavior:
+
- Claude Code CLI should start
- All interactive features should work
- Colors, prompts, and formatting preserved
@@ -63,13 +66,16 @@ Expected behavior:
## Troubleshooting
### "File not found" error
+
- Command not in PATH
- On Windows, ensure command ends with `.exe` or is fully qualified
### "stdin.setRawMode is not a function"
+
- You're not running in a real terminal
- Run directly in terminal, not through a script with pipes
### Process doesn't exit
+
- Some commands may not handle stdin close properly
- Use Ctrl+C to force exit - wrapper handles this gracefully
diff --git a/ts/packages/coderWrapper/src/assistantConfig.ts b/ts/packages/coderWrapper/src/assistantConfig.ts
index 7af1d2def..dd3559644 100644
--- a/ts/packages/coderWrapper/src/assistantConfig.ts
+++ b/ts/packages/coderWrapper/src/assistantConfig.ts
@@ -46,9 +46,7 @@ export const ASSISTANT_CONFIGS: Record = {
/**
* Get assistant configuration by name or return default
*/
-export function getAssistantConfig(
- assistantName?: string,
-): AssistantConfig {
+export function getAssistantConfig(assistantName?: string): AssistantConfig {
const name = assistantName || "claude";
const config = ASSISTANT_CONFIGS[name];
diff --git a/ts/packages/coderWrapper/src/cli.ts b/ts/packages/coderWrapper/src/cli.ts
index 63c448f67..b2933e49c 100644
--- a/ts/packages/coderWrapper/src/cli.ts
+++ b/ts/packages/coderWrapper/src/cli.ts
@@ -66,7 +66,9 @@ async function main() {
const config = getAssistantConfig(assistant);
console.log(`[CoderWrapper] Starting ${config.name}...`);
- console.log(`[CoderWrapper] Command: ${config.command} ${config.args.join(" ")}`);
+ console.log(
+ `[CoderWrapper] Command: ${config.command} ${config.args.join(" ")}`,
+ );
console.log(
`[CoderWrapper] Press Ctrl+C to exit or type 'exit' in the assistant\n`,
);
From 4c5b90d4e338c4459d9be3ec6d78f331eb7c4753 Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 12:26:20 -0800
Subject: [PATCH 11/21] Update pnpm-lock.yaml for coderWrapper package
---
ts/pnpm-lock.yaml | 285 +++++++++++++++++++++++++++++++++++++++-------
1 file changed, 242 insertions(+), 43 deletions(-)
diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml
index 66fdcac77..fe089cfb3 100644
--- a/ts/pnpm-lock.yaml
+++ b/ts/pnpm-lock.yaml
@@ -139,7 +139,7 @@ importers:
version: 8.18.1
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@20.19.23)(ts-node@10.9.2(@types/node@20.19.23)(typescript@5.4.5))
+ version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))
prettier:
specifier: ^3.5.3
version: 3.5.3
@@ -2813,6 +2813,31 @@ importers:
specifier: ^6.0.1
version: 6.0.1
+ packages/coderWrapper:
+ dependencies:
+ '@typeagent/agent-server-client':
+ specifier: workspace:*
+ version: link:../agentServer/client
+ '@typeagent/dispatcher-types':
+ specifier: workspace:*
+ version: link:../dispatcher/types
+ node-pty:
+ specifier: ^1.0.0
+ version: 1.1.0
+ devDependencies:
+ '@types/node':
+ specifier: ^18.19.3
+ version: 18.19.130
+ prettier:
+ specifier: ^3.2.5
+ version: 3.5.3
+ rimraf:
+ specifier: ^5.0.5
+ version: 5.0.10
+ typescript:
+ specifier: ~5.4.5
+ version: 5.4.5
+
packages/commandExecutor:
dependencies:
'@modelcontextprotocol/sdk':
@@ -11120,6 +11145,9 @@ packages:
node-addon-api@4.3.0:
resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==}
+ node-addon-api@7.1.1:
+ resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+
node-api-version@0.2.1:
resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==}
@@ -11157,6 +11185,9 @@ packages:
node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
+ node-pty@1.1.0:
+ resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==}
+
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
@@ -15514,7 +15545,7 @@ snapshots:
'@jest/console@29.7.0':
dependencies:
'@jest/types': 29.6.3
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
chalk: 4.1.2
jest-message-util: 29.7.0
jest-util: 29.7.0
@@ -15527,14 +15558,49 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.9.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
- jest-config: 29.7.0(@types/node@20.19.23)(ts-node@10.9.2(@types/node@20.19.23)(typescript@5.4.5))
+ jest-config: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.23)(typescript@5.4.5))
+ jest-haste-map: 29.7.0
+ jest-message-util: 29.7.0
+ jest-regex-util: 29.6.3
+ jest-resolve: 29.7.0
+ jest-resolve-dependencies: 29.7.0
+ jest-runner: 29.7.0
+ jest-runtime: 29.7.0
+ jest-snapshot: 29.7.0
+ jest-util: 29.7.0
+ jest-validate: 29.7.0
+ jest-watcher: 29.7.0
+ micromatch: 4.0.8
+ pretty-format: 29.7.0
+ slash: 3.0.0
+ strip-ansi: 6.0.1
+ transitivePeerDependencies:
+ - babel-plugin-macros
+ - supports-color
+ - ts-node
+
+ '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))':
+ dependencies:
+ '@jest/console': 29.7.0
+ '@jest/reporters': 29.7.0
+ '@jest/test-result': 29.7.0
+ '@jest/transform': 29.7.0
+ '@jest/types': 29.6.3
+ '@types/node': 20.19.25
+ ansi-escapes: 4.3.2
+ chalk: 4.1.2
+ ci-info: 3.9.0
+ exit: 0.1.2
+ graceful-fs: 4.2.11
+ jest-changed-files: 29.7.0
+ jest-config: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
@@ -15562,14 +15628,14 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.9.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
- jest-config: 29.7.0(@types/node@20.19.23)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5))
+ jest-config: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5))
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
@@ -15594,7 +15660,7 @@ snapshots:
dependencies:
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
jest-mock: 29.7.0
'@jest/expect-utils@29.7.0':
@@ -15612,7 +15678,7 @@ snapshots:
dependencies:
'@jest/types': 29.6.3
'@sinonjs/fake-timers': 10.3.0
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
jest-message-util: 29.7.0
jest-mock: 29.7.0
jest-util: 29.7.0
@@ -15634,7 +15700,7 @@ snapshots:
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@jridgewell/trace-mapping': 0.3.31
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
chalk: 4.1.2
collect-v8-coverage: 1.0.2
exit: 0.1.2
@@ -15704,7 +15770,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.5
'@types/istanbul-reports': 3.0.3
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/yargs': 17.0.29
chalk: 4.1.2
@@ -17255,13 +17321,13 @@ snapshots:
'@types/bonjour@3.5.13':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/cacheable-request@6.0.3':
dependencies:
'@types/http-cache-semantics': 4.0.4
'@types/keyv': 3.1.4
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/responselike': 1.0.3
'@types/chrome@0.0.114':
@@ -17286,11 +17352,11 @@ snapshots:
'@types/connect-history-api-fallback@1.5.4':
dependencies:
'@types/express-serve-static-core': 4.19.6
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/connect@3.4.38':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/cors@2.8.18':
dependencies:
@@ -17441,14 +17507,14 @@ snapshots:
'@types/express-serve-static-core@4.17.41':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/qs': 6.9.10
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
'@types/express-serve-static-core@4.19.6':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/qs': 6.9.18
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@@ -17481,7 +17547,7 @@ snapshots:
'@types/fs-extra@8.1.5':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/fs-extra@9.0.13':
dependencies:
@@ -17492,16 +17558,16 @@ snapshots:
'@types/glob@7.2.0':
dependencies:
'@types/minimatch': 5.1.2
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/glob@8.1.0':
dependencies:
'@types/minimatch': 5.1.2
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/graceful-fs@4.1.8':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/har-format@1.2.15': {}
@@ -17519,7 +17585,7 @@ snapshots:
'@types/http-proxy@1.17.14':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/istanbul-lib-coverage@2.0.5': {}
@@ -17544,7 +17610,7 @@ snapshots:
'@types/jsdom@20.0.1':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/tough-cookie': 4.0.5
parse5: 7.3.0
@@ -17558,7 +17624,7 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/jsonpath@0.2.4': {}
@@ -17586,7 +17652,7 @@ snapshots:
'@types/mailparser@3.4.6':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
iconv-lite: 0.6.3
'@types/markdown-it@14.1.2':
@@ -17619,7 +17685,7 @@ snapshots:
'@types/node-forge@1.3.11':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/node@18.19.130':
dependencies:
@@ -17670,7 +17736,7 @@ snapshots:
'@types/responselike@1.0.3':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/retry@0.12.2': {}
@@ -17679,7 +17745,7 @@ snapshots:
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/serve-index@1.9.4':
dependencies:
@@ -17689,7 +17755,7 @@ snapshots:
dependencies:
'@types/http-errors': 2.0.4
'@types/mime': 3.0.4
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/shimmer@1.2.0': {}
@@ -17697,7 +17763,7 @@ snapshots:
'@types/sockjs@0.3.36':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/spotify-api@0.0.25': {}
@@ -17745,7 +17811,7 @@ snapshots:
'@types/yauzl@2.10.3':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
optional: true
'@typespec/ts-http-runtime@0.2.2':
@@ -18164,7 +18230,7 @@ snapshots:
'@swc/helpers': 0.5.15
'@types/command-line-args': 5.2.3
'@types/command-line-usage': 5.0.4
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
command-line-args: 5.2.1
command-line-usage: 7.0.3
flatbuffers: 24.3.25
@@ -19111,6 +19177,21 @@ snapshots:
- supports-color
- ts-node
+ create-jest@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5)):
+ dependencies:
+ '@jest/types': 29.6.3
+ chalk: 4.1.2
+ exit: 0.1.2
+ graceful-fs: 4.2.11
+ jest-config: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))
+ jest-util: 29.7.0
+ prompts: 2.4.2
+ transitivePeerDependencies:
+ - '@types/node'
+ - babel-plugin-macros
+ - supports-color
+ - ts-node
+
create-jest@29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)):
dependencies:
'@jest/types': 29.6.3
@@ -21300,7 +21381,7 @@ snapshots:
'@jest/expect': 29.7.0
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
chalk: 4.1.2
co: 4.6.0
dedent: 1.7.0
@@ -21339,6 +21420,25 @@ snapshots:
- supports-color
- ts-node
+ jest-cli@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5)):
+ dependencies:
+ '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))
+ '@jest/test-result': 29.7.0
+ '@jest/types': 29.6.3
+ chalk: 4.1.2
+ create-jest: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))
+ exit: 0.1.2
+ import-local: 3.2.0
+ jest-config: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))
+ jest-util: 29.7.0
+ jest-validate: 29.7.0
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - '@types/node'
+ - babel-plugin-macros
+ - supports-color
+ - ts-node
+
jest-cli@29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5))
@@ -21389,7 +21489,7 @@ snapshots:
- babel-plugin-macros
- supports-color
- jest-config@29.7.0(@types/node@20.19.23)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)):
+ jest-config@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.23)(typescript@5.4.5)):
dependencies:
'@babel/core': 7.28.4
'@jest/test-sequencer': 29.7.0
@@ -21414,7 +21514,69 @@ snapshots:
slash: 3.0.0
strip-json-comments: 3.1.1
optionalDependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
+ ts-node: 10.9.2(@types/node@20.19.23)(typescript@5.4.5)
+ transitivePeerDependencies:
+ - babel-plugin-macros
+ - supports-color
+
+ jest-config@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5)):
+ dependencies:
+ '@babel/core': 7.28.4
+ '@jest/test-sequencer': 29.7.0
+ '@jest/types': 29.6.3
+ babel-jest: 29.7.0(@babel/core@7.28.4)
+ chalk: 4.1.2
+ ci-info: 3.9.0
+ deepmerge: 4.3.1
+ glob: 7.2.3
+ graceful-fs: 4.2.11
+ jest-circus: 29.7.0
+ jest-environment-node: 29.7.0
+ jest-get-type: 29.6.3
+ jest-regex-util: 29.6.3
+ jest-resolve: 29.7.0
+ jest-runner: 29.7.0
+ jest-util: 29.7.0
+ jest-validate: 29.7.0
+ micromatch: 4.0.8
+ parse-json: 5.2.0
+ pretty-format: 29.7.0
+ slash: 3.0.0
+ strip-json-comments: 3.1.1
+ optionalDependencies:
+ '@types/node': 20.19.25
+ ts-node: 10.9.2(@types/node@20.19.25)(typescript@5.4.5)
+ transitivePeerDependencies:
+ - babel-plugin-macros
+ - supports-color
+
+ jest-config@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)):
+ dependencies:
+ '@babel/core': 7.28.4
+ '@jest/test-sequencer': 29.7.0
+ '@jest/types': 29.6.3
+ babel-jest: 29.7.0(@babel/core@7.28.4)
+ chalk: 4.1.2
+ ci-info: 3.9.0
+ deepmerge: 4.3.1
+ glob: 7.2.3
+ graceful-fs: 4.2.11
+ jest-circus: 29.7.0
+ jest-environment-node: 29.7.0
+ jest-get-type: 29.6.3
+ jest-regex-util: 29.6.3
+ jest-resolve: 29.7.0
+ jest-runner: 29.7.0
+ jest-util: 29.7.0
+ jest-validate: 29.7.0
+ micromatch: 4.0.8
+ parse-json: 5.2.0
+ pretty-format: 29.7.0
+ slash: 3.0.0
+ strip-json-comments: 3.1.1
+ optionalDependencies:
+ '@types/node': 20.19.25
ts-node: 10.9.2(@types/node@22.15.18)(typescript@5.4.5)
transitivePeerDependencies:
- babel-plugin-macros
@@ -21490,7 +21652,7 @@ snapshots:
'@jest/environment': 29.7.0
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
jest-mock: 29.7.0
jest-util: 29.7.0
@@ -21500,7 +21662,7 @@ snapshots:
dependencies:
'@jest/types': 29.6.3
'@types/graceful-fs': 4.1.8
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
anymatch: 3.1.3
fb-watchman: 2.0.2
graceful-fs: 4.2.11
@@ -21539,7 +21701,7 @@ snapshots:
jest-mock@29.7.0:
dependencies:
'@jest/types': 29.6.3
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
jest-util: 29.7.0
jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
@@ -21574,7 +21736,7 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
chalk: 4.1.2
emittery: 0.13.1
graceful-fs: 4.2.11
@@ -21602,7 +21764,7 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
chalk: 4.1.2
cjs-module-lexer: 1.2.3
collect-v8-coverage: 1.0.2
@@ -21648,7 +21810,7 @@ snapshots:
jest-util@29.7.0:
dependencies:
'@jest/types': 29.6.3
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11
@@ -21667,7 +21829,7 @@ snapshots:
dependencies:
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
@@ -21676,13 +21838,13 @@ snapshots:
jest-worker@27.5.1:
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
merge-stream: 2.0.0
supports-color: 8.1.1
jest-worker@29.7.0:
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
jest-util: 29.7.0
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -21699,6 +21861,18 @@ snapshots:
- supports-color
- ts-node
+ jest@29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5)):
+ dependencies:
+ '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))
+ '@jest/types': 29.6.3
+ import-local: 3.2.0
+ jest-cli: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))
+ transitivePeerDependencies:
+ - '@types/node'
+ - babel-plugin-macros
+ - supports-color
+ - ts-node
+
jest@29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5)):
dependencies:
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5))
@@ -22878,6 +23052,8 @@ snapshots:
node-addon-api@4.3.0: {}
+ node-addon-api@7.1.1: {}
+
node-api-version@0.2.1:
dependencies:
semver: 7.7.2
@@ -22917,6 +23093,10 @@ snapshots:
node-int64@0.4.0: {}
+ node-pty@1.1.0:
+ dependencies:
+ node-addon-api: 7.1.1
+
node-releases@2.0.19: {}
node-rsa@1.1.1:
@@ -24942,6 +25122,25 @@ snapshots:
yn: 3.1.1
optional: true
+ ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5):
+ dependencies:
+ '@cspotcode/source-map-support': 0.8.1
+ '@tsconfig/node10': 1.0.9
+ '@tsconfig/node12': 1.0.11
+ '@tsconfig/node14': 1.0.3
+ '@tsconfig/node16': 1.0.4
+ '@types/node': 20.19.25
+ acorn: 8.11.1
+ acorn-walk: 8.3.0
+ arg: 4.1.3
+ create-require: 1.1.1
+ diff: 4.0.2
+ make-error: 1.3.6
+ typescript: 5.4.5
+ v8-compile-cache-lib: 3.0.1
+ yn: 3.1.1
+ optional: true
+
ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5):
dependencies:
'@cspotcode/source-map-support': 0.8.1
From cfa7c49f1f840687d57fd65f3e5f197325064f97 Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 12:27:25 -0800
Subject: [PATCH 12/21] Add copyright header to test-wrapper.js
---
ts/packages/coderWrapper/test-wrapper.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/ts/packages/coderWrapper/test-wrapper.js b/ts/packages/coderWrapper/test-wrapper.js
index 4c24e3af6..12bc2769a 100644
--- a/ts/packages/coderWrapper/test-wrapper.js
+++ b/ts/packages/coderWrapper/test-wrapper.js
@@ -1,4 +1,7 @@
#!/usr/bin/env node
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
// Simple test of the PTY wrapper with Node REPL
import { PtyWrapper } from "./dist/index.js";
From ccd621cf4d239f80de8eaf2c48a4f68ccf994688 Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 12:37:56 -0800
Subject: [PATCH 13/21] Add trademark section to README and copyright header to
pnpm-lock.yaml
---
ts/packages/coderWrapper/README.md | 7 +++++++
ts/packages/coderWrapper/pnpm-lock.yaml | 3 +++
2 files changed, 10 insertions(+)
diff --git a/ts/packages/coderWrapper/README.md b/ts/packages/coderWrapper/README.md
index 3f9a94250..17a3f82ca 100644
--- a/ts/packages/coderWrapper/README.md
+++ b/ts/packages/coderWrapper/README.md
@@ -120,3 +120,10 @@ npm run clean
## License
MIT
+
+## Trademarks
+
+This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
+trademarks or logos is subject to and must follow
+[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
+Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
diff --git a/ts/packages/coderWrapper/pnpm-lock.yaml b/ts/packages/coderWrapper/pnpm-lock.yaml
index e6e306bd1..61ef0db56 100644
--- a/ts/packages/coderWrapper/pnpm-lock.yaml
+++ b/ts/packages/coderWrapper/pnpm-lock.yaml
@@ -1,3 +1,6 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
lockfileVersion: '9.0'
settings:
From c9179eee951685be0db0f906d1959bc69a7c74f6 Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 12:39:52 -0800
Subject: [PATCH 14/21] Complete trademark section with third-party policy
---
ts/packages/coderWrapper/README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/ts/packages/coderWrapper/README.md b/ts/packages/coderWrapper/README.md
index 17a3f82ca..6e2d5af6e 100644
--- a/ts/packages/coderWrapper/README.md
+++ b/ts/packages/coderWrapper/README.md
@@ -127,3 +127,4 @@ This project may contain trademarks or logos for projects, products, or services
trademarks or logos is subject to and must follow
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
+Any use of third-party trademarks or logos are subject to those third-party's policies.
From f2e77f216a39b77c08f552fdfd6d3d18e481ef0a Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 18:00:30 -0800
Subject: [PATCH 15/21] Add cache support to coderWrapper with shared
dispatcher
This commit adds optional caching functionality to the coderWrapper package, allowing
it to check TypeAgent's cache before forwarding commands to the wrapped CLI agent.
Key changes:
**coderWrapper package:**
- Add cacheClient.ts: MCP client for checking TypeAgent cache via command-executor
- Add debugLogger.ts: Debug logging to ~/.tmp/typeagent-coder-wrapper/
- Update ptyWrapper.ts:
- Add cache checking before forwarding commands to wrapped agent
- Handle \r\n separately to prevent duplicate command execution on Windows
- Add processing lock to prevent concurrent command handling
- Add enableCache option (default: false)
- Add debug option for detailed logging
- Update CLI to support --enable-cache and --debug flags
**commandExecutor MCP server:**
- Add startup connection race handling (return cache miss instead of error)
- Add CACHE_HIT prefix for detection by cacheClient
- Improve error handling for disconnected dispatcher
**dispatcher:**
- Add checkCache RPC method to dispatcher interface
- Implement checkCache in dispatcher client and server
- Support multi-client cache sharing via AsyncLocalStorage routing
**agentServer:**
- Add AsyncLocalStorage-based request routing for multi-client support
- Wrap checkCache calls with client context to route responses correctly
- Each client gets isolated cache results despite shared dispatcher
The cache is disabled by default and must be explicitly enabled with enableCache: true
or --enable-cache flag. When enabled and cache hits occur, results are returned
immediately without invoking the wrapped CLI agent.
Co-Authored-By: Claude Sonnet 4.5
---
ts/packages/agentServer/server/src/server.ts | 164 ++++++++-----
ts/packages/coderWrapper/README.md | 43 +++-
ts/packages/coderWrapper/package.json | 1 +
ts/packages/coderWrapper/src/cacheClient.ts | 222 ++++++++++++++++++
ts/packages/coderWrapper/src/cli.ts | 19 +-
ts/packages/coderWrapper/src/debugLogger.ts | 75 ++++++
ts/packages/coderWrapper/src/index.ts | 2 +
ts/packages/coderWrapper/src/ptyWrapper.ts | 196 +++++++++++++++-
.../commandExecutor/src/commandServer.ts | 53 ++++-
.../dispatcher/dispatcher/src/dispatcher.ts | 46 ++++
.../dispatcher/rpc/src/dispatcherClient.ts | 3 +
.../dispatcher/rpc/src/dispatcherServer.ts | 3 +
.../dispatcher/rpc/src/dispatcherTypes.ts | 2 +
.../dispatcher/types/src/dispatcher.ts | 3 +
ts/pnpm-lock.yaml | 3 +
15 files changed, 761 insertions(+), 74 deletions(-)
create mode 100644 ts/packages/coderWrapper/src/cacheClient.ts
create mode 100644 ts/packages/coderWrapper/src/debugLogger.ts
diff --git a/ts/packages/agentServer/server/src/server.ts b/ts/packages/agentServer/server/src/server.ts
index 97456ec7d..22c9302ba 100644
--- a/ts/packages/agentServer/server/src/server.ts
+++ b/ts/packages/agentServer/server/src/server.ts
@@ -3,7 +3,7 @@
import { createWebSocketChannelServer } from "websocket-channel-server";
import { createDispatcherRpcServer } from "@typeagent/dispatcher-rpc/dispatcher/server";
-import { ClientIO, createDispatcher, RequestId } from "agent-dispatcher";
+import { ClientIO, createDispatcher } from "agent-dispatcher";
import { getInstanceDir, getClientId } from "agent-dispatcher/helpers/data";
import {
getDefaultAppAgentProviders,
@@ -14,46 +14,91 @@ import { getFsStorageProvider } from "dispatcher-node-providers";
import { ChannelProvider } from "@typeagent/agent-rpc/channel";
import { createClientIORpcClient } from "@typeagent/dispatcher-rpc/clientio/client";
import { createRpc } from "@typeagent/agent-rpc/rpc";
-import { createPromiseWithResolvers } from "@typeagent/common-utils";
import {
AgentServerInvokeFunctions,
ChannelName,
} from "@typeagent/agent-server-protocol";
+import { AsyncLocalStorage } from "async_hooks";
import dotenv from "dotenv";
const envPath = new URL("../../../../.env", import.meta.url);
dotenv.config({ path: envPath });
-const nullClientIO: ClientIO = {
- clear: () => {},
- exit: () => {},
- setDisplayInfo: () => {},
- setDisplay: () => {},
- appendDisplay: () => {},
- appendDiagnosticData: () => {},
- setDynamicDisplay: () => {},
- askYesNo: async (
- message: string,
- requestId: RequestId,
- defaultValue: boolean = false,
- ) => defaultValue,
- proposeAction: async () => undefined,
- popupQuestion: async () => {
- throw new Error("popupQuestion not implemented");
- },
- notify: () => {},
- openLocalView: () => {},
- closeLocalView: () => {},
- takeAction: (action: string) => {
- throw new Error(`Action ${action} not supported`);
- },
-};
+// AsyncLocalStorage to track which client is making the current request
+const currentClientContext = new AsyncLocalStorage();
async function main() {
- const clientIO = {
- ...nullClientIO,
- };
const instanceDir = getInstanceDir();
+ // Track all connected clients and their ClientIO
+ const connectedClients = new Map void }>();
+
+ // Create a routing ClientIO that forwards calls to the current request's client
+ const routingClientIO: ClientIO = {
+ clear: (...args) => {
+ const client = currentClientContext.getStore();
+ client?.clear?.(...args);
+ },
+ exit: (...args) => {
+ const client = currentClientContext.getStore();
+ client?.exit?.(...args);
+ },
+ setDisplayInfo: (...args) => {
+ const client = currentClientContext.getStore();
+ client?.setDisplayInfo?.(...args);
+ },
+ setDisplay: (...args) => {
+ const client = currentClientContext.getStore();
+ client?.setDisplay?.(...args);
+ },
+ appendDisplay: (...args) => {
+ const client = currentClientContext.getStore();
+ client?.appendDisplay?.(...args);
+ },
+ appendDiagnosticData: (...args) => {
+ const client = currentClientContext.getStore();
+ client?.appendDiagnosticData?.(...args);
+ },
+ setDynamicDisplay: (...args) => {
+ const client = currentClientContext.getStore();
+ client?.setDynamicDisplay?.(...args);
+ },
+ askYesNo: async (...args) => {
+ const client = currentClientContext.getStore();
+ return client?.askYesNo?.(...args) ?? false;
+ },
+ proposeAction: async (...args) => {
+ const client = currentClientContext.getStore();
+ return client?.proposeAction?.(...args);
+ },
+ popupQuestion: async (...args) => {
+ const client = currentClientContext.getStore();
+ if (!client?.popupQuestion) {
+ throw new Error("popupQuestion not implemented");
+ }
+ return client.popupQuestion(...args);
+ },
+ notify: (...args) => {
+ const client = currentClientContext.getStore();
+ client?.notify?.(...args);
+ },
+ openLocalView: (...args) => {
+ const client = currentClientContext.getStore();
+ client?.openLocalView?.(...args);
+ },
+ closeLocalView: (...args) => {
+ const client = currentClientContext.getStore();
+ client?.closeLocalView?.(...args);
+ },
+ takeAction: (action: string, data?: unknown) => {
+ const client = currentClientContext.getStore();
+ if (!client?.takeAction) {
+ throw new Error(`Action ${action} not supported`);
+ }
+ return client.takeAction(action, data);
+ },
+ };
+
+ // Create single shared dispatcher with routing ClientIO
const dispatcher = await createDispatcher("agent server", {
appAgentProviders: getDefaultAppAgentProviders(instanceDir),
persistSession: true,
@@ -62,7 +107,7 @@ async function main() {
metrics: true,
dblogging: false,
clientId: getClientId(),
- clientIO,
+ clientIO: routingClientIO,
indexingServiceRegistry: await getIndexingServiceRegistry(instanceDir),
constructionProvider: getDefaultConstructionProvider(),
conversationMemorySettings: {
@@ -74,39 +119,15 @@ async function main() {
// Ignore dispatcher close requests
dispatcher.close = async () => {};
- let currentChannelProvider: ChannelProvider | undefined;
- let currentCloseFn: (() => void) | undefined;
await createWebSocketChannelServer(
{ port: 8999 },
(channelProvider, closeFn) => {
const invokeFunctions: AgentServerInvokeFunctions = {
join: async () => {
- if (currentChannelProvider !== undefined) {
- if (channelProvider === currentChannelProvider) {
- throw new Error("Already joined");
- }
-
- const promiseWithResolvers =
- createPromiseWithResolvers();
- currentChannelProvider.on("disconnect", () => {
- promiseWithResolvers.resolve();
- });
- currentCloseFn!();
- await promiseWithResolvers.promise;
- }
-
- if (currentChannelProvider) {
- throw new Error("Unable to disconnect");
+ if (connectedClients.has(channelProvider)) {
+ throw new Error("Already joined");
}
- currentChannelProvider = channelProvider;
- currentCloseFn = closeFn;
- channelProvider.on("disconnect", () => {
- currentChannelProvider = undefined;
- currentCloseFn = undefined;
- Object.assign(clientIO, nullClientIO);
- });
-
const dispatcherChannel = channelProvider.createChannel(
ChannelName.Dispatcher,
);
@@ -115,8 +136,35 @@ async function main() {
);
const clientIORpcClient =
createClientIORpcClient(clientIOChannel);
- Object.assign(clientIO, clientIORpcClient);
- createDispatcherRpcServer(dispatcher, dispatcherChannel);
+
+ // Store this client's ClientIO
+ connectedClients.set(channelProvider, {
+ clientIO: clientIORpcClient,
+ closeFn,
+ });
+
+ channelProvider.on("disconnect", () => {
+ connectedClients.delete(channelProvider);
+ console.log(`Client disconnected. Active connections: ${connectedClients.size}`);
+ });
+
+ // Wrap the dispatcher RPC server to set context for each request
+ const wrappedDispatcher = {
+ ...dispatcher,
+ processCommand: async (command: string, requestId?: string, attachments?: string[]) => {
+ return currentClientContext.run(clientIORpcClient, () =>
+ dispatcher.processCommand(command, requestId, attachments)
+ );
+ },
+ checkCache: async (request: string) => {
+ return currentClientContext.run(clientIORpcClient, () =>
+ dispatcher.checkCache(request)
+ );
+ },
+ };
+
+ createDispatcherRpcServer(wrappedDispatcher as any, dispatcherChannel);
+ console.log(`Client connected. Active connections: ${connectedClients.size}`);
},
};
diff --git a/ts/packages/coderWrapper/README.md b/ts/packages/coderWrapper/README.md
index 6e2d5af6e..2a0e37233 100644
--- a/ts/packages/coderWrapper/README.md
+++ b/ts/packages/coderWrapper/README.md
@@ -38,6 +38,7 @@ coder-wrapper [options]
Options:
-a, --assistant Specify the assistant to use (default: claude)
+ -d, --debug Enable debug logging with cache timing information
-h, --help Show this help message
```
@@ -49,14 +50,36 @@ coder-wrapper
# Explicitly specify Claude
coder-wrapper -a claude
+
+# Enable debug mode to see cache hit/miss timing
+coder-wrapper --debug
```
## How It Works
1. **PTY Wrapper**: Uses `node-pty` to spawn the assistant in a pseudo terminal
-2. **Transparent I/O**: All stdin/stdout/stderr is passed through unchanged
-3. **Terminal Features**: Supports colors, cursor control, and terminal resizing
-4. **Clean Exit**: Handles Ctrl+C and process termination gracefully
+2. **Cache Checking**: Intercepts user input and checks TypeAgent cache before forwarding to assistant
+3. **Cache Hit**: Executes cached actions and returns results immediately (bypasses assistant)
+4. **Cache Miss**: Forwards input to the assistant normally
+5. **Transparent I/O**: All stdin/stdout/stderr is passed through unchanged
+6. **Terminal Features**: Supports colors, cursor control, and terminal resizing
+7. **Clean Exit**: Handles Ctrl+C and process termination gracefully
+
+### Debug Mode
+
+When `--debug` is enabled, the wrapper logs:
+- Cache check attempts with command text
+- Cache hit/miss status with timing (in milliseconds)
+- Whether request was forwarded to assistant
+- Total time for cache hits
+
+Example debug output:
+```
+[CoderWrapper:Debug] Checking cache for: "play hello by adele"
+[CoderWrapper:Debug] ✓ Cache HIT (234.56ms)
+[Action result displayed here]
+[CoderWrapper:Debug] Command completed from cache in 234.56ms
+```
## Architecture
@@ -96,13 +119,19 @@ export const ASSISTANT_CONFIGS: Record = {
};
```
+## Features
+
+- [x] Cache checking before forwarding to assistant
+- [x] Debug mode with timing metrics for cache operations
+- [x] Transparent PTY passthrough
+- [x] Support for multiple CLI assistants
+
## Future Enhancements
-- [ ] Cache checking before forwarding to assistant
-- [ ] Request/response logging
-- [ ] Performance metrics
-- [ ] Cache hit/miss statistics
+- [ ] Request/response logging to file
+- [ ] Cumulative cache hit/miss statistics
- [ ] Support for intercepting and modifying requests
+- [ ] Configuration file support
## Development
diff --git a/ts/packages/coderWrapper/package.json b/ts/packages/coderWrapper/package.json
index 11d9663a0..e6df5f5ba 100644
--- a/ts/packages/coderWrapper/package.json
+++ b/ts/packages/coderWrapper/package.json
@@ -28,6 +28,7 @@
"tsc": "tsc -b"
},
"dependencies": {
+ "@modelcontextprotocol/sdk": "^1.0.4",
"@typeagent/agent-server-client": "workspace:*",
"@typeagent/dispatcher-types": "workspace:*",
"node-pty": "^1.0.0"
diff --git a/ts/packages/coderWrapper/src/cacheClient.ts b/ts/packages/coderWrapper/src/cacheClient.ts
new file mode 100644
index 000000000..d7f5f3494
--- /dev/null
+++ b/ts/packages/coderWrapper/src/cacheClient.ts
@@ -0,0 +1,222 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
+import { DebugLogger } from "./debugLogger.js";
+import * as path from "path";
+import { fileURLToPath } from "url";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+/**
+ * Result of a cache check
+ */
+export interface CacheCheckResult {
+ hit: boolean;
+ result?: string;
+ error?: string;
+}
+
+/**
+ * Client for checking the TypeAgent cache via MCP server
+ */
+export class CacheClient {
+ private client: Client | null = null;
+ private transport: StdioClientTransport | null = null;
+ private logger: DebugLogger | null = null;
+
+ constructor(_mcpServerPath?: string, logger?: DebugLogger) {
+ this.logger = logger || null;
+ if (this.logger) {
+ this.logger.log("CacheClient initialized");
+ }
+ }
+
+ /**
+ * Connect to the MCP server
+ */
+ async connect(): Promise {
+ if (this.client) {
+ if (this.logger) {
+ this.logger.log("Already connected to MCP server");
+ }
+ return; // Already connected
+ }
+
+ try {
+ // Resolve MCP server path relative to coderWrapper package
+ // __dirname points to packages/coderWrapper/dist
+ // We need to go up to ts root: ../../commandExecutor/dist/server.js
+ const mcpServerPath = path.resolve(
+ __dirname,
+ "..",
+ "..",
+ "commandExecutor",
+ "dist",
+ "server.js",
+ );
+
+ if (this.logger) {
+ this.logger.log(
+ `Attempting to connect to MCP server at ${mcpServerPath}`,
+ );
+ }
+
+ // Create transport and client
+ this.transport = new StdioClientTransport({
+ command: "node",
+ args: [mcpServerPath],
+ });
+
+ if (this.logger) {
+ this.logger.log("StdioClientTransport created");
+ }
+
+ this.client = new Client(
+ {
+ name: "coder-wrapper-cache-client",
+ version: "0.0.1",
+ },
+ {
+ capabilities: {},
+ },
+ );
+
+ if (this.logger) {
+ this.logger.log("MCP Client created, connecting...");
+ }
+
+ await this.client.connect(this.transport);
+
+ if (this.logger) {
+ this.logger.log("Successfully connected to MCP server");
+ }
+ } catch (error) {
+ if (this.logger) {
+ this.logger.error("Failed to connect to MCP server", error);
+ }
+ throw error;
+ }
+ }
+
+ /**
+ * Check if a request is in the cache and execute it if found
+ */
+ async checkCache(request: string): Promise {
+ if (this.logger) {
+ this.logger.log(`checkCache called for request: "${request}"`);
+ }
+
+ if (!this.client) {
+ if (this.logger) {
+ this.logger.log("Client not connected, attempting to connect...");
+ }
+ try {
+ await this.connect();
+ } catch (error) {
+ if (this.logger) {
+ this.logger.error("Connection failed during checkCache", error);
+ }
+ return {
+ hit: false,
+ error: `Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`,
+ };
+ }
+ }
+
+ if (!this.client) {
+ if (this.logger) {
+ this.logger.error("Client is still null after connect attempt");
+ }
+ return {
+ hit: false,
+ error: "Failed to connect to MCP server",
+ };
+ }
+
+ try {
+ if (this.logger) {
+ this.logger.log("Calling MCP execute_command tool with cacheCheck=true");
+ }
+
+ const result = await this.client.callTool({
+ name: "execute_command",
+ arguments: {
+ request,
+ cacheCheck: true,
+ },
+ });
+
+ if (this.logger) {
+ this.logger.log(
+ `MCP tool call completed, result: ${JSON.stringify(result, null, 2)}`,
+ );
+ }
+
+ // Parse the result
+ if (
+ result.content &&
+ Array.isArray(result.content) &&
+ result.content.length > 0
+ ) {
+ const content = result.content[0];
+ if (content.type === "text") {
+ const text = content.text;
+
+ if (text.startsWith("CACHE_HIT:")) {
+ if (this.logger) {
+ this.logger.log("Cache HIT detected");
+ }
+ return {
+ hit: true,
+ result: text.substring("CACHE_HIT:".length).trim(),
+ };
+ } else if (text.startsWith("CACHE_MISS:")) {
+ const missReason = text
+ .substring("CACHE_MISS:".length)
+ .trim();
+ if (this.logger) {
+ this.logger.log(`Cache MISS: ${missReason}`);
+ }
+ return {
+ hit: false,
+ error: missReason,
+ };
+ }
+ }
+ }
+
+ if (this.logger) {
+ this.logger.error("Unexpected response format from MCP server");
+ }
+ return {
+ hit: false,
+ error: "Unexpected response format",
+ };
+ } catch (error) {
+ if (this.logger) {
+ this.logger.error("Cache check error", error);
+ }
+ return {
+ hit: false,
+ error: `Cache check error: ${error instanceof Error ? error.message : String(error)}`,
+ };
+ }
+ }
+
+ /**
+ * Close the connection to the MCP server
+ */
+ async close(): Promise {
+ if (this.client) {
+ await this.client.close();
+ this.client = null;
+ }
+ if (this.transport) {
+ await this.transport.close();
+ this.transport = null;
+ }
+ }
+}
diff --git a/ts/packages/coderWrapper/src/cli.ts b/ts/packages/coderWrapper/src/cli.ts
index b2933e49c..7094cd5c4 100644
--- a/ts/packages/coderWrapper/src/cli.ts
+++ b/ts/packages/coderWrapper/src/cli.ts
@@ -8,10 +8,11 @@ import { getAssistantConfig } from "./assistantConfig.js";
/**
* Parse command line arguments
*/
-function parseArgs(): { assistant: string; help: boolean } {
+function parseArgs(): { assistant: string; help: boolean; debug: boolean } {
const args = process.argv.slice(2);
let assistant = "claude";
let help = false;
+ let debug = false;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
@@ -19,10 +20,12 @@ function parseArgs(): { assistant: string; help: boolean } {
help = true;
} else if (arg === "--assistant" || arg === "-a") {
assistant = args[++i];
+ } else if (arg === "--debug" || arg === "-d") {
+ debug = true;
}
}
- return { assistant, help };
+ return { assistant, help, debug };
}
/**
@@ -34,6 +37,7 @@ Usage: coder-wrapper [options]
Options:
-a, --assistant Specify the assistant to use (default: claude)
+ -d, --debug Enable debug logging with cache timing information
-h, --help Show this help message
Available assistants:
@@ -42,11 +46,12 @@ Available assistants:
Examples:
coder-wrapper # Use Claude Code (default)
coder-wrapper -a claude # Explicitly use Claude Code
+ coder-wrapper --debug # Enable debug logging
Description:
Wraps CLI coding assistants in a pseudo terminal with caching support.
- The wrapper transparently passes through all I/O to/from the assistant.
- Future versions will add TypeAgent cache checking before forwarding requests.
+ The wrapper checks the TypeAgent cache before forwarding requests to the assistant.
+ Cache hits are executed and returned immediately without calling the assistant.
`);
}
@@ -54,7 +59,7 @@ Description:
* Main CLI entry point
*/
async function main() {
- const { assistant, help } = parseArgs();
+ const { assistant, help, debug } = parseArgs();
if (help) {
printUsage();
@@ -69,6 +74,9 @@ async function main() {
console.log(
`[CoderWrapper] Command: ${config.command} ${config.args.join(" ")}`,
);
+ if (debug) {
+ console.log(`[CoderWrapper] Debug mode enabled - cache timing will be logged`);
+ }
console.log(
`[CoderWrapper] Press Ctrl+C to exit or type 'exit' in the assistant\n`,
);
@@ -77,6 +85,7 @@ async function main() {
const wrapper = new PtyWrapper(config, {
cols: process.stdout.columns,
rows: process.stdout.rows,
+ debug,
});
wrapper.spawn();
diff --git a/ts/packages/coderWrapper/src/debugLogger.ts b/ts/packages/coderWrapper/src/debugLogger.ts
new file mode 100644
index 000000000..b027db485
--- /dev/null
+++ b/ts/packages/coderWrapper/src/debugLogger.ts
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import * as fs from "fs";
+import * as path from "path";
+import * as os from "os";
+
+/**
+ * Debug logger that writes to a file
+ */
+export class DebugLogger {
+ private logFilePath: string = "";
+ private logStream: fs.WriteStream | null = null;
+ private enabled: boolean;
+
+ constructor(enabled: boolean = false) {
+ this.enabled = enabled;
+
+ if (enabled) {
+ // Use ~/.tmp instead of system temp directory
+ const logDir = path.join(os.homedir(), ".tmp", "typeagent-coder-wrapper");
+ if (!fs.existsSync(logDir)) {
+ fs.mkdirSync(logDir, { recursive: true });
+ }
+ this.logFilePath = path.join(
+ logDir,
+ `coder-wrapper-${Date.now()}.log`,
+ );
+ this.logStream = fs.createWriteStream(this.logFilePath, {
+ flags: "a",
+ });
+ this.log(`Debug log started at: ${new Date().toISOString()}`);
+ this.log(`Log file: ${this.logFilePath}`);
+ console.log(`[CoderWrapper] Debug log: ${this.logFilePath}`);
+ }
+ }
+
+ private formatMessage(message: string): string {
+ const timestamp = new Date().toISOString();
+ return `[${timestamp}] ${message}`;
+ }
+
+ log(message: string): void {
+ if (!this.enabled || !this.logStream) {
+ return;
+ }
+ this.logStream.write(this.formatMessage(message) + "\n");
+ }
+
+ error(message: string, error?: any): void {
+ if (!this.enabled || !this.logStream) {
+ return;
+ }
+ const errorDetails = error
+ ? ` - ${error instanceof Error ? error.message : String(error)}`
+ : "";
+ this.logStream.write(
+ this.formatMessage(`ERROR: ${message}${errorDetails}`) + "\n",
+ );
+ if (error?.stack) {
+ this.logStream.write(error.stack + "\n");
+ }
+ }
+
+ getLogFilePath(): string {
+ return this.logFilePath;
+ }
+
+ close(): void {
+ if (this.logStream) {
+ this.log(`Debug log ended at: ${new Date().toISOString()}`);
+ this.logStream.end();
+ }
+ }
+}
diff --git a/ts/packages/coderWrapper/src/index.ts b/ts/packages/coderWrapper/src/index.ts
index cfe705c9b..693ff7d73 100644
--- a/ts/packages/coderWrapper/src/index.ts
+++ b/ts/packages/coderWrapper/src/index.ts
@@ -7,3 +7,5 @@ export {
ASSISTANT_CONFIGS,
getAssistantConfig,
} from "./assistantConfig.js";
+export { CacheClient, CacheCheckResult } from "./cacheClient.js";
+export { DebugLogger } from "./debugLogger.js";
diff --git a/ts/packages/coderWrapper/src/ptyWrapper.ts b/ts/packages/coderWrapper/src/ptyWrapper.ts
index 0f7c4e013..2046a3873 100644
--- a/ts/packages/coderWrapper/src/ptyWrapper.ts
+++ b/ts/packages/coderWrapper/src/ptyWrapper.ts
@@ -3,6 +3,8 @@
import * as pty from "node-pty";
import { AssistantConfig } from "./assistantConfig.js";
+import { CacheClient } from "./cacheClient.js";
+import { DebugLogger } from "./debugLogger.js";
/**
* Options for the PTY wrapper
@@ -12,6 +14,8 @@ export interface PtyWrapperOptions {
rows?: number;
cwd?: string;
env?: Record;
+ enableCache?: boolean;
+ debug?: boolean;
}
/**
@@ -21,6 +25,11 @@ export class PtyWrapper {
private ptyProcess: pty.IPty | null = null;
private readonly config: AssistantConfig;
private readonly options: PtyWrapperOptions;
+ private cacheClient: CacheClient | null = null;
+ private inputBuffer: string = "";
+ private debugLogger: DebugLogger | null = null;
+ private processingCommand: boolean = false;
+ private lastInputWasCarriageReturn: boolean = false;
constructor(config: AssistantConfig, options: PtyWrapperOptions = {}) {
this.config = config;
@@ -36,7 +45,23 @@ export class PtyWrapper {
rows: options.rows || 30,
cwd: options.cwd || process.cwd(),
env: { ...cleanEnv, ...config.env, ...options.env },
+ enableCache: options.enableCache ?? false,
+ debug: options.debug ?? false,
};
+
+ // Initialize debug logger if enabled
+ if (this.options.debug) {
+ this.debugLogger = new DebugLogger(true);
+ this.debugLogger.log(`PtyWrapper initialized for ${config.name}`);
+ }
+
+ // Initialize cache client if enabled
+ if (this.options.enableCache) {
+ this.cacheClient = new CacheClient(undefined, this.debugLogger || undefined);
+ if (this.debugLogger) {
+ this.debugLogger.log("Cache client initialized");
+ }
+ }
}
/**
@@ -80,9 +105,53 @@ export class PtyWrapper {
// Handle stdin from the user
process.stdin.setRawMode(true);
- process.stdin.on("data", (data: Buffer) => {
- if (this.ptyProcess) {
- this.ptyProcess.write(data.toString());
+ process.stdin.on("data", async (data: Buffer) => {
+ if (!this.ptyProcess) {
+ return;
+ }
+
+ const input = data.toString();
+
+ if (this.debugLogger) {
+ this.debugLogger.log(`stdin data received: ${JSON.stringify(input)} (length: ${input.length})`);
+ }
+
+ // If the last input was \r and this input is \n, skip it (Windows sends both separately)
+ if (this.lastInputWasCarriageReturn && input === "\n") {
+ if (this.debugLogger) {
+ this.debugLogger.log("Skipping \\n that follows \\r");
+ }
+ this.lastInputWasCarriageReturn = false;
+ return;
+ }
+
+ // Check for Enter key (carriage return, newline, or both)
+ if (input === "\r" || input === "\n" || input === "\r\n") {
+ // Track if this was a \r so we can skip the following \n
+ this.lastInputWasCarriageReturn = (input === "\r");
+
+ // User pressed Enter - check if we should check cache
+ if (this.inputBuffer.trim() && this.cacheClient) {
+ await this.handleCommand(this.inputBuffer.trim());
+ this.inputBuffer = "";
+ } else {
+ // No cache or empty input, pass through
+ this.ptyProcess.write(input);
+ this.inputBuffer = "";
+ }
+ } else if (input.includes("\r") || input.includes("\n")) {
+ // Input contains newline but with other characters - pass through
+ if (this.debugLogger) {
+ this.debugLogger.log(`Mixed input with newline detected, passing through: ${JSON.stringify(input)}`);
+ }
+ this.ptyProcess.write(input);
+ this.inputBuffer = "";
+ this.lastInputWasCarriageReturn = false;
+ } else {
+ // Buffer the input and pass it through to PTY for echo
+ this.inputBuffer += input;
+ this.ptyProcess.write(input);
+ this.lastInputWasCarriageReturn = false;
}
});
@@ -116,15 +185,136 @@ export class PtyWrapper {
this.ptyProcess.resize(cols, rows);
}
+ /**
+ * Handle a command - check cache first, then forward to assistant if needed
+ */
+ private async handleCommand(command: string): Promise {
+ // Prevent concurrent execution - skip if already processing
+ if (this.processingCommand) {
+ if (this.debugLogger) {
+ this.debugLogger.log(`Skipping duplicate command (already processing): "${command}"`);
+ }
+ return;
+ }
+
+ this.processingCommand = true;
+ try {
+ await this.handleCommandInternal(command);
+ } finally {
+ this.processingCommand = false;
+ }
+ }
+
+ private async handleCommandInternal(command: string): Promise {
+ if (!this.cacheClient || !this.ptyProcess) {
+ // No cache client or process, just forward
+ if (this.debugLogger) {
+ this.debugLogger.log(
+ "No cache client or pty process, forwarding to assistant",
+ );
+ }
+ this.ptyProcess?.write("\r");
+ return;
+ }
+
+ // Immediately provide feedback by echoing the command
+ // This happens BEFORE the cache check so user sees response immediately
+ const terminalWidth = process.stdout.columns || 80;
+ const grayColor = "\x1b[90m"; // ANSI gray color
+ const resetColor = "\x1b[0m"; // Reset to default color
+ const separator = grayColor + "─".repeat(terminalWidth) + resetColor;
+
+ // Clear current input line and echo the command with prompt prefix (no separator after)
+ process.stdout.write("\r\x1b[K");
+ process.stdout.write(`> ${command}\n`);
+
+ const startTime = performance.now();
+
+ if (this.debugLogger) {
+ this.debugLogger.log(`Handling command: "${command}"`);
+ }
+
+ try {
+ const cacheResult = await this.cacheClient.checkCache(command);
+ const elapsedMs = performance.now() - startTime;
+
+ if (cacheResult.hit && cacheResult.result) {
+ // Cache hit! The command was already echoed above, now print the output
+
+ if (this.debugLogger) {
+ this.debugLogger.log(
+ `✓ Cache HIT (${elapsedMs.toFixed(2)}ms) - printing result to terminal`,
+ );
+ // Print timing indicator on its own line BEFORE any separator
+ process.stdout.write(`(${Math.round(elapsedMs)}ms)\n`);
+ }
+
+ // Print the result (might be empty for some commands)
+ if (cacheResult.result.trim()) {
+ process.stdout.write(cacheResult.result + "\n");
+ }
+
+ // Print separator line before prompt (gray)
+ process.stdout.write(separator + "\n");
+
+ // Print prompt and immediately save cursor position
+ process.stdout.write("> ");
+ process.stdout.write("\x1b7"); // Save cursor position (after prompt)
+
+ // Print separator line after prompt (gray) on next line
+ process.stdout.write("\n" + separator);
+
+ // Restore cursor to saved position (after the prompt)
+ process.stdout.write("\x1b8"); // Restore cursor position
+ } else {
+ // Cache miss - forward to assistant
+ if (this.debugLogger) {
+ this.debugLogger.log(
+ `✗ Cache MISS (${elapsedMs.toFixed(2)}ms): ${cacheResult.error}`,
+ );
+ this.debugLogger.log("Forwarding to assistant");
+ }
+
+ // Forward the command normally
+ this.ptyProcess.write("\r");
+ }
+ } catch (error) {
+ const elapsedMs = performance.now() - startTime;
+ // Error checking cache - fall back to forwarding
+ if (this.debugLogger) {
+ this.debugLogger.error(
+ `Cache check error (${elapsedMs.toFixed(2)}ms)`,
+ error,
+ );
+ this.debugLogger.log("Forwarding to assistant after error");
+ }
+ this.ptyProcess.write("\r");
+ }
+ }
+
/**
* Kill the assistant process
*/
kill(signal?: string): void {
+ if (this.debugLogger) {
+ this.debugLogger.log("Kill requested");
+ }
+
if (this.ptyProcess) {
console.log(`\n[CoderWrapper] Killing ${this.config.name}...`);
this.ptyProcess.kill(signal);
this.ptyProcess = null;
}
+ // Close cache client
+ if (this.cacheClient) {
+ this.cacheClient.close().catch(console.error);
+ this.cacheClient = null;
+ }
+ // Close debug logger
+ if (this.debugLogger) {
+ this.debugLogger.close();
+ this.debugLogger = null;
+ }
}
/**
diff --git a/ts/packages/commandExecutor/src/commandServer.ts b/ts/packages/commandExecutor/src/commandServer.ts
index be2966bdf..d78ae8a03 100644
--- a/ts/packages/commandExecutor/src/commandServer.ts
+++ b/ts/packages/commandExecutor/src/commandServer.ts
@@ -21,6 +21,7 @@ import * as os from "os";
function executeCommandRequestSchema() {
return {
request: z.string(),
+ cacheCheck: z.boolean().optional(),
};
}
const ExecuteCommandRequestSchema = z.object(executeCommandRequestSchema());
@@ -41,7 +42,8 @@ class Logger {
private logStream: fs.WriteStream;
constructor() {
- const logDir = path.join(os.tmpdir(), "typeagent-mcp");
+ // Use ~/.tmp instead of system temp directory
+ const logDir = path.join(os.homedir(), ".tmp", "typeagent-mcp");
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
@@ -377,11 +379,60 @@ export class CommandServer {
}
if (!this.dispatcher) {
+ // During cache check, return cache miss instead of error to avoid startup race condition messages
+ if (request.cacheCheck) {
+ this.logger.log("Cache check requested but not connected yet - returning cache miss");
+ return toolResult("CACHE_MISS: Not connected to TypeAgent dispatcher yet");
+ }
const errorMsg = `Cannot execute command: not connected to TypeAgent dispatcher at ${this.agentServerUrl}. Make sure the TypeAgent server is running with: pnpm run start:agent-server`;
this.logger.error(errorMsg);
return toolResult(errorMsg);
}
+ // If cacheCheck is requested, check cache and execute if hit
+ if (request.cacheCheck) {
+ try {
+ this.logger.log(`Cache check requested for: ${request.request}`);
+
+ // Clear response collector before cache check
+ this.responseCollector.messages = [];
+
+ const cacheResult = await this.dispatcher.checkCache(
+ request.request,
+ );
+
+ if (cacheResult?.lastError) {
+ // Cache miss or error
+ this.logger.log(`Cache miss: ${cacheResult.lastError}`);
+ return toolResult(`CACHE_MISS: ${cacheResult.lastError}`);
+ }
+
+ // Cache hit - actions were executed, return the collected messages
+ this.logger.log(`Cache hit - executed successfully`);
+
+ if (this.responseCollector.messages.length > 0) {
+ const response = this.responseCollector.messages.join("\n\n");
+ const processedResponse = await processHtmlImages(response);
+ // Return with CACHE_HIT prefix for detection (cacheClient strips it)
+ return toolResult(`CACHE_HIT: ${processedResponse}`);
+ }
+
+ // Fallback if no messages were collected
+ return toolResult(`CACHE_HIT: Successfully executed from cache`);
+ } catch (error) {
+ const errorMsg = `Cache check failed: ${error instanceof Error ? error.message : String(error)}`;
+ this.logger.error(errorMsg);
+
+ // If the error is "Agent channel disconnected", reset the dispatcher to trigger reconnection
+ if (error instanceof Error && error.message.includes("Agent channel disconnected")) {
+ this.logger.log("Dispatcher connection lost, will reconnect on next request");
+ this.dispatcher = null;
+ }
+
+ return toolResult(`CACHE_MISS: ${errorMsg}`);
+ }
+ }
+
try {
// Clear response collector before processing new command
this.responseCollector.messages = [];
diff --git a/ts/packages/dispatcher/dispatcher/src/dispatcher.ts b/ts/packages/dispatcher/dispatcher/src/dispatcher.ts
index 561b76681..704fcc483 100644
--- a/ts/packages/dispatcher/dispatcher/src/dispatcher.ts
+++ b/ts/packages/dispatcher/dispatcher/src/dispatcher.ts
@@ -68,6 +68,49 @@ async function getTemplateCompletion(
);
}
+async function checkCache(
+ request: string,
+ context: CommandHandlerContext,
+): Promise {
+ const agentCache = context.agentCache;
+
+ // Check if cache is enabled
+ if (!agentCache.isEnabled()) {
+ return {
+ lastError: "Cache is not enabled",
+ };
+ }
+
+ // Get active schema names from enabled agents
+ const activeSchemaNames = context.agents.getActiveSchemas();
+
+ if (activeSchemaNames.length === 0) {
+ return {
+ lastError: "No active agents",
+ };
+ }
+
+ // Attempt to match the request against the cache
+ const matches = agentCache.match(request, {
+ wildcard: context.session.getConfig().cache.matchWildcard,
+ entityWildcard: context.session.getConfig().cache.matchEntityWildcard,
+ rejectReferences: context.session
+ .getConfig()
+ .explainer.filter.reference.list,
+ namespaceKeys: agentCache.getNamespaceKeys(activeSchemaNames, undefined),
+ });
+
+ if (matches.length === 0) {
+ return {
+ lastError: "No cache match found",
+ };
+ }
+
+ // Cache hit - execute the command normally to get the full result
+ // This will execute the cached actions and return proper results through ClientIO
+ return await processCommand(request, context);
+}
+
/**
* Create a instance of the dispatcher.
*
@@ -87,6 +130,9 @@ export async function createDispatcher(
getCommandCompletion(prefix) {
return getCommandCompletion(prefix, context);
},
+ checkCache(request) {
+ return checkCache(request, context);
+ },
getDynamicDisplay(appAgentName, type, id) {
return getDynamicDisplay(context, appAgentName, type, id);
diff --git a/ts/packages/dispatcher/rpc/src/dispatcherClient.ts b/ts/packages/dispatcher/rpc/src/dispatcherClient.ts
index 9bacb608d..78753a67b 100644
--- a/ts/packages/dispatcher/rpc/src/dispatcherClient.ts
+++ b/ts/packages/dispatcher/rpc/src/dispatcherClient.ts
@@ -25,6 +25,9 @@ export function createDispatcherRpcClient(channel: RpcChannel): Dispatcher {
async getCommandCompletion(...args) {
return rpc.invoke("getCommandCompletion", ...args);
},
+ async checkCache(...args) {
+ return rpc.invoke("checkCache", ...args);
+ },
async close() {
return rpc.invoke("close");
},
diff --git a/ts/packages/dispatcher/rpc/src/dispatcherServer.ts b/ts/packages/dispatcher/rpc/src/dispatcherServer.ts
index 51236f3e2..e4c8de071 100644
--- a/ts/packages/dispatcher/rpc/src/dispatcherServer.ts
+++ b/ts/packages/dispatcher/rpc/src/dispatcherServer.ts
@@ -26,6 +26,9 @@ export function createDispatcherRpcServer(
getCommandCompletion: async (...args) => {
return dispatcher.getCommandCompletion(...args);
},
+ checkCache: async (...args) => {
+ return dispatcher.checkCache(...args);
+ },
close: async () => {
await dispatcher.close();
},
diff --git a/ts/packages/dispatcher/rpc/src/dispatcherTypes.ts b/ts/packages/dispatcher/rpc/src/dispatcherTypes.ts
index 0a253487b..15ed68071 100644
--- a/ts/packages/dispatcher/rpc/src/dispatcherTypes.ts
+++ b/ts/packages/dispatcher/rpc/src/dispatcherTypes.ts
@@ -41,6 +41,8 @@ export type DispatcherInvokeFunctions = {
prefix: string,
): Promise;
+ checkCache(request: string): Promise;
+
close(): Promise;
getStatus(): Promise;
diff --git a/ts/packages/dispatcher/types/src/dispatcher.ts b/ts/packages/dispatcher/types/src/dispatcher.ts
index f63a2989c..3c8d2838c 100644
--- a/ts/packages/dispatcher/types/src/dispatcher.ts
+++ b/ts/packages/dispatcher/types/src/dispatcher.ts
@@ -120,5 +120,8 @@ export interface Dispatcher {
prefix: string,
): Promise;
+ // Check if a request can be handled by cache without executing
+ checkCache(request: string): Promise;
+
getStatus(): Promise;
}
diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml
index fe089cfb3..c7e8306ba 100644
--- a/ts/pnpm-lock.yaml
+++ b/ts/pnpm-lock.yaml
@@ -2815,6 +2815,9 @@ importers:
packages/coderWrapper:
dependencies:
+ '@modelcontextprotocol/sdk':
+ specifier: ^1.0.4
+ version: 1.25.2(hono@4.11.3)(zod@4.1.13)
'@typeagent/agent-server-client':
specifier: workspace:*
version: link:../agentServer/client
From b0010a9f9b9712ae98a09a65ac50b398525a5412 Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 18:02:55 -0800
Subject: [PATCH 16/21] Fix prettier formatting
Co-Authored-By: Claude Sonnet 4.5
---
ts/packages/agentServer/server/src/server.ts | 39 ++++++++++++++-----
ts/packages/coderWrapper/README.md | 2 +
ts/packages/coderWrapper/src/cacheClient.ts | 13 +++++--
ts/packages/coderWrapper/src/cli.ts | 4 +-
ts/packages/coderWrapper/src/debugLogger.ts | 6 ++-
ts/packages/coderWrapper/src/ptyWrapper.ts | 19 ++++++---
.../commandExecutor/src/commandServer.ts | 28 +++++++++----
.../dispatcher/dispatcher/src/dispatcher.ts | 10 +++--
8 files changed, 91 insertions(+), 30 deletions(-)
diff --git a/ts/packages/agentServer/server/src/server.ts b/ts/packages/agentServer/server/src/server.ts
index 22c9302ba..3e979dcee 100644
--- a/ts/packages/agentServer/server/src/server.ts
+++ b/ts/packages/agentServer/server/src/server.ts
@@ -30,7 +30,10 @@ async function main() {
const instanceDir = getInstanceDir();
// Track all connected clients and their ClientIO
- const connectedClients = new Map void }>();
+ const connectedClients = new Map<
+ ChannelProvider,
+ { clientIO: ClientIO; closeFn: () => void }
+ >();
// Create a routing ClientIO that forwards calls to the current request's client
const routingClientIO: ClientIO = {
@@ -145,26 +148,44 @@ async function main() {
channelProvider.on("disconnect", () => {
connectedClients.delete(channelProvider);
- console.log(`Client disconnected. Active connections: ${connectedClients.size}`);
+ console.log(
+ `Client disconnected. Active connections: ${connectedClients.size}`,
+ );
});
// Wrap the dispatcher RPC server to set context for each request
const wrappedDispatcher = {
...dispatcher,
- processCommand: async (command: string, requestId?: string, attachments?: string[]) => {
- return currentClientContext.run(clientIORpcClient, () =>
- dispatcher.processCommand(command, requestId, attachments)
+ processCommand: async (
+ command: string,
+ requestId?: string,
+ attachments?: string[],
+ ) => {
+ return currentClientContext.run(
+ clientIORpcClient,
+ () =>
+ dispatcher.processCommand(
+ command,
+ requestId,
+ attachments,
+ ),
);
},
checkCache: async (request: string) => {
- return currentClientContext.run(clientIORpcClient, () =>
- dispatcher.checkCache(request)
+ return currentClientContext.run(
+ clientIORpcClient,
+ () => dispatcher.checkCache(request),
);
},
};
- createDispatcherRpcServer(wrappedDispatcher as any, dispatcherChannel);
- console.log(`Client connected. Active connections: ${connectedClients.size}`);
+ createDispatcherRpcServer(
+ wrappedDispatcher as any,
+ dispatcherChannel,
+ );
+ console.log(
+ `Client connected. Active connections: ${connectedClients.size}`,
+ );
},
};
diff --git a/ts/packages/coderWrapper/README.md b/ts/packages/coderWrapper/README.md
index 2a0e37233..55d64530c 100644
--- a/ts/packages/coderWrapper/README.md
+++ b/ts/packages/coderWrapper/README.md
@@ -68,12 +68,14 @@ coder-wrapper --debug
### Debug Mode
When `--debug` is enabled, the wrapper logs:
+
- Cache check attempts with command text
- Cache hit/miss status with timing (in milliseconds)
- Whether request was forwarded to assistant
- Total time for cache hits
Example debug output:
+
```
[CoderWrapper:Debug] Checking cache for: "play hello by adele"
[CoderWrapper:Debug] ✓ Cache HIT (234.56ms)
diff --git a/ts/packages/coderWrapper/src/cacheClient.ts b/ts/packages/coderWrapper/src/cacheClient.ts
index d7f5f3494..d0984bc7f 100644
--- a/ts/packages/coderWrapper/src/cacheClient.ts
+++ b/ts/packages/coderWrapper/src/cacheClient.ts
@@ -111,13 +111,18 @@ export class CacheClient {
if (!this.client) {
if (this.logger) {
- this.logger.log("Client not connected, attempting to connect...");
+ this.logger.log(
+ "Client not connected, attempting to connect...",
+ );
}
try {
await this.connect();
} catch (error) {
if (this.logger) {
- this.logger.error("Connection failed during checkCache", error);
+ this.logger.error(
+ "Connection failed during checkCache",
+ error,
+ );
}
return {
hit: false,
@@ -138,7 +143,9 @@ export class CacheClient {
try {
if (this.logger) {
- this.logger.log("Calling MCP execute_command tool with cacheCheck=true");
+ this.logger.log(
+ "Calling MCP execute_command tool with cacheCheck=true",
+ );
}
const result = await this.client.callTool({
diff --git a/ts/packages/coderWrapper/src/cli.ts b/ts/packages/coderWrapper/src/cli.ts
index 7094cd5c4..720dd4181 100644
--- a/ts/packages/coderWrapper/src/cli.ts
+++ b/ts/packages/coderWrapper/src/cli.ts
@@ -75,7 +75,9 @@ async function main() {
`[CoderWrapper] Command: ${config.command} ${config.args.join(" ")}`,
);
if (debug) {
- console.log(`[CoderWrapper] Debug mode enabled - cache timing will be logged`);
+ console.log(
+ `[CoderWrapper] Debug mode enabled - cache timing will be logged`,
+ );
}
console.log(
`[CoderWrapper] Press Ctrl+C to exit or type 'exit' in the assistant\n`,
diff --git a/ts/packages/coderWrapper/src/debugLogger.ts b/ts/packages/coderWrapper/src/debugLogger.ts
index b027db485..0cbc31307 100644
--- a/ts/packages/coderWrapper/src/debugLogger.ts
+++ b/ts/packages/coderWrapper/src/debugLogger.ts
@@ -18,7 +18,11 @@ export class DebugLogger {
if (enabled) {
// Use ~/.tmp instead of system temp directory
- const logDir = path.join(os.homedir(), ".tmp", "typeagent-coder-wrapper");
+ const logDir = path.join(
+ os.homedir(),
+ ".tmp",
+ "typeagent-coder-wrapper",
+ );
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
diff --git a/ts/packages/coderWrapper/src/ptyWrapper.ts b/ts/packages/coderWrapper/src/ptyWrapper.ts
index 2046a3873..7b8709c84 100644
--- a/ts/packages/coderWrapper/src/ptyWrapper.ts
+++ b/ts/packages/coderWrapper/src/ptyWrapper.ts
@@ -57,7 +57,10 @@ export class PtyWrapper {
// Initialize cache client if enabled
if (this.options.enableCache) {
- this.cacheClient = new CacheClient(undefined, this.debugLogger || undefined);
+ this.cacheClient = new CacheClient(
+ undefined,
+ this.debugLogger || undefined,
+ );
if (this.debugLogger) {
this.debugLogger.log("Cache client initialized");
}
@@ -113,7 +116,9 @@ export class PtyWrapper {
const input = data.toString();
if (this.debugLogger) {
- this.debugLogger.log(`stdin data received: ${JSON.stringify(input)} (length: ${input.length})`);
+ this.debugLogger.log(
+ `stdin data received: ${JSON.stringify(input)} (length: ${input.length})`,
+ );
}
// If the last input was \r and this input is \n, skip it (Windows sends both separately)
@@ -128,7 +133,7 @@ export class PtyWrapper {
// Check for Enter key (carriage return, newline, or both)
if (input === "\r" || input === "\n" || input === "\r\n") {
// Track if this was a \r so we can skip the following \n
- this.lastInputWasCarriageReturn = (input === "\r");
+ this.lastInputWasCarriageReturn = input === "\r";
// User pressed Enter - check if we should check cache
if (this.inputBuffer.trim() && this.cacheClient) {
@@ -142,7 +147,9 @@ export class PtyWrapper {
} else if (input.includes("\r") || input.includes("\n")) {
// Input contains newline but with other characters - pass through
if (this.debugLogger) {
- this.debugLogger.log(`Mixed input with newline detected, passing through: ${JSON.stringify(input)}`);
+ this.debugLogger.log(
+ `Mixed input with newline detected, passing through: ${JSON.stringify(input)}`,
+ );
}
this.ptyProcess.write(input);
this.inputBuffer = "";
@@ -192,7 +199,9 @@ export class PtyWrapper {
// Prevent concurrent execution - skip if already processing
if (this.processingCommand) {
if (this.debugLogger) {
- this.debugLogger.log(`Skipping duplicate command (already processing): "${command}"`);
+ this.debugLogger.log(
+ `Skipping duplicate command (already processing): "${command}"`,
+ );
}
return;
}
diff --git a/ts/packages/commandExecutor/src/commandServer.ts b/ts/packages/commandExecutor/src/commandServer.ts
index d78ae8a03..97bcd8904 100644
--- a/ts/packages/commandExecutor/src/commandServer.ts
+++ b/ts/packages/commandExecutor/src/commandServer.ts
@@ -381,8 +381,12 @@ export class CommandServer {
if (!this.dispatcher) {
// During cache check, return cache miss instead of error to avoid startup race condition messages
if (request.cacheCheck) {
- this.logger.log("Cache check requested but not connected yet - returning cache miss");
- return toolResult("CACHE_MISS: Not connected to TypeAgent dispatcher yet");
+ this.logger.log(
+ "Cache check requested but not connected yet - returning cache miss",
+ );
+ return toolResult(
+ "CACHE_MISS: Not connected to TypeAgent dispatcher yet",
+ );
}
const errorMsg = `Cannot execute command: not connected to TypeAgent dispatcher at ${this.agentServerUrl}. Make sure the TypeAgent server is running with: pnpm run start:agent-server`;
this.logger.error(errorMsg);
@@ -392,7 +396,9 @@ export class CommandServer {
// If cacheCheck is requested, check cache and execute if hit
if (request.cacheCheck) {
try {
- this.logger.log(`Cache check requested for: ${request.request}`);
+ this.logger.log(
+ `Cache check requested for: ${request.request}`,
+ );
// Clear response collector before cache check
this.responseCollector.messages = [];
@@ -411,21 +417,29 @@ export class CommandServer {
this.logger.log(`Cache hit - executed successfully`);
if (this.responseCollector.messages.length > 0) {
- const response = this.responseCollector.messages.join("\n\n");
+ const response =
+ this.responseCollector.messages.join("\n\n");
const processedResponse = await processHtmlImages(response);
// Return with CACHE_HIT prefix for detection (cacheClient strips it)
return toolResult(`CACHE_HIT: ${processedResponse}`);
}
// Fallback if no messages were collected
- return toolResult(`CACHE_HIT: Successfully executed from cache`);
+ return toolResult(
+ `CACHE_HIT: Successfully executed from cache`,
+ );
} catch (error) {
const errorMsg = `Cache check failed: ${error instanceof Error ? error.message : String(error)}`;
this.logger.error(errorMsg);
// If the error is "Agent channel disconnected", reset the dispatcher to trigger reconnection
- if (error instanceof Error && error.message.includes("Agent channel disconnected")) {
- this.logger.log("Dispatcher connection lost, will reconnect on next request");
+ if (
+ error instanceof Error &&
+ error.message.includes("Agent channel disconnected")
+ ) {
+ this.logger.log(
+ "Dispatcher connection lost, will reconnect on next request",
+ );
this.dispatcher = null;
}
diff --git a/ts/packages/dispatcher/dispatcher/src/dispatcher.ts b/ts/packages/dispatcher/dispatcher/src/dispatcher.ts
index 704fcc483..260f710a2 100644
--- a/ts/packages/dispatcher/dispatcher/src/dispatcher.ts
+++ b/ts/packages/dispatcher/dispatcher/src/dispatcher.ts
@@ -94,10 +94,12 @@ async function checkCache(
const matches = agentCache.match(request, {
wildcard: context.session.getConfig().cache.matchWildcard,
entityWildcard: context.session.getConfig().cache.matchEntityWildcard,
- rejectReferences: context.session
- .getConfig()
- .explainer.filter.reference.list,
- namespaceKeys: agentCache.getNamespaceKeys(activeSchemaNames, undefined),
+ rejectReferences:
+ context.session.getConfig().explainer.filter.reference.list,
+ namespaceKeys: agentCache.getNamespaceKeys(
+ activeSchemaNames,
+ undefined,
+ ),
});
if (matches.length === 0) {
From cba739e821d6032071c7fa4fbbbcae55a0ea6b9f Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 22:53:26 -0800
Subject: [PATCH 17/21] Add Agent SDK wrapper with TypeAgent caching and MCP
integration
This commit introduces a new agentSdkWrapper package that provides direct
integration with the Anthropic Agent SDK, offering an alternative to the
PTY-based coderWrapper approach with enhanced caching capabilities.
New Package: agentSdkWrapper
- Direct API integration using Agent SDK's query() function
- Programmatic control over request/response cycle
- Intelligent caching through TypeAgent's cache infrastructure
- Cache context injection via UserPromptSubmit hooks
- Extended thinking/reasoning display support
- MCP server integration for music, lists, calendar, and VSCode automation
- Configurable model selection (Sonnet/Opus) and tool restrictions
- Debug mode with detailed timing and logging
Key Features:
- Cache-first architecture: checks TypeAgent cache before API calls
- Context injection: follow-up questions can reference cached interactions
- Streaming support: displays responses and reasoning in real-time
- Session continuity: maintains conversation context across queries
- MCP command-executor integration with acceptEdits permission mode
Improvements to Existing Packages:
- commandExecutor: Added lifecycle documentation explaining transient/persistent
connection patterns, HTML-to-plaintext conversion for cache results
- agentServer: Enhanced error handling for disconnected clients, maintains
connection logging for debugging
Architecture Notes:
- Agent SDK spawns new Claude Code process per query (transient connections)
- Each process spawns fresh MCP server instance connecting to shared dispatcher
- agentServer maintains single persistent dispatcher across all connections
- Connection/disconnection messages retained for debugging purposes
Benefits over PTY wrapper:
- Better performance (no process spawning/PTY overhead)
- More control over API calls and tool configuration
- Easier testing (pure TypeScript functions)
- Cleaner code (no terminal escape codes)
- Programmatic access for TypeScript integration
Co-Authored-By: Claude Sonnet 4.5
---
ts/packages/agentSdkWrapper/.prettierignore | 4 +
ts/packages/agentSdkWrapper/README.md | 271 +++++++++
ts/packages/agentSdkWrapper/package.json | 41 ++
ts/packages/agentSdkWrapper/src/cli.ts | 535 ++++++++++++++++++
ts/packages/agentSdkWrapper/src/index.ts | 6 +
ts/packages/agentSdkWrapper/tsconfig.json | 8 +
ts/packages/agentServer/server/src/server.ts | 196 +++++--
.../commandExecutor/src/commandServer.ts | 68 ++-
ts/pnpm-lock.yaml | 52 +-
9 files changed, 1129 insertions(+), 52 deletions(-)
create mode 100644 ts/packages/agentSdkWrapper/.prettierignore
create mode 100644 ts/packages/agentSdkWrapper/README.md
create mode 100644 ts/packages/agentSdkWrapper/package.json
create mode 100644 ts/packages/agentSdkWrapper/src/cli.ts
create mode 100644 ts/packages/agentSdkWrapper/src/index.ts
create mode 100644 ts/packages/agentSdkWrapper/tsconfig.json
diff --git a/ts/packages/agentSdkWrapper/.prettierignore b/ts/packages/agentSdkWrapper/.prettierignore
new file mode 100644
index 000000000..48ada111f
--- /dev/null
+++ b/ts/packages/agentSdkWrapper/.prettierignore
@@ -0,0 +1,4 @@
+dist/
+node_modules/
+*.tsbuildinfo
+*.log
diff --git a/ts/packages/agentSdkWrapper/README.md b/ts/packages/agentSdkWrapper/README.md
new file mode 100644
index 000000000..1b978c2c9
--- /dev/null
+++ b/ts/packages/agentSdkWrapper/README.md
@@ -0,0 +1,271 @@
+# Agent SDK Wrapper
+
+Direct integration with the Anthropic Agent SDK with TypeAgent caching support.
+
+## Overview
+
+This package provides a CLI tool that uses the Anthropic Agent SDK (`@anthropic-ai/claude-agent-sdk`) directly, with intelligent caching through TypeAgent's cache infrastructure. It represents a different architectural approach compared to the PTY-based `coderWrapper`.
+
+## Architecture Differences
+
+### Agent SDK Wrapper (this package)
+
+- **Direct API Integration**: Uses the Agent SDK's `query()` function directly
+- **Programmatic Control**: Full control over the request/response cycle in TypeScript
+- **Streaming Support**: Can leverage the SDK's streaming capabilities
+- **Custom Tool Configuration**: Can specify which tools to enable per request
+- **Simpler I/O**: Standard readline interface for user input
+- **Cache-First**: Checks TypeAgent cache before making any API calls
+- **Lightweight**: No pseudo-terminal overhead, just API calls
+
+### PTY Wrapper (coderWrapper)
+
+- **Process Wrapping**: Spawns Claude Code CLI in a pseudo-terminal
+- **Transparent Passthrough**: Acts as a man-in-the-middle, intercepting I/O
+- **Terminal Emulation**: Provides full terminal experience with colors, formatting
+- **CLI-First**: Wraps existing CLI tools without modification
+- **Cache Injection**: Intercepts commands and injects cached responses
+- **Heavier**: Requires node-pty and terminal emulation
+
+## Benefits of Direct Agent SDK Usage
+
+1. **Better Performance**: No process spawning or PTY overhead
+2. **More Control**: Can customize every aspect of the API call
+3. **Easier Testing**: Pure TypeScript functions are easier to test
+4. **Programmatic Access**: Can be imported and used in other TypeScript code
+5. **Cleaner Code**: No terminal escape codes or PTY management
+6. **Tool Selection**: Can dynamically enable/disable specific tools
+7. **Streaming**: Can implement streaming responses efficiently
+
+## Installation
+
+From the TypeAgent repository root:
+
+```bash
+cd ts/packages/agentSdkWrapper
+npm install
+npm run build
+```
+
+## Usage
+
+### Basic Usage
+
+```bash
+# Start the interactive CLI with defaults (Sonnet, cache enabled)
+npm start
+
+# Or use the binary directly
+node dist/cli.js
+```
+
+### Command Line Options
+
+```bash
+# Use Claude Opus instead of Sonnet
+npm start -- -m opus
+
+# Use a specific model ID
+npm start -- -m claude-sonnet-4-5-20250929
+
+# Enable debug mode with timing information
+npm start -- --debug
+
+# Disable cache checking
+npm start -- --no-cache
+
+# Enable only specific tools
+npm start -- -t bash,read,write
+
+# Combine options
+npm start -- -m opus --debug -t bash,read
+```
+
+### Interactive Commands
+
+Once running, you can:
+
+- Type your prompts and press Enter
+- Type `exit`, `quit`, `.exit`, or `.quit` to quit
+- Press Ctrl+C to exit
+
+## How It Works
+
+1. **User Input**: Reads prompts via readline interface
+2. **Cache Check**: If cache is enabled, checks TypeAgent's cache first
+3. **Cache Hit**: Returns cached result immediately with timing info, and stores the interaction for context injection
+4. **Cache Miss**: Calls Agent SDK's `query()` function with configured options
+5. **Context Injection**: When a cache hit occurs, the next user message automatically includes context about the cached interaction via a `UserPromptSubmit` hook
+6. **Streaming**: Displays the response from Claude
+7. **Debug Mode**: Logs detailed timing and cache information
+
+### Cache Context Injection
+
+When a cache hit occurs, the cached request and result are stored temporarily. On the next user message, a `UserPromptSubmit` hook automatically injects this context into the conversation, allowing Claude to reference the cached interaction in follow-up questions.
+
+**Example:**
+
+```
+> get playlist brandenburg
+(45ms) # Cache hit
+────────────────────────────────────────────────────────────────────────────────
+[Playlist results...]
+────────────────────────────────────────────────────────────────────────────────
+
+> what's track 7 from that playlist?
+# Claude now has context about the previous cache hit and can answer
+```
+
+This solves the problem where cache hits were invisible to Claude's conversation history.
+
+## Cache Integration
+
+The wrapper reuses the `CacheClient` from the `coderWrapper` package, which:
+
+- Connects to TypeAgent's MCP server (`commandExecutor`)
+- Checks if a command has been executed before
+- Returns cached results instantly when available
+- Falls back to API calls on cache misses
+
+## Configuration
+
+### Model Selection
+
+- `sonnet`: Claude Sonnet 4.5 (default) - `claude-sonnet-4-5-20250929`
+- `opus`: Claude Opus 4.5 - `claude-opus-4-5-20251101`
+- Or provide any custom model ID
+
+### Tool Selection
+
+By default, all tools are enabled. You can restrict to specific tools:
+
+```bash
+# Enable only bash and file reading
+npm start -- -t bash,read
+
+# Enable only write operations
+npm start -- -t write
+```
+
+Available tools depend on the Agent SDK configuration.
+
+### MCP Server Integration
+
+The Agent SDK wrapper automatically configures the `command-executor` MCP server, which provides access to:
+
+- **Music & media control**: Play songs, control playback
+- **List management**: Shopping lists, todo lists
+- **Calendar operations**: Schedule events, view calendar
+- **VSCode automation**: Change themes, open files, create folders, run tasks, manage editor layout
+
+The MCP server is configured to use the TypeAgent dispatcher at `ws://localhost:8999`. Make sure the TypeAgent server is running:
+
+```bash
+# From the TypeAgent repository root
+pnpm run start:agent-server
+```
+
+The command-executor tool is available with permission mode set to `acceptEdits`, meaning Claude can execute commands without asking for permission each time.
+
+#### MCP Connection Lifecycle
+
+**When used with Agent SDK wrapper (this package):**
+
+- The Agent SDK spawns a new Claude Code process for each `query()` call (using `--continue` flag for session continuity)
+- Each query spawns a fresh Claude Code process with a new command-executor MCP server instance
+- MCP server connects to agentServer, executes tools, then disconnects when the query completes
+- This transient connection pattern is normal and expected for the Agent SDK architecture
+
+**When command-executor is called directly from Claude Code CLI:**
+
+- The MCP server maintains a persistent connection throughout the session
+- Connection persists across multiple user requests until the CLI exits
+
+The agentServer logs "Client connected/disconnected" messages to help debug connection issues. The agentServer maintains a single persistent shared dispatcher that handles requests from all MCP connections (whether transient or persistent).
+
+### Debug Mode
+
+Debug mode provides:
+
+- Timing information for cache checks
+- Timing information for API calls
+- Cache hit/miss logging
+- Detailed log file in `~/.tmp/typeagent-coder-wrapper/`
+
+## Examples
+
+### Example 1: Quick Question (Cache Miss)
+
+```
+> What is TypeScript?
+(1234ms)
+────────────────────────────────────────────────────────────────────────────────
+TypeScript is a strongly typed programming language that builds on JavaScript...
+────────────────────────────────────────────────────────────────────────────────
+```
+
+### Example 2: Same Question (Cache Hit)
+
+```
+> What is TypeScript?
+(45ms)
+────────────────────────────────────────────────────────────────────────────────
+TypeScript is a strongly typed programming language that builds on JavaScript...
+────────────────────────────────────────────────────────────────────────────────
+```
+
+### Example 3: With Debug Mode
+
+```
+> npm start -- --debug
+
+[AgentSDK] Debug log: /home/user/.tmp/typeagent-coder-wrapper/coder-wrapper-1234567890.log
+
+> What is TypeScript?
+(45ms)
+────────────────────────────────────────────────────────────────────────────────
+TypeScript is a strongly typed programming language that builds on JavaScript...
+────────────────────────────────────────────────────────────────────────────────
+```
+
+## Development
+
+### Build
+
+```bash
+npm run build
+```
+
+### Clean
+
+```bash
+npm run clean
+```
+
+### Format Code
+
+```bash
+npm run prettier:fix
+```
+
+## Dependencies
+
+- `@anthropic-ai/claude-agent-sdk`: Direct API access to Claude via Agent SDK
+- `@modelcontextprotocol/sdk`: MCP protocol for cache communication
+- `coder-wrapper`: Reuses CacheClient and DebugLogger utilities
+
+## Future Enhancements
+
+Possible improvements:
+
+1. **Streaming Output**: Implement real-time streaming of responses
+2. **Multi-turn Conversations**: Maintain conversation context
+3. **File Attachments**: Support uploading files with prompts
+4. **Custom Tools**: Allow registering custom tool implementations
+5. **Response Formatting**: Better markdown rendering in terminal
+6. **History**: Command history with up/down arrows
+7. **Auto-completion**: Tab completion for common commands
+
+## License
+
+MIT - Copyright (c) Microsoft Corporation
diff --git a/ts/packages/agentSdkWrapper/package.json b/ts/packages/agentSdkWrapper/package.json
new file mode 100644
index 000000000..81dd8f123
--- /dev/null
+++ b/ts/packages/agentSdkWrapper/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "agent-sdk-wrapper",
+ "version": "0.0.1",
+ "private": true,
+ "description": "Direct Anthropic Agent SDK integration with TypeAgent caching support",
+ "homepage": "https://github.com/microsoft/TypeAgent#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/microsoft/TypeAgent.git",
+ "directory": "ts/packages/agentSdkWrapper"
+ },
+ "license": "MIT",
+ "author": "Microsoft",
+ "type": "module",
+ "exports": {
+ ".": "./dist/index.js"
+ },
+ "types": "./dist/index.d.ts",
+ "bin": {
+ "agent-sdk-wrapper": "./dist/cli.js"
+ },
+ "scripts": {
+ "build": "npm run tsc",
+ "clean": "rimraf --glob dist *.tsbuildinfo *.done.build.log",
+ "prettier": "prettier --check . --ignore-path ../../.prettierignore",
+ "prettier:fix": "prettier --write . --ignore-path ../../.prettierignore",
+ "start": "node dist/cli.js",
+ "tsc": "tsc -b"
+ },
+ "dependencies": {
+ "@anthropic-ai/claude-agent-sdk": "^0.2.12",
+ "@modelcontextprotocol/sdk": "^1.0.4",
+ "coder-wrapper": "workspace:*"
+ },
+ "devDependencies": {
+ "@types/node": "^18.19.3",
+ "prettier": "^3.2.5",
+ "rimraf": "^5.0.5",
+ "typescript": "~5.4.5"
+ }
+}
diff --git a/ts/packages/agentSdkWrapper/src/cli.ts b/ts/packages/agentSdkWrapper/src/cli.ts
new file mode 100644
index 000000000..ee985ec0a
--- /dev/null
+++ b/ts/packages/agentSdkWrapper/src/cli.ts
@@ -0,0 +1,535 @@
+#!/usr/bin/env node
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import {
+ query,
+ type SDKMessage,
+ type Options,
+} from "@anthropic-ai/claude-agent-sdk";
+import * as readline from "readline";
+import * as path from "path";
+import { fileURLToPath } from "url";
+import { CacheClient } from "coder-wrapper";
+import { DebugLogger } from "coder-wrapper";
+
+/**
+ * ClaudeSDKClient wrapper for continuous conversation with memory.
+ * Uses the SDK's continue mode to maintain context across multiple queries.
+ */
+class ClaudeSDKClient {
+ private options: Options;
+ private connected: boolean = false;
+ private isFirstQuery: boolean = true;
+
+ constructor(options: Options) {
+ this.options = options;
+ }
+
+ /**
+ * Connect to the SDK (just marks as connected)
+ */
+ async connect(): Promise {
+ if (this.connected) {
+ return;
+ }
+ this.connected = true;
+ }
+
+ /**
+ * Query the agent with a message and return an async generator of responses
+ */
+ async *queryAndReceive(message: string): AsyncGenerator {
+ if (!this.connected) {
+ throw new Error("Client not connected. Call connect() first.");
+ }
+
+ // Use continue mode after first query to maintain context
+ const queryOptions: Options = {
+ ...this.options,
+ continue: !this.isFirstQuery,
+ };
+
+ // Mark that we've done the first query
+ if (this.isFirstQuery) {
+ this.isFirstQuery = false;
+ }
+
+ // Execute query and yield all messages
+ const queryInstance = query({
+ prompt: message,
+ options: queryOptions,
+ });
+
+ yield* queryInstance;
+ }
+
+ /**
+ * Disconnect from the session
+ */
+ disconnect(): void {
+ this.connected = false;
+ this.isFirstQuery = true;
+ }
+}
+
+/**
+ * CLI options
+ */
+interface CliOptions {
+ model: string;
+ debug: boolean;
+ enableCache: boolean;
+ tools: string[];
+ help: boolean;
+}
+
+/**
+ * Parse command line arguments
+ */
+function parseArgs(): CliOptions {
+ const args = process.argv.slice(2);
+ let model = "claude-sonnet-4-5-20250929";
+ let debug = false;
+ let enableCache = true;
+ let tools: string[] = [];
+ let help = false;
+
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i];
+ switch (arg) {
+ case "--help":
+ case "-h":
+ help = true;
+ break;
+ case "--model":
+ case "-m":
+ const modelArg = args[++i]?.toLowerCase();
+ if (modelArg === "sonnet") {
+ model = "claude-sonnet-4-5-20250929";
+ } else if (modelArg === "opus") {
+ model = "claude-opus-4-5-20251101";
+ } else {
+ model = args[i]; // Use custom model string
+ }
+ break;
+ case "--debug":
+ case "-d":
+ debug = true;
+ break;
+ case "--no-cache":
+ enableCache = false;
+ break;
+ case "--tools":
+ case "-t":
+ tools = args[++i]?.split(",") || [];
+ break;
+ }
+ }
+
+ return { model, debug, enableCache, tools, help };
+}
+
+/**
+ * Print usage information
+ */
+function printUsage(): void {
+ console.log(`
+Usage: agent-sdk-wrapper [options]
+
+Options:
+ -m, --model Specify the model to use (default: sonnet)
+ - sonnet: Claude Sonnet 4.5
+ - opus: Claude Opus 4.5
+ - or provide a full model ID
+ -d, --debug Enable debug logging with cache timing information
+ --no-cache Disable cache checking
+ -t, --tools Comma-separated list of tools to enable (e.g., bash,read,write)
+ Default: all tools enabled
+ -h, --help Show this help message
+
+Examples:
+ agent-sdk-wrapper # Use Claude Sonnet with cache
+ agent-sdk-wrapper -m opus # Use Claude Opus
+ agent-sdk-wrapper --debug # Enable debug logging
+ agent-sdk-wrapper --no-cache # Disable cache checking
+ agent-sdk-wrapper -t bash,read # Enable only bash and read tools
+
+Description:
+ Direct integration with the Anthropic Agent SDK with TypeAgent caching support.
+ Uses ClaudeSDKClient for continuous conversation with memory across multiple inputs.
+ The wrapper maintains context across user inputs like a REPL and checks the cache
+ before making API calls to Claude. Cache hits are returned immediately without
+ calling the API.
+
+ Type 'exit' or press Ctrl+C to quit.
+`);
+}
+
+/**
+ * Format output with gray separators like coderWrapper
+ */
+function formatOutput(text: string, terminalWidth: number): string {
+ const grayColor = "\x1b[90m";
+ const resetColor = "\x1b[0m";
+ const separator = grayColor + "─".repeat(terminalWidth) + resetColor;
+ return `${separator}\n${text}\n${separator}`;
+}
+
+/**
+ * Main CLI entry point
+ */
+async function main() {
+ const options = parseArgs();
+
+ if (options.help) {
+ printUsage();
+ process.exit(0);
+ }
+
+ // Initialize debug logger
+ const debugLogger = options.debug ? new DebugLogger(true) : null;
+
+ // Initialize cache client
+ const cacheClient = options.enableCache
+ ? new CacheClient(undefined, debugLogger || undefined)
+ : null;
+
+ if (cacheClient) {
+ try {
+ await cacheClient.connect();
+ if (debugLogger) {
+ debugLogger.log("Cache client connected");
+ }
+ } catch (error) {
+ console.error(
+ `[AgentSDK] Warning: Failed to connect to cache: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ if (debugLogger) {
+ debugLogger.error("Cache connection failed", error);
+ }
+ }
+ }
+
+ // Build tool configuration
+ const allowedTools =
+ options.tools.length > 0
+ ? options.tools
+ : [
+ "Read",
+ "Write",
+ "Edit",
+ "Bash",
+ "Glob",
+ "Grep",
+ "WebSearch",
+ "WebFetch",
+ "Task",
+ "NotebookEdit",
+ "TodoWrite",
+ // Allow all tools from the command-executor MCP server
+ "mcp__command-executor__*",
+ ];
+
+ // Track cache hits for context injection
+ let lastCacheHit: { request: string; result: string } | null = null;
+
+ // Initialize ClaudeSDKClient for continuous conversation with Claude Code configuration
+ if (debugLogger) {
+ debugLogger.log(
+ "Creating ClaudeSDKClient with Claude Code configuration",
+ );
+ }
+
+ // Configure the command-executor MCP server
+ const currentFilePath = fileURLToPath(import.meta.url);
+ const currentDir = path.dirname(currentFilePath);
+ const commandExecutorPath = path.resolve(
+ currentDir,
+ "../../commandExecutor/dist/server.js",
+ );
+
+ if (debugLogger) {
+ debugLogger.log(`Command executor path: ${commandExecutorPath}`);
+ }
+
+ const client = new ClaudeSDKClient({
+ systemPrompt: {
+ type: "preset",
+ preset: "claude_code",
+ },
+ model: options.model,
+ permissionMode: "acceptEdits",
+ allowedTools,
+ cwd: process.cwd(),
+ settingSources: ["project"],
+ maxTurns: 20,
+ maxThinkingTokens: 10000,
+ mcpServers: {
+ "command-executor": {
+ command: "node",
+ args: [commandExecutorPath],
+ },
+ },
+ hooks: {
+ UserPromptSubmit: [
+ {
+ hooks: [
+ async (input) => {
+ // If there was a recent cache hit, inject it as context
+ if (lastCacheHit) {
+ const contextMessage = `\n\n[Previous cached interaction - for context only]\nUser previously asked: "${lastCacheHit.request}"\nResult: ${lastCacheHit.result.substring(0, 500)}${lastCacheHit.result.length > 500 ? "..." : ""}`;
+ // Clear after injecting once
+ lastCacheHit = null;
+ return {
+ hookSpecificOutput: {
+ hookEventName: "UserPromptSubmit",
+ additionalContext: contextMessage,
+ },
+ };
+ }
+ return { continue: true };
+ },
+ ],
+ },
+ ],
+ },
+ });
+
+ // Connect to the client
+ await client.connect();
+
+ if (debugLogger) {
+ debugLogger.log("ClaudeSDKClient connected successfully");
+ }
+
+ console.log(`[AgentSDK] Starting Anthropic Agent SDK wrapper`);
+ console.log(`[AgentSDK] Model: ${options.model}`);
+ console.log(
+ `[AgentSDK] Cache: ${options.enableCache ? "enabled" : "disabled"}`,
+ );
+ if (options.debug) {
+ console.log(
+ `[AgentSDK] Debug mode enabled - cache timing will be logged`,
+ );
+ if (debugLogger) {
+ console.log(
+ `[AgentSDK] Debug log: ${debugLogger.getLogFilePath()}`,
+ );
+ }
+ }
+ if (options.tools.length > 0) {
+ console.log(`[AgentSDK] Tools: ${options.tools.join(", ")}`);
+ } else {
+ console.log(`[AgentSDK] Tools: ${allowedTools.join(", ")}`);
+ }
+ console.log(`[AgentSDK] Type 'exit' or press Ctrl+C to quit\n`);
+
+ // Create readline interface
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ prompt: "> ",
+ });
+
+ rl.prompt();
+
+ rl.on("line", async (input: string) => {
+ const trimmed = input.trim();
+
+ // Handle exit commands
+ if (
+ trimmed === "exit" ||
+ trimmed === "quit" ||
+ trimmed === ".exit" ||
+ trimmed === ".quit"
+ ) {
+ console.log("\n[AgentSDK] Goodbye!");
+ client.disconnect();
+ rl.close();
+ if (cacheClient) {
+ await cacheClient.close();
+ }
+ if (debugLogger) {
+ debugLogger.close();
+ }
+ process.exit(0);
+ }
+
+ // Skip empty inputs
+ if (!trimmed) {
+ rl.prompt();
+ return;
+ }
+
+ const terminalWidth = process.stdout.columns || 80;
+ const startTime = performance.now();
+
+ try {
+ // Check cache first if enabled
+ if (cacheClient) {
+ if (debugLogger) {
+ debugLogger.log(`Checking cache for: "${trimmed}"`);
+ }
+
+ const cacheResult = await cacheClient.checkCache(trimmed);
+ const elapsedMs = performance.now() - startTime;
+
+ if (cacheResult.hit && cacheResult.result) {
+ // Cache hit!
+ if (debugLogger) {
+ debugLogger.log(
+ `✓ Cache HIT (${elapsedMs.toFixed(2)}ms)`,
+ );
+ }
+
+ // Store cache hit for context injection into next user message
+ lastCacheHit = {
+ request: trimmed,
+ result: cacheResult.result,
+ };
+
+ // Print timing on cache hit (always show timing for cache hits)
+ console.log(`(${Math.round(elapsedMs)}ms)`);
+
+ // Print the cached result with separators
+ if (cacheResult.result.trim()) {
+ console.log(
+ formatOutput(cacheResult.result, terminalWidth),
+ );
+ }
+
+ rl.prompt();
+ return;
+ } else {
+ // Cache miss
+ if (debugLogger) {
+ debugLogger.log(
+ `✗ Cache MISS (${elapsedMs.toFixed(2)}ms): ${cacheResult.error || "not found"}`,
+ );
+ debugLogger.log("Calling ClaudeSDKClient");
+ }
+ }
+ }
+
+ // Call ClaudeSDKClient
+ if (debugLogger) {
+ debugLogger.log("Sending query to ClaudeSDKClient");
+ }
+
+ const apiStartTime = performance.now();
+
+ // Query client and receive responses
+ let finalResult = "";
+ let hasShownReasoning = false;
+ for await (const message of client.queryAndReceive(trimmed)) {
+ if (message.type === "result") {
+ // Final result from the agent
+ if (message.subtype === "success") {
+ finalResult = message.result || "";
+ } else {
+ // Handle error results
+ const errors =
+ "errors" in message
+ ? (message as any).errors
+ : undefined;
+ finalResult = `Error: ${errors?.join(", ") || "Unknown error"}`;
+ }
+ break; // Exit loop after result
+ } else if (message.type === "assistant") {
+ // Assistant message during processing - check for reasoning
+ const msg = message.message;
+ const content = msg.content;
+
+ if (Array.isArray(content)) {
+ // Look for thinking/reasoning blocks
+ for (const block of content) {
+ if (block.type === "thinking" && block.thinking) {
+ // Display reasoning in gray
+ if (!hasShownReasoning) {
+ const grayColor = "\x1b[90m";
+ const resetColor = "\x1b[0m";
+ console.log(
+ `\n${grayColor}${block.thinking}${resetColor}\n`,
+ );
+ hasShownReasoning = true;
+ }
+ }
+ }
+ }
+
+ if (options.debug && debugLogger) {
+ const textContent = Array.isArray(content)
+ ? content.find((c: any) => c.type === "text")?.text
+ : "";
+ debugLogger.log(
+ `Assistant message: ${textContent?.substring(0, 100) || "(no text)"}...`,
+ );
+ }
+ }
+ }
+
+ const apiElapsedMs = performance.now() - apiStartTime;
+
+ if (debugLogger) {
+ debugLogger.log(
+ `ClaudeSDKClient query completed (${apiElapsedMs.toFixed(2)}ms)`,
+ );
+ }
+
+ // Don't print timing for API calls (only cache hits show timing)
+
+ // Print the result with separators
+ if (finalResult && finalResult.trim()) {
+ console.log(formatOutput(finalResult, terminalWidth));
+ }
+ } catch (error) {
+ const elapsedMs = performance.now() - startTime;
+ const errorMsg = `Error: ${error instanceof Error ? error.message : String(error)}`;
+
+ if (debugLogger) {
+ debugLogger.error(
+ `Error after ${elapsedMs.toFixed(2)}ms`,
+ error,
+ );
+ }
+
+ console.error(`\n${errorMsg}\n`);
+ }
+
+ rl.prompt();
+ });
+
+ rl.on("close", async () => {
+ // Don't print goodbye here - it's already printed by the exit command handler
+ client.disconnect();
+ if (cacheClient) {
+ await cacheClient.close();
+ }
+ if (debugLogger) {
+ debugLogger.close();
+ }
+ process.exit(0);
+ });
+
+ // Handle Ctrl+C
+ process.on("SIGINT", async () => {
+ console.log("\n[AgentSDK] Received SIGINT, shutting down...");
+ client.disconnect();
+ if (cacheClient) {
+ await cacheClient.close();
+ }
+ if (debugLogger) {
+ debugLogger.close();
+ }
+ process.exit(0);
+ });
+}
+
+// Run the CLI
+main().catch((error) => {
+ console.error(
+ `[AgentSDK] Fatal error: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ process.exit(1);
+});
diff --git a/ts/packages/agentSdkWrapper/src/index.ts b/ts/packages/agentSdkWrapper/src/index.ts
new file mode 100644
index 000000000..21e518ae5
--- /dev/null
+++ b/ts/packages/agentSdkWrapper/src/index.ts
@@ -0,0 +1,6 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Re-export CacheClient for use in other packages
+export { CacheClient, CacheCheckResult } from "coder-wrapper";
+export { DebugLogger } from "coder-wrapper";
diff --git a/ts/packages/agentSdkWrapper/tsconfig.json b/ts/packages/agentSdkWrapper/tsconfig.json
new file mode 100644
index 000000000..a013e0c30
--- /dev/null
+++ b/ts/packages/agentSdkWrapper/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src"
+ },
+ "include": ["src/**/*"]
+}
diff --git a/ts/packages/agentServer/server/src/server.ts b/ts/packages/agentServer/server/src/server.ts
index 3e979dcee..9c8b90856 100644
--- a/ts/packages/agentServer/server/src/server.ts
+++ b/ts/packages/agentServer/server/src/server.ts
@@ -36,68 +36,200 @@ async function main() {
>();
// Create a routing ClientIO that forwards calls to the current request's client
+ // Wraps all methods to catch "Agent channel disconnected" errors gracefully
const routingClientIO: ClientIO = {
clear: (...args) => {
- const client = currentClientContext.getStore();
- client?.clear?.(...args);
+ try {
+ const client = currentClientContext.getStore();
+ client?.clear?.(...args);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes("Agent channel disconnected")
+ ) {
+ throw error;
+ }
+ // Silently ignore disconnected client errors
+ }
},
exit: (...args) => {
- const client = currentClientContext.getStore();
- client?.exit?.(...args);
+ try {
+ const client = currentClientContext.getStore();
+ client?.exit?.(...args);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes("Agent channel disconnected")
+ ) {
+ throw error;
+ }
+ }
},
setDisplayInfo: (...args) => {
- const client = currentClientContext.getStore();
- client?.setDisplayInfo?.(...args);
+ try {
+ const client = currentClientContext.getStore();
+ client?.setDisplayInfo?.(...args);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes("Agent channel disconnected")
+ ) {
+ throw error;
+ }
+ }
},
setDisplay: (...args) => {
- const client = currentClientContext.getStore();
- client?.setDisplay?.(...args);
+ try {
+ const client = currentClientContext.getStore();
+ client?.setDisplay?.(...args);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes("Agent channel disconnected")
+ ) {
+ throw error;
+ }
+ }
},
appendDisplay: (...args) => {
- const client = currentClientContext.getStore();
- client?.appendDisplay?.(...args);
+ try {
+ const client = currentClientContext.getStore();
+ client?.appendDisplay?.(...args);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes("Agent channel disconnected")
+ ) {
+ throw error;
+ }
+ }
},
appendDiagnosticData: (...args) => {
- const client = currentClientContext.getStore();
- client?.appendDiagnosticData?.(...args);
+ try {
+ const client = currentClientContext.getStore();
+ client?.appendDiagnosticData?.(...args);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes("Agent channel disconnected")
+ ) {
+ throw error;
+ }
+ }
},
setDynamicDisplay: (...args) => {
- const client = currentClientContext.getStore();
- client?.setDynamicDisplay?.(...args);
+ try {
+ const client = currentClientContext.getStore();
+ client?.setDynamicDisplay?.(...args);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes("Agent channel disconnected")
+ ) {
+ throw error;
+ }
+ }
},
askYesNo: async (...args) => {
- const client = currentClientContext.getStore();
- return client?.askYesNo?.(...args) ?? false;
+ try {
+ const client = currentClientContext.getStore();
+ return client?.askYesNo?.(...args) ?? false;
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes("Agent channel disconnected")
+ ) {
+ throw error;
+ }
+ return false;
+ }
},
proposeAction: async (...args) => {
- const client = currentClientContext.getStore();
- return client?.proposeAction?.(...args);
+ try {
+ const client = currentClientContext.getStore();
+ return client?.proposeAction?.(...args);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes("Agent channel disconnected")
+ ) {
+ throw error;
+ }
+ return undefined;
+ }
},
popupQuestion: async (...args) => {
- const client = currentClientContext.getStore();
- if (!client?.popupQuestion) {
- throw new Error("popupQuestion not implemented");
+ try {
+ const client = currentClientContext.getStore();
+ if (!client?.popupQuestion) {
+ throw new Error("popupQuestion not implemented");
+ }
+ return client.popupQuestion(...args);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ error.message.includes("Agent channel disconnected")
+ ) {
+ return 0; // Return default choice
+ }
+ throw error;
}
- return client.popupQuestion(...args);
},
notify: (...args) => {
- const client = currentClientContext.getStore();
- client?.notify?.(...args);
+ try {
+ const client = currentClientContext.getStore();
+ client?.notify?.(...args);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes("Agent channel disconnected")
+ ) {
+ throw error;
+ }
+ }
},
openLocalView: (...args) => {
- const client = currentClientContext.getStore();
- client?.openLocalView?.(...args);
+ try {
+ const client = currentClientContext.getStore();
+ client?.openLocalView?.(...args);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes("Agent channel disconnected")
+ ) {
+ throw error;
+ }
+ }
},
closeLocalView: (...args) => {
- const client = currentClientContext.getStore();
- client?.closeLocalView?.(...args);
+ try {
+ const client = currentClientContext.getStore();
+ client?.closeLocalView?.(...args);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ !error.message.includes("Agent channel disconnected")
+ ) {
+ throw error;
+ }
+ }
},
takeAction: (action: string, data?: unknown) => {
- const client = currentClientContext.getStore();
- if (!client?.takeAction) {
- throw new Error(`Action ${action} not supported`);
+ try {
+ const client = currentClientContext.getStore();
+ if (!client?.takeAction) {
+ throw new Error(`Action ${action} not supported`);
+ }
+ return client.takeAction(action, data);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ error.message.includes("Agent channel disconnected")
+ ) {
+ return; // Silently ignore
+ }
+ throw error;
}
- return client.takeAction(action, data);
},
};
diff --git a/ts/packages/commandExecutor/src/commandServer.ts b/ts/packages/commandExecutor/src/commandServer.ts
index 97bcd8904..239cb8f74 100644
--- a/ts/packages/commandExecutor/src/commandServer.ts
+++ b/ts/packages/commandExecutor/src/commandServer.ts
@@ -92,25 +92,44 @@ function stripAnsi(text: string): string {
return text.replace(/\x1b\[[0-9;]*m/g, "");
}
-// downloadImage function removed - images are not downloaded or displayed
+/**
+ * Convert HTML content to plain text by stripping all HTML tags
+ */
+function htmlToPlainText(html: string): string {
+ // Remove img tags entirely
+ let text = html.replace(/
]*>/gi, "");
+
+ // Convert common HTML elements to plain text equivalents
+ text = text.replace(/
/gi, "\n"); //
to newline
+ text = text.replace(/<\/p>/gi, "\n\n"); //
to double newline
+ text = text.replace(/<\/div>/gi, "\n"); // to newline
+ text = text.replace(/<\/li>/gi, "\n"); // to newline
+ text = text.replace(/]*>/gi, "• "); // to bullet point
+
+ // Remove all remaining HTML tags
+ text = text.replace(/<[^>]+>/g, "");
+
+ // Decode common HTML entities
+ text = text.replace(/&/g, "&");
+ text = text.replace(/</g, "<");
+ text = text.replace(/>/g, ">");
+ text = text.replace(/"/g, '"');
+ text = text.replace(/'/g, "'");
+ text = text.replace(/ /g, " ");
+
+ // Clean up excessive whitespace
+ text = text.replace(/\n\s*\n\s*\n/g, "\n\n"); // Max 2 consecutive newlines
+ text = text.replace(/[ \t]+/g, " "); // Multiple spaces to single space
+ text = text.trim();
+
+ return text;
+}
/**
- * Process HTML content to download images and replace img tags with file references
+ * Process HTML content to convert it to plain text
*/
async function processHtmlImages(content: string): Promise {
- // Find all img tags with src attributes
- const imgRegex = /
]+src="([^"]+)"[^>]*>/gi;
- let processed = content;
- const matches = [...content.matchAll(imgRegex)];
-
- for (const match of matches) {
- const fullTag = match[0];
-
- // Just remove the img tag entirely - don't download or display artwork
- processed = processed.replace(fullTag, "");
- }
-
- return processed;
+ return htmlToPlainText(content);
}
/**
@@ -243,6 +262,20 @@ function createMcpClientIO(
};
}
+/**
+ * MCP server that executes commands through TypeAgent dispatcher.
+ *
+ * Lifecycle when used with Agent SDK:
+ * - Each Agent SDK query() spawns a new Claude Code process
+ * - Claude Code spawns a new instance of this MCP server
+ * - MCP server connects to agentServer (persistent shared dispatcher)
+ * - Query executes, tools are called as needed
+ * - Claude Code process exits
+ * - MCP server disconnects from agentServer
+ *
+ * This transient connection pattern is normal and expected.
+ * The agentServer maintains a persistent shared dispatcher across all MCP connections.
+ */
export class CommandServer {
public server: McpServer;
private dispatcher: Dispatcher | null = null;
@@ -282,9 +315,12 @@ export class CommandServer {
await this.server.connect(transport);
// Connect to the TypeAgent dispatcher
+ // Note: When spawned by Agent SDK, this is a transient process per query
+ // Lazy connection on first tool call handles startup race conditions
await this.connectToDispatcher();
- // Start reconnection monitoring
+ // Start reconnection monitoring for cases where dispatcher restarts
+ // When spawned by Agent SDK, this process is transient per query anyway
this.startReconnectionMonitoring();
}
diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml
index c7e8306ba..375acacaa 100644
--- a/ts/pnpm-lock.yaml
+++ b/ts/pnpm-lock.yaml
@@ -1046,6 +1046,31 @@ importers:
specifier: ~5.4.5
version: 5.4.5
+ packages/agentSdkWrapper:
+ dependencies:
+ '@anthropic-ai/claude-agent-sdk':
+ specifier: ^0.2.12
+ version: 0.2.12(zod@4.1.13)
+ '@modelcontextprotocol/sdk':
+ specifier: ^1.0.4
+ version: 1.25.2(hono@4.11.3)(zod@4.1.13)
+ coder-wrapper:
+ specifier: workspace:*
+ version: link:../coderWrapper
+ devDependencies:
+ '@types/node':
+ specifier: ^18.19.3
+ version: 18.19.130
+ prettier:
+ specifier: ^3.2.5
+ version: 3.5.3
+ rimraf:
+ specifier: ^5.0.5
+ version: 5.0.10
+ typescript:
+ specifier: ~5.4.5
+ version: 5.4.5
+
packages/agentServer/client:
dependencies:
'@typeagent/agent-rpc':
@@ -4244,6 +4269,12 @@ packages:
'@antfu/utils@8.1.1':
resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==}
+ '@anthropic-ai/claude-agent-sdk@0.2.12':
+ resolution: {integrity: sha512-lto5qlffODYa3He4jbSVdXtPCWVWUxEqWFj+8mWp4tSnY6tMsQBXjwalm7Bz8YgBsEbrCZrceYMcKSw0eL7H+A==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ zod: ^4.0.0
+
'@asamuzakjp/css-color@3.1.7':
resolution: {integrity: sha512-Ok5fYhtwdyJQmU1PpEv6Si7Y+A4cYb8yNM9oiIJC9TzXPMuN9fvdonKJqcnz9TbFqV6bQ8z0giRq0iaOpGZV2g==}
@@ -13638,6 +13669,19 @@ snapshots:
'@antfu/utils@8.1.1': {}
+ '@anthropic-ai/claude-agent-sdk@0.2.12(zod@4.1.13)':
+ dependencies:
+ zod: 4.1.13
+ optionalDependencies:
+ '@img/sharp-darwin-arm64': 0.33.5
+ '@img/sharp-darwin-x64': 0.33.5
+ '@img/sharp-linux-arm': 0.33.5
+ '@img/sharp-linux-arm64': 0.33.5
+ '@img/sharp-linux-x64': 0.33.5
+ '@img/sharp-linuxmusl-arm64': 0.33.5
+ '@img/sharp-linuxmusl-x64': 0.33.5
+ '@img/sharp-win32-x64': 0.33.5
+
'@asamuzakjp/css-color@3.1.7':
dependencies:
'@csstools/css-calc': 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
@@ -17320,7 +17364,7 @@ snapshots:
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/bonjour@3.5.13':
dependencies:
@@ -17363,7 +17407,7 @@ snapshots:
'@types/cors@2.8.18':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/cytoscape-dagre@2.3.3':
dependencies:
@@ -17619,7 +17663,7 @@ snapshots:
'@types/jsdom@21.1.7':
dependencies:
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
'@types/tough-cookie': 4.0.5
parse5: 7.3.0
@@ -21641,7 +21685,7 @@ snapshots:
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/jsdom': 20.0.1
- '@types/node': 20.19.23
+ '@types/node': 20.19.25
jest-mock: 29.7.0
jest-util: 29.7.0
jsdom: 20.0.3
From 3be03156a7fd15f2a1f374d67ef6cedbe2de7d84 Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 23:01:45 -0800
Subject: [PATCH 18/21] Fix CodeQL security issues in HTML processing
Replaced regex-based HTML sanitization with html-to-text library to address:
1. Incomplete multi-character sanitization (High severity)
2. Double escaping/unescaping vulnerability (High severity)
Changes:
- Removed manual regex chains for HTML tag stripping
- Removed unsafe entity decoding sequence
- Added html-to-text library with proper HTML parsing
- Configured to skip images and preserve formatting
- Added @types/html-to-text for TypeScript support
This eliminates injection risks from incomplete tag removal and
prevents double-decoding vulnerabilities.
Co-Authored-By: Claude Sonnet 4.5
---
.../commandExecutor/src/commandServer.ts | 39 ++++++-------------
ts/pnpm-lock.yaml | 7 +++-
2 files changed, 16 insertions(+), 30 deletions(-)
diff --git a/ts/packages/commandExecutor/src/commandServer.ts b/ts/packages/commandExecutor/src/commandServer.ts
index 239cb8f74..8a717df78 100644
--- a/ts/packages/commandExecutor/src/commandServer.ts
+++ b/ts/packages/commandExecutor/src/commandServer.ts
@@ -17,6 +17,7 @@ import { DisplayAppendMode } from "@typeagent/agent-sdk";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
+import { convert } from "html-to-text";
function executeCommandRequestSchema() {
return {
@@ -93,36 +94,18 @@ function stripAnsi(text: string): string {
}
/**
- * Convert HTML content to plain text by stripping all HTML tags
+ * Convert HTML content to plain text using html-to-text library
+ * This provides secure HTML parsing instead of regex-based sanitization
*/
function htmlToPlainText(html: string): string {
- // Remove img tags entirely
- let text = html.replace(/
]*>/gi, "");
-
- // Convert common HTML elements to plain text equivalents
- text = text.replace(/
/gi, "\n"); //
to newline
- text = text.replace(/<\/p>/gi, "\n\n"); // to double newline
- text = text.replace(/<\/div>/gi, "\n"); // to newline
- text = text.replace(/<\/li>/gi, "\n"); // to newline
- text = text.replace(/]*>/gi, "• "); // to bullet point
-
- // Remove all remaining HTML tags
- text = text.replace(/<[^>]+>/g, "");
-
- // Decode common HTML entities
- text = text.replace(/&/g, "&");
- text = text.replace(/</g, "<");
- text = text.replace(/>/g, ">");
- text = text.replace(/"/g, '"');
- text = text.replace(/'/g, "'");
- text = text.replace(/ /g, " ");
-
- // Clean up excessive whitespace
- text = text.replace(/\n\s*\n\s*\n/g, "\n\n"); // Max 2 consecutive newlines
- text = text.replace(/[ \t]+/g, " "); // Multiple spaces to single space
- text = text.trim();
-
- return text;
+ return convert(html, {
+ wordwrap: false,
+ preserveNewlines: true,
+ selectors: [
+ { selector: "img", format: "skip" }, // Skip images entirely
+ { selector: "a", options: { ignoreHref: true } }, // Keep link text, ignore URLs
+ ],
+ });
}
/**
diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml
index c7de5ff3c..2c90e8d45 100644
--- a/ts/pnpm-lock.yaml
+++ b/ts/pnpm-lock.yaml
@@ -139,7 +139,7 @@ importers:
version: 8.18.1
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))
+ version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5))
prettier:
specifier: ^3.5.3
version: 3.5.3
@@ -864,7 +864,7 @@ importers:
version: 12.0.2(webpack@5.99.8)
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5))
+ version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))
prettier:
specifier: ^3.5.3
version: 3.5.3
@@ -2847,6 +2847,9 @@ importers:
specifier: ^4.1.13
version: 4.1.13
devDependencies:
+ '@types/html-to-text':
+ specifier: ^9.0.4
+ version: 9.0.4
prettier:
specifier: ^3.2.5
version: 3.5.3
From 5dc7496a64f220976f31b20de990eb385342bed6 Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 23:04:23 -0800
Subject: [PATCH 19/21] Add html-to-text dependencies to package.json
Fix build failures by properly declaring html-to-text dependencies:
- Added html-to-text ^9.0.5 to dependencies
- Added @types/html-to-text ^9.0.4 to devDependencies
- Updated pnpm-lock.yaml to match package.json
This resolves the ERR_PNPM_OUTDATED_LOCKFILE errors in CI builds.
Co-Authored-By: Claude Sonnet 4.5
---
ts/packages/commandExecutor/package.json | 2 ++
ts/pnpm-lock.yaml | 7 +++++--
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/ts/packages/commandExecutor/package.json b/ts/packages/commandExecutor/package.json
index 9778f8854..e3e491d6c 100644
--- a/ts/packages/commandExecutor/package.json
+++ b/ts/packages/commandExecutor/package.json
@@ -30,10 +30,12 @@
"@typeagent/agent-server-client": "workspace:*",
"@typeagent/dispatcher-types": "workspace:*",
"dotenv": "^16.3.1",
+ "html-to-text": "^9.0.5",
"isomorphic-ws": "^5.0.0",
"zod": "^4.1.13"
},
"devDependencies": {
+ "@types/html-to-text": "^9.0.4",
"prettier": "^3.2.5",
"rimraf": "^5.0.5",
"typescript": "~5.4.5"
diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml
index 2c90e8d45..6df3011a1 100644
--- a/ts/pnpm-lock.yaml
+++ b/ts/pnpm-lock.yaml
@@ -139,7 +139,7 @@ importers:
version: 8.18.1
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5))
+ version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))
prettier:
specifier: ^3.5.3
version: 3.5.3
@@ -864,7 +864,7 @@ importers:
version: 12.0.2(webpack@5.99.8)
jest:
specifier: ^29.7.0
- version: 29.7.0(@types/node@20.19.25)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.4.5))
+ version: 29.7.0(@types/node@22.15.18)(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.4.5))
prettier:
specifier: ^3.5.3
version: 3.5.3
@@ -2840,6 +2840,9 @@ importers:
dotenv:
specifier: ^16.3.1
version: 16.5.0
+ html-to-text:
+ specifier: ^9.0.5
+ version: 9.0.5
isomorphic-ws:
specifier: ^5.0.0
version: 5.0.0(ws@8.18.2)
From 0bcba33d450c91ea6fc59de9a80e6f72e01cda90 Mon Sep 17 00:00:00 2001
From: steveluc
Date: Mon, 19 Jan 2026 23:09:08 -0800
Subject: [PATCH 20/21] Add trademark section to agentSdkWrapper README
Co-Authored-By: Claude Sonnet 4.5
---
ts/packages/agentSdkWrapper/README.md | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/ts/packages/agentSdkWrapper/README.md b/ts/packages/agentSdkWrapper/README.md
index 1b978c2c9..a8167f090 100644
--- a/ts/packages/agentSdkWrapper/README.md
+++ b/ts/packages/agentSdkWrapper/README.md
@@ -269,3 +269,11 @@ Possible improvements:
## License
MIT - Copyright (c) Microsoft Corporation
+
+## Trademarks
+
+This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
+trademarks or logos is subject to and must follow
+[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
+Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
+Any use of third-party trademarks or logos are subject to those third-party's policies.
From f75868af5de6c367146f58e7b66e9a86c74f735f Mon Sep 17 00:00:00 2001
From: steveluc
Date: Tue, 20 Jan 2026 11:58:44 -0800
Subject: [PATCH 21/21] Add user confirmation flow for yes/no prompts in MCP
server
Implemented a two-phase confirmation flow to handle TypeAgent's askYesNo
prompts in the MCP server context. Since MCP tools are request-response
based and cannot pause for user input, the flow works as follows:
1. When a command requires confirmation, askYesNo() throws a
USER_CONFIRMATION_REQUIRED error instead of blocking
2. The MCP server catches this error and returns a user-friendly message
3. Claude uses AskUserQuestion to confirm with the user
4. If approved, Claude retries the command with confirmed=true parameter
5. On retry, askYesNo() sees the confirmed flag and returns true immediately
Changes:
- Added optional 'confirmed' parameter to ExecuteCommandRequest schema
- Added currentRequestConfirmed state tracking to CommandServer
- Modified askYesNo() to check confirmation flag and throw error if not confirmed
- Updated executeCommand() to handle USER_CONFIRMATION_REQUIRED errors
- Enhanced tool description to document the confirmation flow
- Added proper cleanup in finally block to reset confirmation state
This implementation is stateless and works across MCP server restarts,
providing a clean API for handling destructive operations safely.
Co-Authored-By: Claude Sonnet 4.5
---
.../commandExecutor/src/commandServer.ts | 56 ++++++++++++++++++-
1 file changed, 53 insertions(+), 3 deletions(-)
diff --git a/ts/packages/commandExecutor/src/commandServer.ts b/ts/packages/commandExecutor/src/commandServer.ts
index 8a717df78..070035a06 100644
--- a/ts/packages/commandExecutor/src/commandServer.ts
+++ b/ts/packages/commandExecutor/src/commandServer.ts
@@ -23,6 +23,7 @@ function executeCommandRequestSchema() {
return {
request: z.string(),
cacheCheck: z.boolean().optional(),
+ confirmed: z.boolean().optional(),
};
}
const ExecuteCommandRequestSchema = z.object(executeCommandRequestSchema());
@@ -122,6 +123,7 @@ async function processHtmlImages(content: string): Promise {
function createMcpClientIO(
logger: Logger,
responseCollector: { messages: string[] },
+ getConfirmedFlag: () => boolean,
): ClientIO {
return {
clear(): void {
@@ -195,10 +197,19 @@ function createMcpClientIO(
requestId: RequestId,
defaultValue?: boolean,
): Promise {
+ // Check if this request was pre-confirmed
+ if (getConfirmedFlag()) {
+ logger.log(
+ `ClientIO: askYesNo(requestId=${requestId}) - "${message}" (auto-approved due to confirmed=true)`,
+ );
+ return true;
+ }
+
+ // Otherwise, throw error requiring user confirmation
logger.log(
- `ClientIO: askYesNo(requestId=${requestId}) - "${message}" (defaulting to ${defaultValue ?? false})`,
+ `ClientIO: askYesNo(requestId=${requestId}) - "${message}" (requires user confirmation)`,
);
- return defaultValue ?? false;
+ throw new Error(`USER_CONFIRMATION_REQUIRED: ${message}`);
},
async proposeAction(
actionTemplates: TemplateEditConfig,
@@ -268,6 +279,7 @@ export class CommandServer {
private reconnectDelayMs: number = 5000; // 5 seconds between reconnection attempts
private logger: Logger;
private responseCollector: { messages: string[] } = { messages: [] };
+ private currentRequestConfirmed: boolean = false;
/**
* Creates a new CommandServer instance
@@ -317,6 +329,7 @@ export class CommandServer {
const clientIO = createMcpClientIO(
this.logger,
this.responseCollector,
+ () => this.currentRequestConfirmed,
);
this.dispatcher = await connectDispatcher(
clientIO,
@@ -377,7 +390,15 @@ export class CommandServer {
"- Music & media: play songs, control playback\n" +
"- Lists & tasks: manage shopping lists, todo lists\n" +
"- Calendar: schedule events, view calendar\n" +
- "- VSCode automation: change theme (e.g. 'switch to monokai theme'), open files, create folders, run tasks, manage editor layout, open terminals, toggle settings",
+ "- VSCode automation: change theme (e.g. 'switch to monokai theme'), open files, create folders, run tasks, manage editor layout, open terminals, toggle settings\n\n" +
+ "Parameters:\n" +
+ "- request: The command to execute\n" +
+ "- cacheCheck: (optional) Check cache before executing\n" +
+ "- confirmed: (optional) Set to true if user has already confirmed any yes/no prompts\n\n" +
+ "Confirmation Flow:\n" +
+ "Some commands (like deleting sessions or clearing data) require user confirmation. " +
+ "If a command requires confirmation, the tool will return an error message indicating what needs to be confirmed. " +
+ "Ask the user for confirmation, then retry the same command with confirmed=true if they approve.",
},
async (request: ExecuteCommandRequest) =>
this.executeCommand(request),
@@ -389,6 +410,12 @@ export class CommandServer {
): Promise {
this.logger.log(`User request: ${request.request}`);
+ // Set confirmation flag for this request
+ this.currentRequestConfirmed = request.confirmed ?? false;
+ if (this.currentRequestConfirmed) {
+ this.logger.log("Request has confirmed=true flag");
+ }
+
// If not connected, try to connect now (lazy connection)
if (!this.dispatcher && !this.isConnecting) {
this.logger.log(
@@ -502,6 +529,26 @@ export class CommandServer {
// Fallback if no messages were collected
return toolResult(`Successfully executed: ${request.request}`);
} catch (error) {
+ // Check if this is a user confirmation request
+ if (
+ error instanceof Error &&
+ error.message.startsWith("USER_CONFIRMATION_REQUIRED:")
+ ) {
+ const question = error.message.replace(
+ "USER_CONFIRMATION_REQUIRED: ",
+ "",
+ );
+ this.logger.log(
+ `Command requires user confirmation: ${question}`,
+ );
+ return toolResult(
+ `⚠️ Confirmation Required\n\n` +
+ `The action you requested requires confirmation:\n\n` +
+ `"${question}"\n\n` +
+ `Please confirm with the user, then retry the command with confirmed=true if they approve.`,
+ );
+ }
+
const errorMsg = `Failed to execute command: ${error instanceof Error ? error.message : String(error)}`;
this.logger.error(errorMsg);
@@ -509,6 +556,9 @@ export class CommandServer {
this.dispatcher = null;
return toolResult(errorMsg);
+ } finally {
+ // Always reset confirmation flag after request completes
+ this.currentRequestConfirmed = false;
}
}