From 43545c105cb3610cba544b4f53cf0bad4a68f3b1 Mon Sep 17 00:00:00 2001 From: steveluc Date: Fri, 16 Jan 2026 23:19:53 -0800 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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}`,