From 99919d6dfbfee48b45029b316b8ada0699537d84 Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 23 Jan 2026 23:16:21 +0100 Subject: [PATCH] feat: replace SpawnDetector with MCP server for spawn1337 protocol Instead of parsing terminal output for tags, spawn capabilities are now exposed as native MCP tools that Claude Code can call directly. The MCP server (stdio transport) proxies requests to the existing REST API. - Add src/mcp-server.ts with 6 tools: spawn_agent, list_agents, get_agent_status, get_agent_result, send_agent_message, cancel_agent - Remove src/spawn-detector.ts and all references in session.ts/server.ts - Add CLAUDEMAN_API_URL env var propagation to sessions and screens - Write .mcp.json to case directories during creation - Remove spawn1337 tag documentation from case-template.md - Add claudeman-mcp bin entry to package.json Co-Authored-By: Claude Opus 4.5 --- package-lock.json | 923 ++++++++++++++++++++++++++++++++- package.json | 9 +- src/mcp-server.ts | 370 +++++++++++++ src/screen-manager.ts | 2 +- src/session.ts | 50 +- src/spawn-detector.ts | 292 ----------- src/spawn-types.ts | 2 +- src/templates/case-template.md | 84 --- src/web/server.ts | 54 +- test/mcp-server.test.ts | 224 ++++++++ test/spawn-detector.test.ts | 249 --------- 11 files changed, 1549 insertions(+), 710 deletions(-) create mode 100644 src/mcp-server.ts delete mode 100644 src/spawn-detector.ts create mode 100644 test/mcp-server.test.ts delete mode 100644 test/spawn-detector.test.ts diff --git a/package-lock.json b/package-lock.json index 79235232..71eb3771 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@fastify/static": "^8.0.0", + "@modelcontextprotocol/sdk": "^1.25.3", "chalk": "^5.3.0", "commander": "^12.1.0", "fastify": "^5.1.0", @@ -18,7 +19,8 @@ "ink-text-input": "^6.0.0", "node-pty": "^1.1.0", "react": "^19.2.3", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "zod": "^4.3.6" }, "bin": { "claudeman": "dist/index.js" @@ -741,6 +743,18 @@ "glob": "^11.0.0" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -816,6 +830,45 @@ "node": ">=8" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.25.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.3.tgz", + "integrity": "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "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/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -1379,6 +1432,19 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, + "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==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/agent-browser": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/agent-browser/-/agent-browser-0.6.0.tgz", @@ -1395,6 +1461,16 @@ "agent-browser": "bin/agent-browser" } }, + "node_modules/agent-browser/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -1520,6 +1596,68 @@ "fastq": "^1.17.1" } }, + "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==", + "license": "MIT", + "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/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1664,6 +1802,15 @@ "node": ">= 0.6" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-to-spaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", @@ -1686,6 +1833,32 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1707,6 +1880,23 @@ "devOptional": true, "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "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", @@ -1725,18 +1915,47 @@ "node": ">=6" } }, + "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==", + "license": "MIT", + "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==", "license": "MIT" }, + "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==", + "license": "MIT" + }, "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==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -1749,6 +1968,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -1756,6 +1993,18 @@ "dev": true, "license": "MIT" }, + "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==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-toolkit": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", @@ -1833,6 +2082,36 @@ "@types/estree": "^1.0.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -1843,6 +2122,86 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1980,6 +2339,27 @@ } } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "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/find-my-way": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.4.0.tgz", @@ -2010,6 +2390,24 @@ "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2025,6 +2423,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-east-asian-width": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", @@ -2037,6 +2444,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-tsconfig": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", @@ -2073,6 +2517,18 @@ "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2083,6 +2539,40 @@ "node": ">=8" } }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.5.tgz", + "integrity": "sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2110,6 +2600,22 @@ "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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/indent-string": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", @@ -2303,6 +2809,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2363,6 +2875,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -2395,6 +2916,12 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "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==", + "license": "BSD-2-Clause" + }, "node_modules/light-my-request": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", @@ -2479,6 +3006,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -2491,6 +3048,31 @@ "node": ">=10.0.0" } }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -2524,6 +3106,12 @@ "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==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2543,6 +3131,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -2559,6 +3156,27 @@ "node-addon-api": "^7.1.0" } }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -2579,6 +3197,27 @@ "node": ">=14.0.0" } }, + "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==", + "license": "MIT", + "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==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -2600,6 +3239,15 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/patch-console": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", @@ -2634,6 +3282,16 @@ "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==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -2698,6 +3356,15 @@ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, + "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==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/playwright-core": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", @@ -2756,12 +3423,73 @@ ], "license": "MIT" }, + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/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==", + "license": "MIT", + "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==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", @@ -2906,6 +3634,22 @@ "fsevents": "~2.3.2" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "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/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2954,6 +3698,12 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2988,6 +3738,51 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -3021,6 +3816,78 @@ "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -3349,6 +4216,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "MIT", + "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.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3370,6 +4251,15 @@ "dev": true, "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", @@ -3383,6 +4273,15 @@ "uuid": "dist/bin/uuid" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -3697,6 +4596,12 @@ "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==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -3725,14 +4630,22 @@ "license": "MIT" }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "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==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/package.json b/package.json index f7ec50de..fbf8c1e6 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "type": "module", "main": "dist/index.js", "bin": { - "claudeman": "./dist/index.js" + "claudeman": "./dist/index.js", + "claudeman-mcp": "./dist/mcp-server.js" }, "scripts": { - "build": "tsc && chmod +x dist/index.js && mkdir -p dist/web dist/templates && cp -r src/web/public dist/web/ && cp src/templates/case-template.md dist/templates/", + "build": "tsc && chmod +x dist/index.js dist/mcp-server.js && mkdir -p dist/web dist/templates && cp -r src/web/public dist/web/ && cp src/templates/case-template.md dist/templates/", "start": "node dist/index.js", "dev": "tsx src/index.ts", "web": "node dist/index.js web", @@ -36,6 +37,7 @@ "license": "MIT", "dependencies": { "@fastify/static": "^8.0.0", + "@modelcontextprotocol/sdk": "^1.25.3", "chalk": "^5.3.0", "commander": "^12.1.0", "fastify": "^5.1.0", @@ -44,7 +46,8 @@ "ink-text-input": "^6.0.0", "node-pty": "^1.1.0", "react": "^19.2.3", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "zod": "^4.3.6" }, "devDependencies": { "@types/node": "^20.14.0", diff --git a/src/mcp-server.ts b/src/mcp-server.ts new file mode 100644 index 00000000..64110695 --- /dev/null +++ b/src/mcp-server.ts @@ -0,0 +1,370 @@ +#!/usr/bin/env node + +/** + * @fileoverview MCP Server for Claudeman Spawn1337 protocol. + * + * Exposes spawn capabilities as native MCP tools that Claude Code can call + * directly, replacing the terminal-tag-parsing approach (SpawnDetector). + * + * Tools: + * - spawn_agent: Spawn a new autonomous agent + * - list_agents: List all agents (active + completed) + * - get_agent_status: Get detailed agent status + * - get_agent_result: Read a completed agent's result + * - send_agent_message: Send a message to a running agent + * - cancel_agent: Cancel a running agent + * + * Environment: + * - CLAUDEMAN_API_URL: Base URL for the Claudeman API (default: http://localhost:3000) + * - CLAUDEMAN_SESSION_ID: Session ID of the calling Claude session + * + * @module mcp-server + */ + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; + +// ========== Configuration ========== + +const API_URL = process.env.CLAUDEMAN_API_URL || 'http://localhost:3000'; +const SESSION_ID = process.env.CLAUDEMAN_SESSION_ID || ''; + +// ========== API Helper ========== + +/** + * Make an HTTP request to the Claudeman API. + */ +async function apiRequest(method: string, path: string, body?: unknown): Promise<{ status: number; data: unknown }> { + const url = `${API_URL}${path}`; + const options: RequestInit = { + method, + headers: { 'Content-Type': 'application/json' }, + }; + if (body !== undefined) { + options.body = JSON.stringify(body); + } + + const response = await fetch(url, options); + const data = await response.json(); + return { status: response.status, data }; +} + +// ========== YAML Construction ========== + +/** + * Build a YAML frontmatter + body task spec from structured parameters. + */ +export function buildTaskSpec(params: { + agentId: string; + name: string; + instructions: string; + type?: string; + priority?: string; + maxTokens?: number; + maxCost?: number; + timeoutMinutes?: number; + canModifyParentFiles?: boolean; + contextFiles?: string[]; + dependsOn?: string[]; + completionPhrase?: string; + outputFormat?: string; + successCriteria?: string; + workingDir?: string; +}): string { + const lines: string[] = ['---']; + + lines.push(`agentId: ${params.agentId}`); + lines.push(`name: ${params.name}`); + + if (params.type) lines.push(`type: ${params.type}`); + if (params.priority) lines.push(`priority: ${params.priority}`); + if (params.maxTokens != null) lines.push(`maxTokens: ${params.maxTokens}`); + if (params.maxCost != null) lines.push(`maxCost: ${params.maxCost}`); + if (params.timeoutMinutes != null) lines.push(`timeoutMinutes: ${params.timeoutMinutes}`); + if (params.canModifyParentFiles != null) lines.push(`canModifyParentFiles: ${params.canModifyParentFiles}`); + if (params.completionPhrase) lines.push(`completionPhrase: ${params.completionPhrase}`); + if (params.outputFormat) lines.push(`outputFormat: ${params.outputFormat}`); + if (params.successCriteria) lines.push(`successCriteria: "${params.successCriteria.replace(/"/g, '\\"')}"`); + if (params.workingDir) lines.push(`workingDir: ${params.workingDir}`); + + if (params.contextFiles && params.contextFiles.length > 0) { + lines.push(`contextFiles: [${params.contextFiles.join(', ')}]`); + } + + if (params.dependsOn && params.dependsOn.length > 0) { + lines.push(`dependsOn: [${params.dependsOn.join(', ')}]`); + } + + lines.push('---'); + lines.push(''); + lines.push(params.instructions); + + return lines.join('\n'); +} + +// ========== MCP Server Setup ========== + +const server = new McpServer({ + name: 'claudeman-spawn', + version: '1.0.0', +}); + +// ---- spawn_agent ---- + +server.tool( + 'spawn_agent', + 'Spawn a new autonomous Claude agent to handle a subtask. The agent runs in its own session with full capabilities.', + { + agentId: z.string().describe('Unique identifier for the agent (e.g., "research-auth-001")'), + name: z.string().describe('Human-readable name for the agent'), + instructions: z.string().describe('Detailed task instructions for the agent (markdown)'), + type: z.enum(['explore', 'implement', 'test', 'review', 'refactor', 'research', 'generate', 'fix', 'general']).optional().describe('Task type/category'), + priority: z.enum(['low', 'normal', 'high', 'critical']).optional().describe('Priority for queue ordering'), + maxTokens: z.number().optional().describe('Maximum token budget (input + output combined)'), + maxCost: z.number().optional().describe('Maximum cost in USD'), + timeoutMinutes: z.number().optional().describe('Maximum runtime in minutes (max: 120)'), + canModifyParentFiles: z.boolean().optional().describe('Whether agent can modify files in parent project directory'), + contextFiles: z.array(z.string()).optional().describe('Files to symlink into agent workspace as context'), + dependsOn: z.array(z.string()).optional().describe('Agent IDs that must complete before this one starts'), + completionPhrase: z.string().optional().describe('Phrase agent outputs when finished (default: auto-generated)'), + outputFormat: z.enum(['markdown', 'json', 'code', 'structured', 'freeform']).optional().describe('Expected output format'), + successCriteria: z.string().optional().describe('Success criteria included in agent instructions'), + workingDir: z.string().optional().describe('Working directory (relative to parent, or absolute)'), + }, + async (params) => { + if (!SESSION_ID) { + return { + content: [{ type: 'text', text: 'Error: CLAUDEMAN_SESSION_ID not set. This tool must be run within a Claudeman-managed session.' }], + isError: true, + }; + } + + const taskSpec = buildTaskSpec(params); + + try { + const { status, data } = await apiRequest('POST', '/api/spawn/trigger', { + parentSessionId: SESSION_ID, + taskContent: taskSpec, + }); + + if (status >= 400) { + const errorData = data as { error?: { code?: string; details?: string } }; + const errorMsg = errorData.error?.details || errorData.error?.code || 'Unknown error'; + return { + content: [{ type: 'text', text: `Error spawning agent: ${errorMsg}` }], + isError: true, + }; + } + + const result = data as { success: boolean; data?: { agentId: string } }; + const agentId = result.data?.agentId || params.agentId; + return { + content: [{ type: 'text', text: `Agent spawned successfully.\n\nAgent ID: ${agentId}` }], + }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error: Could not connect to Claudeman API at ${API_URL}. Is the web server running?\n\n${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } + } +); + +// ---- list_agents ---- + +server.tool( + 'list_agents', + 'List all spawn agents (active, queued, and completed).', + {}, + async () => { + try { + const { status, data } = await apiRequest('GET', '/api/spawn/agents'); + + if (status >= 400) { + return { + content: [{ type: 'text', text: `Error listing agents: ${JSON.stringify(data)}` }], + isError: true, + }; + } + + const agents = data as Array<{ agentId: string; name: string; status: string; type: string; priority: string }>; + if (agents.length === 0) { + return { content: [{ type: 'text', text: 'No agents found.' }] }; + } + + const summary = agents.map(a => `- ${a.agentId} (${a.name}): ${a.status} [${a.type}, ${a.priority}]`).join('\n'); + return { content: [{ type: 'text', text: `Agents (${agents.length}):\n\n${summary}` }] }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error: Could not connect to Claudeman API at ${API_URL}.\n\n${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } + } +); + +// ---- get_agent_status ---- + +server.tool( + 'get_agent_status', + 'Get detailed status and progress of a specific agent.', + { + agentId: z.string().describe('The agent ID to query'), + }, + async ({ agentId }) => { + try { + const { status, data } = await apiRequest('GET', `/api/spawn/agents/${encodeURIComponent(agentId)}`); + + if (status === 404) { + return { + content: [{ type: 'text', text: `Agent not found: ${agentId}` }], + isError: true, + }; + } + + if (status >= 400) { + return { + content: [{ type: 'text', text: `Error getting agent status: ${JSON.stringify(data)}` }], + isError: true, + }; + } + + return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error: Could not connect to Claudeman API at ${API_URL}.\n\n${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } + } +); + +// ---- get_agent_result ---- + +server.tool( + 'get_agent_result', + 'Read the result of a completed agent. Returns the agent\'s output and metadata.', + { + agentId: z.string().describe('The agent ID whose result to read'), + }, + async ({ agentId }) => { + try { + const { status, data } = await apiRequest('GET', `/api/spawn/agents/${encodeURIComponent(agentId)}/result`); + + if (status === 404) { + return { + content: [{ type: 'text', text: `Agent or result not found: ${agentId}. The agent may not have completed yet.` }], + isError: true, + }; + } + + if (status >= 400) { + return { + content: [{ type: 'text', text: `Error getting agent result: ${JSON.stringify(data)}` }], + isError: true, + }; + } + + // Result could be a string (raw markdown) or an object + const resultText = typeof data === 'string' ? data : JSON.stringify(data, null, 2); + return { content: [{ type: 'text', text: resultText }] }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error: Could not connect to Claudeman API at ${API_URL}.\n\n${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } + } +); + +// ---- send_agent_message ---- + +server.tool( + 'send_agent_message', + 'Send a message to a running agent. The message is written to the agent\'s communication channel.', + { + agentId: z.string().describe('The agent ID to message'), + message: z.string().describe('The message content (markdown)'), + }, + async ({ agentId, message }) => { + try { + const { status, data } = await apiRequest('POST', `/api/spawn/agents/${encodeURIComponent(agentId)}/message`, { + content: message, + sender: 'parent', + }); + + if (status === 404) { + return { + content: [{ type: 'text', text: `Agent not found: ${agentId}` }], + isError: true, + }; + } + + if (status >= 400) { + return { + content: [{ type: 'text', text: `Error sending message: ${JSON.stringify(data)}` }], + isError: true, + }; + } + + return { content: [{ type: 'text', text: `Message sent to agent ${agentId}.` }] }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error: Could not connect to Claudeman API at ${API_URL}.\n\n${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } + } +); + +// ---- cancel_agent ---- + +server.tool( + 'cancel_agent', + 'Cancel a running agent. Sends a graceful shutdown signal.', + { + agentId: z.string().describe('The agent ID to cancel'), + reason: z.string().optional().describe('Reason for cancellation'), + }, + async ({ agentId, reason }) => { + try { + const { status, data } = await apiRequest('POST', `/api/spawn/agents/${encodeURIComponent(agentId)}/cancel`, { + reason: reason || 'Cancelled by parent session', + }); + + if (status === 404) { + return { + content: [{ type: 'text', text: `Agent not found: ${agentId}` }], + isError: true, + }; + } + + if (status >= 400) { + return { + content: [{ type: 'text', text: `Error cancelling agent: ${JSON.stringify(data)}` }], + isError: true, + }; + } + + return { content: [{ type: 'text', text: `Agent ${agentId} cancel request sent.` }] }; + } catch (err) { + return { + content: [{ type: 'text', text: `Error: Could not connect to Claudeman API at ${API_URL}.\n\n${err instanceof Error ? err.message : String(err)}` }], + isError: true, + }; + } + } +); + +// ========== Start Server ========== + +async function main(): Promise { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((err) => { + console.error('MCP server failed to start:', err); + process.exit(1); +}); diff --git a/src/screen-manager.ts b/src/screen-manager.ts index 218e3287..8359b203 100644 --- a/src/screen-manager.ts +++ b/src/screen-manager.ts @@ -168,7 +168,7 @@ export class ScreenManager extends EventEmitter { // Create screen in detached mode with the appropriate command // Set CLAUDEMAN_SCREEN=1 so Claude sessions know they're running in Claudeman // This helps prevent Claude from attempting to kill its own screen session - const envVars = `CLAUDEMAN_SCREEN=1 CLAUDEMAN_SESSION_ID=${sessionId} CLAUDEMAN_SCREEN_NAME=${screenName}`; + const envVars = `CLAUDEMAN_SCREEN=1 CLAUDEMAN_SESSION_ID=${sessionId} CLAUDEMAN_SCREEN_NAME=${screenName} CLAUDEMAN_API_URL=${process.env.CLAUDEMAN_API_URL || 'http://localhost:3000'}`; const cmd = mode === 'claude' ? `${envVars} claude --dangerously-skip-permissions` : `${envVars} $SHELL`; diff --git a/src/session.ts b/src/session.ts index 813b8350..f01a4660 100644 --- a/src/session.ts +++ b/src/session.ts @@ -21,7 +21,6 @@ import * as pty from 'node-pty'; import { SessionState, SessionStatus, SessionConfig, ScreenSession, RalphTrackerState, RalphTodoItem } from './types.js'; import { TaskTracker, type BackgroundTask } from './task-tracker.js'; import { RalphTracker } from './ralph-tracker.js'; -import { SpawnDetector } from './spawn-detector.js'; import { ScreenManager } from './screen-manager.js'; export type { BackgroundTask } from './task-tracker.js'; @@ -226,14 +225,6 @@ export interface SessionEvents { ralphTodoUpdate: (todos: RalphTodoItem[]) => void; /** Ralph completion phrase detected */ ralphCompletionDetected: (phrase: string) => void; - /** Spawn1337 agent spawn requested */ - spawnRequested: (filePath: string, rawLine: string) => void; - /** Spawn1337 agent status query */ - spawnStatusRequested: (agentId: string) => void; - /** Spawn1337 agent cancel request */ - spawnCancelRequested: (agentId: string) => void; - /** Spawn1337 message to child agent */ - spawnMessageToChild: (agentId: string, content: string) => void; } /** @@ -334,9 +325,6 @@ export class Session extends EventEmitter { // Ralph tracking (Ralph Wiggum loops and todo lists inside Claude Code) private _ralphTracker: RalphTracker; - // Spawn1337 detection (agent spawning protocol) - private _spawnDetector: SpawnDetector; - // Agent tree tracking private _parentAgentId: string | null = null; private _childAgentIds: string[] = []; @@ -355,13 +343,6 @@ export class Session extends EventEmitter { completionDetected: (phrase: string) => void; } | null = null; - private _spawnHandlers: { - spawnRequested: (filePath: string, rawLine: string) => void; - statusRequested: (agentId: string) => void; - cancelRequested: (agentId: string) => void; - messageToChild: (agentId: string, content: string) => void; - } | null = null; - constructor(config: Partial & { workingDir: string; mode?: SessionMode; @@ -405,18 +386,6 @@ export class Session extends EventEmitter { this._ralphTracker.on('todoUpdate', this._ralphHandlers.todoUpdate); this._ralphTracker.on('completionDetected', this._ralphHandlers.completionDetected); - // Initialize Spawn detector and forward events (store handlers for cleanup) - this._spawnDetector = new SpawnDetector(); - this._spawnHandlers = { - spawnRequested: (filePath, rawLine) => this.emit('spawnRequested', filePath, rawLine), - statusRequested: (agentId) => this.emit('spawnStatusRequested', agentId), - cancelRequested: (agentId) => this.emit('spawnCancelRequested', agentId), - messageToChild: (agentId, content) => this.emit('spawnMessageToChild', agentId, content), - }; - this._spawnDetector.on('spawnRequested', this._spawnHandlers.spawnRequested); - this._spawnDetector.on('statusRequested', this._spawnHandlers.statusRequested); - this._spawnDetector.on('cancelRequested', this._spawnHandlers.cancelRequested); - this._spawnDetector.on('messageToChild', this._spawnHandlers.messageToChild); } get status(): SessionStatus { @@ -504,11 +473,6 @@ export class Session extends EventEmitter { return this._ralphTracker.getTodoStats(); } - // Spawn1337 tracking getters - get spawnDetector(): SpawnDetector { - return this._spawnDetector; - } - get parentAgentId(): string | null { return this._parentAgentId; } @@ -785,6 +749,7 @@ export class Session extends EventEmitter { // Inform Claude it's running within Claudeman (helps prevent self-termination) CLAUDEMAN_SCREEN: '1', CLAUDEMAN_SESSION_ID: this.id, + CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000', }, }); } @@ -809,9 +774,6 @@ export class Session extends EventEmitter { // Forward to Ralph tracker to detect Ralph loops and todos this._ralphTracker.processTerminalData(data); - // Forward to Spawn detector to detect spawn1337 protocol tags - this._spawnDetector.processTerminalData(data); - // Parse token count from status line (e.g., "123.4k tokens" or "5234 tokens") this.parseTokensFromStatusLine(data); @@ -958,6 +920,7 @@ export class Session extends EventEmitter { TERM: 'xterm-256color', CLAUDEMAN_SCREEN: '1', CLAUDEMAN_SESSION_ID: this.id, + CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000', }, }); } @@ -1067,6 +1030,7 @@ export class Session extends EventEmitter { // Inform Claude it's running within Claudeman CLAUDEMAN_SCREEN: '1', CLAUDEMAN_SESSION_ID: this.id, + CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000', }, }); @@ -1460,14 +1424,6 @@ export class Session extends EventEmitter { this._ralphHandlers = null; } - // Remove SpawnDetector handlers - if (this._spawnHandlers) { - this._spawnDetector.off('spawnRequested', this._spawnHandlers.spawnRequested); - this._spawnDetector.off('statusRequested', this._spawnHandlers.statusRequested); - this._spawnDetector.off('cancelRequested', this._spawnHandlers.cancelRequested); - this._spawnDetector.off('messageToChild', this._spawnHandlers.messageToChild); - this._spawnHandlers = null; - } } /** diff --git a/src/spawn-detector.ts b/src/spawn-detector.ts deleted file mode 100644 index 402dd9df..00000000 --- a/src/spawn-detector.ts +++ /dev/null @@ -1,292 +0,0 @@ -/** - * @fileoverview Spawn Detector - Detects spawn1337 tags in terminal output. - * - * Monitors terminal output for spawn protocol patterns: - * - filename.md - Agent spawn request - * - - Status query - * - - Cancel request - * - content - Message to child - * - * Same architecture as ralph-tracker.ts: line-buffered, auto-enabling, - * debounced events, pre-compiled patterns. - * - * @module spawn-detector - */ - -import { EventEmitter } from 'node:events'; -import { SpawnTrackerState, createInitialSpawnTrackerState } from './spawn-types.js'; - -// ========== Configuration Constants ========== - -/** Debounce interval for state event emissions (ms) */ -const EVENT_DEBOUNCE_MS = 50; - -/** Maximum line buffer size to prevent unbounded growth */ -const MAX_LINE_BUFFER_SIZE = 64 * 1024; - -// ========== Pre-compiled Regex Patterns ========== - -/** Matches spawn request tags: filename.md */ -const SPAWN_TAG_PATTERN = /([^<]+)<\/spawn1337>/; - -/** Quick check string before running regex */ -const SPAWN_QUICK_CHECK = 'spawn1337'; - -/** Matches status query: */ -const SPAWN_STATUS_PATTERN = //; - -/** Matches cancel request: */ -const SPAWN_CANCEL_PATTERN = //; - -/** Matches message to child: content */ -const SPAWN_MESSAGE_PATTERN = /([\s\S]*?)<\/spawn1337-message>/; - -/** Removes ANSI escape codes from terminal output */ -const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g; - -// ========== Event Types ========== - -/** - * Events emitted by SpawnDetector - */ -export interface SpawnDetectorEvents { - /** Emitted when a spawn request tag is detected */ - spawnRequested: (filePath: string, rawLine: string) => void; - /** Emitted when a status query is detected */ - statusRequested: (agentId: string) => void; - /** Emitted when a cancel request is detected */ - cancelRequested: (agentId: string) => void; - /** Emitted when a message to child is detected */ - messageToChild: (agentId: string, content: string) => void; - /** Emitted when tracker state changes */ - stateUpdate: (state: SpawnTrackerState) => void; -} - -/** - * SpawnDetector - Parses terminal output to detect spawn1337 protocol tags. - * - * This class monitors Claude Code session output to detect agent spawn requests - * and related communication patterns. It auto-enables when any spawn1337 pattern - * is first detected, reducing overhead for sessions not using the spawn protocol. - * - * ## Pattern Detection - * - * 1. **Spawn Request**: `path/to/task.md` - * 2. **Status Query**: `` - * 3. **Cancel Request**: `` - * 4. **Message**: `content` - * - * @extends EventEmitter - */ -export class SpawnDetector extends EventEmitter { - /** Whether the detector is actively monitoring output */ - private _enabled: boolean = false; - - /** Buffer for incomplete lines from terminal data */ - private _lineBuffer: string = ''; - - /** Current tracker state */ - private _state: SpawnTrackerState; - - /** Debounce timer for state events */ - private _stateUpdateTimer: NodeJS.Timeout | null = null; - - /** Flag indicating pending state update emission */ - private _stateUpdatePending: boolean = false; - - constructor() { - super(); - this._state = createInitialSpawnTrackerState(); - } - - /** - * Whether the detector is enabled and actively monitoring output. - */ - get enabled(): boolean { - return this._enabled; - } - - /** - * Get a copy of the current tracker state. - */ - get state(): SpawnTrackerState { - return { ...this._state }; - } - - /** - * Enable the detector to start monitoring terminal output. - */ - enable(): void { - if (!this._enabled) { - this._enabled = true; - this._state.enabled = true; - this.emitStateUpdateDebounced(); - } - } - - /** - * Disable the detector. - */ - disable(): void { - if (this._enabled) { - this._enabled = false; - this._state.enabled = false; - this.emitStateUpdateDebounced(); - } - } - - /** - * Reset all state. - */ - reset(): void { - this.clearDebounceTimers(); - this._enabled = false; - this._lineBuffer = ''; - this._state = createInitialSpawnTrackerState(); - this.emit('stateUpdate', this.state); - } - - /** - * Update state from orchestrator data. - * Called by server.ts when orchestrator state changes. - */ - updateState(state: Partial): void { - Object.assign(this._state, state); - this.emitStateUpdateDebounced(); - } - - /** - * Process raw terminal data to detect spawn patterns. - * - * @param data - Raw terminal data (may include ANSI codes) - */ - processTerminalData(data: string): void { - // Remove ANSI escape codes - const cleanData = data.replace(ANSI_ESCAPE_PATTERN, ''); - - // Buffer data for line-based processing - this._lineBuffer += cleanData; - - // Prevent unbounded line buffer growth - if (this._lineBuffer.length > MAX_LINE_BUFFER_SIZE) { - this._lineBuffer = this._lineBuffer.slice(-MAX_LINE_BUFFER_SIZE / 2); - } - - // Quick pre-check: if no spawn patterns in buffer, just drain lines - if (!this._lineBuffer.includes(SPAWN_QUICK_CHECK)) { - const lines = this._lineBuffer.split('\n'); - this._lineBuffer = lines.pop() || ''; - return; - } - - // Auto-enable on first spawn pattern detection - if (!this._enabled) { - this.enable(); - } - - // Process complete lines - const lines = this._lineBuffer.split('\n'); - this._lineBuffer = lines.pop() || ''; - - for (const line of lines) { - this.processLine(line); - } - - // Also check the full chunk for multi-line patterns (message tag can span lines) - this.checkMultiLinePatterns(cleanData); - } - - /** - * Process a single line for spawn patterns. - */ - private processLine(line: string): void { - const trimmed = line.trim(); - if (!trimmed || !trimmed.includes(SPAWN_QUICK_CHECK)) return; - - // Check spawn request: filename.md - const spawnMatch = trimmed.match(SPAWN_TAG_PATTERN); - if (spawnMatch) { - const filePath = spawnMatch[1].trim(); - this._state.totalSpawned++; - this.emit('spawnRequested', filePath, trimmed); - this.emitStateUpdateDebounced(); - return; - } - - // Check status query: - const statusMatch = trimmed.match(SPAWN_STATUS_PATTERN); - if (statusMatch) { - this.emit('statusRequested', statusMatch[1]); - return; - } - - // Check cancel request: - const cancelMatch = trimmed.match(SPAWN_CANCEL_PATTERN); - if (cancelMatch) { - this.emit('cancelRequested', cancelMatch[1]); - return; - } - - // Check message (single line): content - const msgMatch = trimmed.match(SPAWN_MESSAGE_PATTERN); - if (msgMatch) { - this.emit('messageToChild', msgMatch[1], msgMatch[2]); - return; - } - } - - /** - * Check for patterns that might span multiple lines. - * The message tag content can be multiline. - */ - private checkMultiLinePatterns(data: string): void { - if (!data.includes('spawn1337-message')) return; - - const msgMatch = data.match(SPAWN_MESSAGE_PATTERN); - if (msgMatch) { - this.emit('messageToChild', msgMatch[1], msgMatch[2]); - } - } - - /** - * Emit stateUpdate with debouncing. - */ - private emitStateUpdateDebounced(): void { - this._stateUpdatePending = true; - if (this._stateUpdateTimer) { - clearTimeout(this._stateUpdateTimer); - } - this._stateUpdateTimer = setTimeout(() => { - if (this._stateUpdatePending) { - this._stateUpdatePending = false; - this._stateUpdateTimer = null; - this.emit('stateUpdate', this.state); - } - }, EVENT_DEBOUNCE_MS); - } - - /** - * Flush any pending debounced events immediately. - */ - flushPendingEvents(): void { - if (this._stateUpdatePending) { - this._stateUpdatePending = false; - if (this._stateUpdateTimer) { - clearTimeout(this._stateUpdateTimer); - this._stateUpdateTimer = null; - } - this.emit('stateUpdate', this.state); - } - } - - /** - * Clear all debounce timers. - */ - private clearDebounceTimers(): void { - if (this._stateUpdateTimer) { - clearTimeout(this._stateUpdateTimer); - this._stateUpdateTimer = null; - } - this._stateUpdatePending = false; - } -} diff --git a/src/spawn-types.ts b/src/spawn-types.ts index f989fa71..5043712b 100644 --- a/src/spawn-types.ts +++ b/src/spawn-types.ts @@ -212,7 +212,7 @@ export interface AgentStatusReport { dependencyStatus: 'waiting' | 'ready' | 'n/a'; } -// ========== Tracker State (for SpawnDetector) ========== +// ========== Tracker State (for SpawnOrchestrator) ========== export interface SpawnTrackerState { enabled: boolean; diff --git a/src/templates/case-template.md b/src/templates/case-template.md index 14a3575a..2996eb77 100644 --- a/src/templates/case-template.md +++ b/src/templates/case-template.md @@ -357,90 +357,6 @@ Instead: Fix the issue, verify, then complete. For time-aware loops: generate mo --- -## Spawn1337: Autonomous Agent Spawning - -You can spawn autonomous child agents to handle subtasks in parallel. Each agent runs in its own Claude session with full capabilities. - -### How to Spawn an Agent - -1. **Write a task spec file** (anywhere in your working directory): - -```markdown ---- -agentId: my-agent-001 -name: My Research Agent -type: explore -priority: normal -maxTokens: 150000 -maxCost: 0.50 -timeoutMinutes: 15 -canModifyParentFiles: false -contextFiles: [src/auth.ts, src/types.ts] -completionPhrase: RESEARCH_DONE -outputFormat: structured ---- - -Your task instructions here. Be specific about what you want the agent to do -and what output format you expect. -``` - -2. **Output the spawn tag** to trigger the orchestrator: - -``` -path/to/task-spec.md -``` - -The orchestrator will read the file, create the agent's workspace, and start a new Claude session. - -### Task Spec Fields - -| Field | Required | Default | Description | -|-------|----------|---------|-------------| -| `agentId` | Yes | - | Unique identifier for the agent | -| `name` | Yes | - | Human-readable name | -| `type` | No | `general` | `explore\|implement\|test\|review\|refactor\|research\|generate\|fix\|general` | -| `priority` | No | `normal` | `low\|normal\|high\|critical` | -| `maxTokens` | No | 150000 | Token budget | -| `maxCost` | No | 0.50 | Cost budget in USD | -| `timeoutMinutes` | No | 30 | Max runtime (max: 120) | -| `canModifyParentFiles` | No | `false` | Whether agent can edit parent project files | -| `contextFiles` | No | `[]` | Files to symlink into agent workspace | -| `dependsOn` | No | `[]` | Agent IDs that must complete first | -| `completionPhrase` | No | `AGENT_DONE` | Phrase agent outputs when finished | -| `outputFormat` | No | `structured` | `markdown\|json\|code\|structured\|freeform` | - -### Monitoring and Communication - -``` - -``` -Query an agent's current status and progress. - -``` - -Please also check the error handling in auth.ts - -``` -Send a message to a running agent (written to its comms directory). - -``` - -``` -Cancel a running agent (graceful shutdown). - -### Resource Limits - -- Max concurrent agents: 5 -- Max spawn depth: 3 (agents can spawn children) -- Budget warning at 80%, shutdown at 100%, force kill at 110% -- Default timeout: 30 min, max: 120 min - -### Agent Results - -When an agent completes, its result is written to `spawn-comms/result.md` in its workspace. The orchestrator notifies you via SSE events. Results include YAML frontmatter with status, summary, and files changed. - ---- - ## Code Standards ### Before Writing diff --git a/src/web/server.ts b/src/web/server.ts index 8c4533a4..859b225a 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -1036,6 +1036,9 @@ export class WebServer extends EventEmitter { const claudeMd = generateClaudeMd(name, description || '', templatePath); writeFileSync(join(casePath, 'CLAUDE.md'), claudeMd); + // Write .mcp.json for Claude Code to discover spawn tools + this.writeMcpConfig(casePath); + this.broadcast('case:created', { name, path: casePath }); return { success: true, case: { name, path: casePath } }; @@ -1173,6 +1176,9 @@ export class WebServer extends EventEmitter { const claudeMd = generateClaudeMd(caseName, '', templatePath); writeFileSync(join(casePath, 'CLAUDE.md'), claudeMd); + // Write .mcp.json for Claude Code to discover spawn tools + this.writeMcpConfig(casePath); + this.broadcast('case:created', { name: caseName, path: casePath }); } catch (err) { return { success: false, error: `Failed to create case: ${getErrorMessage(err)}` }; @@ -1636,34 +1642,6 @@ export class WebServer extends EventEmitter { this.broadcast('session:ralphCompletionDetected', { sessionId: session.id, phrase }); }); - // Spawn1337 protocol events - session.on('spawnRequested', (filePath: string) => { - this.spawnOrchestrator.handleSpawnRequest( - filePath, - session.id, - session.workingDir, - session.parentAgentId ? 1 : 0 // Simple depth tracking - ).catch(err => { - console.error(`[Server] Spawn request failed for session ${session.id}:`, getErrorMessage(err)); - }); - }); - - session.on('spawnStatusRequested', (agentId: string) => { - const status = this.spawnOrchestrator.getAgentStatus(agentId); - this.broadcast('spawn:statusResponse', { sessionId: session.id, agentId, status }); - }); - - session.on('spawnCancelRequested', (agentId: string) => { - this.spawnOrchestrator.cancelAgent(agentId, 'Cancelled by parent session').catch(err => { - console.error(`[Server] Spawn cancel failed:`, getErrorMessage(err)); - }); - }); - - session.on('spawnMessageToChild', (agentId: string, content: string) => { - this.spawnOrchestrator.sendMessageToAgent(agentId, content).catch(err => { - console.error(`[Server] Spawn message failed:`, getErrorMessage(err)); - }); - }); } private setupRespawnListeners(sessionId: string, controller: RespawnController): void { @@ -1829,6 +1807,23 @@ export class WebServer extends EventEmitter { return undefined; } + /** + * Write .mcp.json to a case directory for Claude Code to discover spawn MCP tools. + */ + private writeMcpConfig(casePath: string): void { + const projectRoot = join(__dirname, '..', '..'); + const mcpServerPath = join(projectRoot, 'dist', 'mcp-server.js'); + const mcpConfig = { + mcpServers: { + 'claudeman-spawn': { + command: 'node', + args: [mcpServerPath], + }, + }, + }; + writeFileSync(join(casePath, '.mcp.json'), JSON.stringify(mcpConfig, null, 2) + '\n'); + } + private async startScheduledRun(prompt: string, workingDir: string, durationMinutes: number): Promise { const id = uuidv4(); const now = Date.now(); @@ -2164,6 +2159,9 @@ export class WebServer extends EventEmitter { await this.app.listen({ port: this.port, host: '0.0.0.0' }); console.log(`Claudeman web interface running at http://localhost:${this.port}`); + // Set API URL for child processes (MCP server, spawned sessions) + process.env.CLAUDEMAN_API_URL = `http://localhost:${this.port}`; + // Start scheduled runs cleanup timer this.scheduledCleanupTimer = setInterval(() => { this.cleanupScheduledRuns(); diff --git a/test/mcp-server.test.ts b/test/mcp-server.test.ts new file mode 100644 index 00000000..e94ba491 --- /dev/null +++ b/test/mcp-server.test.ts @@ -0,0 +1,224 @@ +import { describe, it, expect } from 'vitest'; +import { buildTaskSpec } from '../src/mcp-server.js'; + +describe('mcp-server', () => { + describe('buildTaskSpec', () => { + it('should build minimal task spec with required fields only', () => { + const result = buildTaskSpec({ + agentId: 'test-001', + name: 'Test Agent', + instructions: 'Do something useful.', + }); + + expect(result).toContain('---'); + expect(result).toContain('agentId: test-001'); + expect(result).toContain('name: Test Agent'); + expect(result).toContain('Do something useful.'); + + // Should not contain optional fields + expect(result).not.toContain('type:'); + expect(result).not.toContain('priority:'); + expect(result).not.toContain('maxTokens:'); + expect(result).not.toContain('maxCost:'); + expect(result).not.toContain('timeoutMinutes:'); + expect(result).not.toContain('contextFiles:'); + expect(result).not.toContain('dependsOn:'); + }); + + it('should include all optional fields when provided', () => { + const result = buildTaskSpec({ + agentId: 'full-agent', + name: 'Full Agent', + instructions: 'Complete task.', + type: 'implement', + priority: 'high', + maxTokens: 200000, + maxCost: 1.50, + timeoutMinutes: 60, + canModifyParentFiles: true, + contextFiles: ['src/auth.ts', 'src/types.ts'], + dependsOn: ['agent-a', 'agent-b'], + completionPhrase: 'FULL_DONE', + outputFormat: 'structured', + successCriteria: 'All tests pass', + workingDir: '/tmp/workspace', + }); + + expect(result).toContain('agentId: full-agent'); + expect(result).toContain('name: Full Agent'); + expect(result).toContain('type: implement'); + expect(result).toContain('priority: high'); + expect(result).toContain('maxTokens: 200000'); + expect(result).toContain('maxCost: 1.5'); + expect(result).toContain('timeoutMinutes: 60'); + expect(result).toContain('canModifyParentFiles: true'); + expect(result).toContain('contextFiles: [src/auth.ts, src/types.ts]'); + expect(result).toContain('dependsOn: [agent-a, agent-b]'); + expect(result).toContain('completionPhrase: FULL_DONE'); + expect(result).toContain('outputFormat: structured'); + expect(result).toContain('successCriteria: "All tests pass"'); + expect(result).toContain('workingDir: /tmp/workspace'); + expect(result).toContain('Complete task.'); + }); + + it('should produce valid YAML frontmatter structure', () => { + const result = buildTaskSpec({ + agentId: 'yaml-test', + name: 'YAML Test', + instructions: 'Body content here.', + }); + + const lines = result.split('\n'); + expect(lines[0]).toBe('---'); + + // Find closing --- + const closingIndex = lines.indexOf('---', 1); + expect(closingIndex).toBeGreaterThan(0); + + // Body should come after closing --- + const body = lines.slice(closingIndex + 1).join('\n').trim(); + expect(body).toBe('Body content here.'); + }); + + it('should escape double quotes in successCriteria', () => { + const result = buildTaskSpec({ + agentId: 'escape-test', + name: 'Escape Test', + instructions: 'test', + successCriteria: 'Output "hello" correctly', + }); + + expect(result).toContain('successCriteria: "Output \\"hello\\" correctly"'); + }); + + it('should handle empty context files array', () => { + const result = buildTaskSpec({ + agentId: 'empty-ctx', + name: 'Empty Context', + instructions: 'test', + contextFiles: [], + }); + + expect(result).not.toContain('contextFiles:'); + }); + + it('should handle empty dependsOn array', () => { + const result = buildTaskSpec({ + agentId: 'empty-deps', + name: 'Empty Deps', + instructions: 'test', + dependsOn: [], + }); + + expect(result).not.toContain('dependsOn:'); + }); + + it('should handle canModifyParentFiles: false', () => { + const result = buildTaskSpec({ + agentId: 'no-modify', + name: 'No Modify', + instructions: 'test', + canModifyParentFiles: false, + }); + + expect(result).toContain('canModifyParentFiles: false'); + }); + + it('should handle multiline instructions', () => { + const instructions = `# Step 1 +Do this first. + +# Step 2 +Then do this. + +## Notes +- Important detail +- Another detail`; + + const result = buildTaskSpec({ + agentId: 'multiline', + name: 'Multiline', + instructions, + }); + + expect(result).toContain('# Step 1'); + expect(result).toContain('Do this first.'); + expect(result).toContain('# Step 2'); + expect(result).toContain('- Important detail'); + }); + + it('should be parseable by parseYamlFrontmatter', async () => { + const { parseYamlFrontmatter } = await import('../src/spawn-types.js'); + + const result = buildTaskSpec({ + agentId: 'parse-test', + name: 'Parse Test', + instructions: 'Verify parsing works.', + type: 'explore', + priority: 'high', + maxTokens: 100000, + timeoutMinutes: 15, + }); + + const parsed = parseYamlFrontmatter(result); + expect(parsed).not.toBeNull(); + expect(parsed!.frontmatter.agentId).toBe('parse-test'); + expect(parsed!.frontmatter.name).toBe('Parse Test'); + expect(parsed!.frontmatter.type).toBe('explore'); + expect(parsed!.frontmatter.priority).toBe('high'); + expect(parsed!.frontmatter.maxTokens).toBe(100000); + expect(parsed!.frontmatter.timeoutMinutes).toBe(15); + expect(parsed!.body).toBe('Verify parsing works.'); + }); + + it('should produce spec parseable by parseTaskSpecFile', async () => { + const { parseTaskSpecFile } = await import('../src/spawn-types.js'); + + const result = buildTaskSpec({ + agentId: 'full-spec', + name: 'Full Spec Agent', + instructions: 'Do the work.', + type: 'implement', + priority: 'critical', + maxTokens: 250000, + maxCost: 2.0, + timeoutMinutes: 45, + canModifyParentFiles: true, + completionPhrase: 'SPEC_DONE', + outputFormat: 'json', + }); + + const parsed = parseTaskSpecFile(result, 'fallback-id'); + expect(parsed).not.toBeNull(); + expect(parsed!.spec.agentId).toBe('full-spec'); + expect(parsed!.spec.name).toBe('Full Spec Agent'); + expect(parsed!.spec.type).toBe('implement'); + expect(parsed!.spec.priority).toBe('critical'); + expect(parsed!.spec.maxTokens).toBe(250000); + expect(parsed!.spec.maxCost).toBe(2.0); + expect(parsed!.spec.timeoutMinutes).toBe(45); + expect(parsed!.spec.canModifyParentFiles).toBe(true); + expect(parsed!.spec.completionPhrase).toBe('SPEC_DONE'); + expect(parsed!.spec.outputFormat).toBe('json'); + expect(parsed!.instructions).toBe('Do the work.'); + }); + + it('should handle contextFiles in parseable format', async () => { + const { parseYamlFrontmatter } = await import('../src/spawn-types.js'); + + const result = buildTaskSpec({ + agentId: 'ctx-parse', + name: 'Context Parse', + instructions: 'test', + contextFiles: ['src/foo.ts', 'src/bar.ts'], + }); + + const parsed = parseYamlFrontmatter(result); + expect(parsed).not.toBeNull(); + expect(Array.isArray(parsed!.frontmatter.contextFiles)).toBe(true); + const files = parsed!.frontmatter.contextFiles as string[]; + expect(files).toContain('src/foo.ts'); + expect(files).toContain('src/bar.ts'); + }); + }); +}); diff --git a/test/spawn-detector.test.ts b/test/spawn-detector.test.ts deleted file mode 100644 index 2fba749f..00000000 --- a/test/spawn-detector.test.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { SpawnDetector } from '../src/spawn-detector.js'; - -describe('SpawnDetector', () => { - let detector: SpawnDetector; - - beforeEach(() => { - detector = new SpawnDetector(); - }); - - describe('Initialization', () => { - it('should start disabled', () => { - expect(detector.enabled).toBe(false); - }); - - it('should start with initial state', () => { - const state = detector.state; - expect(state.enabled).toBe(false); - expect(state.activeCount).toBe(0); - expect(state.totalSpawned).toBe(0); - }); - }); - - describe('Auto-Enable', () => { - it('should auto-enable on spawn tag detection', () => { - detector.processTerminalData('task.md\n'); - expect(detector.enabled).toBe(true); - }); - - it('should not enable on unrelated data', () => { - detector.processTerminalData('Hello world\nsome output\n'); - expect(detector.enabled).toBe(false); - }); - - it('should auto-enable on status tag', () => { - detector.processTerminalData('\n'); - expect(detector.enabled).toBe(true); - }); - - it('should auto-enable on cancel tag', () => { - detector.processTerminalData('\n'); - expect(detector.enabled).toBe(true); - }); - }); - - describe('Spawn Request Detection', () => { - it('should detect spawn tag and emit event', () => { - const handler = vi.fn(); - detector.on('spawnRequested', handler); - - detector.processTerminalData('tasks/auth-explore.md\n'); - - expect(handler).toHaveBeenCalledWith('tasks/auth-explore.md', expect.any(String)); - }); - - it('should detect spawn tag with path containing slashes', () => { - const handler = vi.fn(); - detector.on('spawnRequested', handler); - - detector.processTerminalData('src/tasks/deep/nested.md\n'); - - expect(handler).toHaveBeenCalledWith('src/tasks/deep/nested.md', expect.any(String)); - }); - - it('should handle spawn tag with surrounding text', () => { - const handler = vi.fn(); - detector.on('spawnRequested', handler); - - detector.processTerminalData('Starting agent: task.md done\n'); - - expect(handler).toHaveBeenCalledWith('task.md', expect.any(String)); - }); - - it('should increment totalSpawned count', () => { - detector.processTerminalData('task1.md\n'); - detector.processTerminalData('task2.md\n'); - detector.flushPendingEvents(); - - expect(detector.state.totalSpawned).toBe(2); - }); - - it('should handle multiple spawn tags in one chunk', () => { - const handler = vi.fn(); - detector.on('spawnRequested', handler); - - detector.processTerminalData( - 'task1.md\ntask2.md\n' - ); - - expect(handler).toHaveBeenCalledTimes(2); - }); - - it('should strip ANSI codes before parsing', () => { - const handler = vi.fn(); - detector.on('spawnRequested', handler); - - detector.processTerminalData('\x1b[32mtask.md\x1b[0m\n'); - - expect(handler).toHaveBeenCalledWith('task.md', expect.any(String)); - }); - - it('should trim whitespace from file path', () => { - const handler = vi.fn(); - detector.on('spawnRequested', handler); - - detector.processTerminalData(' task.md \n'); - - expect(handler).toHaveBeenCalledWith('task.md', expect.any(String)); - }); - }); - - describe('Status Request Detection', () => { - it('should detect status query and emit event', () => { - const handler = vi.fn(); - detector.on('statusRequested', handler); - - detector.processTerminalData('\n'); - - expect(handler).toHaveBeenCalledWith('auth-001'); - }); - - it('should handle status with complex agent ID', () => { - const handler = vi.fn(); - detector.on('statusRequested', handler); - - detector.processTerminalData('\n'); - - expect(handler).toHaveBeenCalledWith('my-complex-agent-123'); - }); - }); - - describe('Cancel Request Detection', () => { - it('should detect cancel request and emit event', () => { - const handler = vi.fn(); - detector.on('cancelRequested', handler); - - detector.processTerminalData('\n'); - - expect(handler).toHaveBeenCalledWith('test-agent'); - }); - }); - - describe('Message Detection', () => { - it('should detect single-line message', () => { - const handler = vi.fn(); - detector.on('messageToChild', handler); - - detector.processTerminalData( - 'Focus on the JWT flow\n' - ); - - expect(handler).toHaveBeenCalledWith('agent-001', 'Focus on the JWT flow'); - }); - - it('should detect multi-line message via checkMultiLinePatterns', () => { - const handler = vi.fn(); - detector.on('messageToChild', handler); - - const multiline = 'Line 1\nLine 2\nLine 3'; - detector.processTerminalData(multiline + '\n'); - - expect(handler).toHaveBeenCalledWith('agent-001', 'Line 1\nLine 2\nLine 3'); - }); - }); - - describe('Enable/Disable', () => { - it('should emit stateUpdate when enabled', async () => { - const handler = vi.fn(); - detector.on('stateUpdate', handler); - - detector.enable(); - detector.flushPendingEvents(); - - expect(handler).toHaveBeenCalled(); - expect(handler.mock.calls[0][0].enabled).toBe(true); - }); - - it('should emit stateUpdate when disabled', () => { - detector.enable(); - detector.flushPendingEvents(); - - const handler = vi.fn(); - detector.on('stateUpdate', handler); - - detector.disable(); - detector.flushPendingEvents(); - - expect(handler).toHaveBeenCalled(); - expect(handler.mock.calls[0][0].enabled).toBe(false); - }); - }); - - describe('Reset', () => { - it('should reset all state', () => { - detector.processTerminalData('task.md\n'); - detector.flushPendingEvents(); - expect(detector.enabled).toBe(true); - - detector.reset(); - - expect(detector.enabled).toBe(false); - expect(detector.state.totalSpawned).toBe(0); - }); - }); - - describe('State Update', () => { - it('should allow external state updates', () => { - detector.updateState({ activeCount: 3, queuedCount: 2 }); - detector.flushPendingEvents(); - - expect(detector.state.activeCount).toBe(3); - expect(detector.state.queuedCount).toBe(2); - }); - }); - - describe('Line Buffer', () => { - it('should handle data split across chunks', () => { - const handler = vi.fn(); - detector.on('spawnRequested', handler); - - // Send in two chunks (split in the middle of the tag) - detector.processTerminalData('task.md\n'); - - expect(handler).toHaveBeenCalledWith('task.md', expect.any(String)); - }); - - it('should handle partial lines without emitting', () => { - const handler = vi.fn(); - detector.on('spawnRequested', handler); - - // No newline - data stays in buffer - detector.processTerminalData('task.md'); - - // Should not emit until newline - expect(handler).not.toHaveBeenCalled(); - }); - - it('should emit on subsequent newline', () => { - const handler = vi.fn(); - detector.on('spawnRequested', handler); - - detector.processTerminalData('task.md'); - detector.processTerminalData('\n'); - - expect(handler).toHaveBeenCalledWith('task.md', expect.any(String)); - }); - }); -});