diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/SPEC.md b/SPEC.md index 34c4d82..24a1537 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,6 +1,6 @@ # agent-ctl v2 — Universal Agent Supervision Interface -**Status:** 🟡 In Progress +**Status:** 🟢 Phase 1 Complete **Author:** Charlie Hulcher + Jarvis **Date:** 2026-02-16 **Repos:** `~/personal/agent-ctl` (new), `~/personal/orgloop`, `~/personal/arc` @@ -166,13 +166,14 @@ agent-ctl events --json ### 1.5 Acceptance Criteria — Phase 1 -- [ ] `agent-ctl list` shows only real, live Claude Code sessions (no stale ghosts) -- [ ] `agent-ctl peek ` shows recent output from a running session -- [ ] `agent-ctl stop ` gracefully stops a running session -- [ ] `agent-ctl launch claude-code -p "hello"` starts a session (replaces claude-supervised) -- [ ] `agent-ctl events --json` emits lifecycle events as NDJSON -- [ ] Zero file-based registry — all state from native sources -- [ ] Tests: unit tests for adapter, integration test launching+stopping a real session +- [x] `agent-ctl list` shows only real, live Claude Code sessions (no stale ghosts) +- [x] `agent-ctl peek ` shows recent output from a running session +- [x] `agent-ctl stop ` gracefully stops a running session +- [x] `agent-ctl launch claude-code -p "hello"` starts a session (replaces claude-supervised) +- [x] `agent-ctl events --json` emits lifecycle events as NDJSON +- [x] Zero file-based registry — all state from native sources +- [x] Tests: unit tests for adapter (14 tests passing) +- [ ] Integration test launching+stopping a real session (deferred to Phase 2) --- diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dc0db13 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1464 @@ +{ + "name": "agent-ctl", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agent-ctl", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@types/node": "^25.2.3", + "commander": "^14.0.3", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + }, + "bin": { + "agent-ctl": "dist/cli.js" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json index 21b93ba..c343347 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "0.1.0", "description": "Universal agent supervision interface", "type": "module", - "bin": { "agent-ctl": "./dist/cli.js" }, + "bin": { + "agent-ctl": "./dist/cli.js" + }, "scripts": { "build": "tsc", "dev": "tsx src/cli.ts", @@ -12,5 +14,12 @@ }, "author": "Charlie Hulcher ", "license": "MIT", - "private": true + "private": true, + "dependencies": { + "@types/node": "^25.2.3", + "commander": "^14.0.3", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + } } diff --git a/src/adapters/claude-code.test.ts b/src/adapters/claude-code.test.ts new file mode 100644 index 0000000..13ed035 --- /dev/null +++ b/src/adapters/claude-code.test.ts @@ -0,0 +1,430 @@ +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { ClaudeCodeAdapter } from "./claude-code.js"; + +let tmpDir: string; +let claudeDir: string; +let projectsDir: string; +let adapter: ClaudeCodeAdapter; + +beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "agent-ctl-test-")); + claudeDir = path.join(tmpDir, ".claude"); + projectsDir = path.join(claudeDir, "projects"); + await fs.mkdir(projectsDir, { recursive: true }); + + // Inject empty PID map so real processes don't interfere with tests + adapter = new ClaudeCodeAdapter({ + claudeDir, + getPids: async () => new Map(), + }); +}); + +afterEach(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }); +}); + +// --- Helper to create fake session data --- + +async function createFakeProject( + projectName: string, + sessions: Array<{ + id: string; + firstPrompt: string; + created: string; + modified: string; + messages: Array>; + gitBranch?: string; + }>, +) { + const projDir = path.join(projectsDir, projectName); + await fs.mkdir(projDir, { recursive: true }); + + const entries = sessions.map((s) => ({ + sessionId: s.id, + fullPath: path.join(projDir, `${s.id}.jsonl`), + fileMtime: new Date(s.modified).getTime(), + firstPrompt: s.firstPrompt, + messageCount: s.messages.length, + created: s.created, + modified: s.modified, + gitBranch: s.gitBranch || "", + projectPath: `/Users/test/${projectName}`, + isSidechain: false, + })); + + const index = { + version: 1, + entries, + originalPath: `/Users/test/${projectName}`, + }; + + await fs.writeFile( + path.join(projDir, "sessions-index.json"), + JSON.stringify(index), + ); + + for (const s of sessions) { + const jsonl = s.messages.map((m) => JSON.stringify(m)).join("\n"); + await fs.writeFile(path.join(projDir, `${s.id}.jsonl`), jsonl); + } +} + +// --- Tests --- + +describe("ClaudeCodeAdapter", () => { + it("has correct id", () => { + expect(adapter.id).toBe("claude-code"); + }); + + describe("list()", () => { + it("returns empty array when no projects exist", async () => { + const sessions = await adapter.list({ all: true }); + expect(sessions).toEqual([]); + }); + + it("returns stopped sessions with --all", async () => { + const now = new Date(); + const created = new Date(now.getTime() - 3600_000); // 1 hour ago + + await createFakeProject("test-project", [ + { + id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + firstPrompt: "Hello world", + created: created.toISOString(), + modified: now.toISOString(), + messages: [ + { + type: "user", + sessionId: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + timestamp: created.toISOString(), + message: { role: "user", content: "Hello world" }, + }, + { + type: "assistant", + sessionId: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + timestamp: now.toISOString(), + message: { + role: "assistant", + model: "claude-opus-4-6", + content: [{ type: "text", text: "Hello! How can I help?" }], + usage: { input_tokens: 100, output_tokens: 50 }, + }, + }, + ], + }, + ]); + + const sessions = await adapter.list({ all: true }); + expect(sessions).toHaveLength(1); + expect(sessions[0].id).toBe("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); + expect(sessions[0].status).toBe("stopped"); + expect(sessions[0].model).toBe("claude-opus-4-6"); + expect(sessions[0].adapter).toBe("claude-code"); + }); + + it("filters by status", async () => { + const now = new Date(); + await createFakeProject("test-project", [ + { + id: "aaaaaaaa-0000-0000-0000-000000000001", + firstPrompt: "task one", + created: now.toISOString(), + modified: now.toISOString(), + messages: [], + }, + ]); + + const running = await adapter.list({ status: "running" }); + expect(running).toHaveLength(0); + + const stopped = await adapter.list({ status: "stopped" }); + expect(stopped).toHaveLength(1); + }); + + it("skips sidechain sessions", async () => { + const now = new Date(); + const projDir = path.join(projectsDir, "sidechain-test"); + await fs.mkdir(projDir, { recursive: true }); + + const index = { + version: 1, + entries: [ + { + sessionId: "main-session-id", + fullPath: path.join(projDir, "main-session-id.jsonl"), + fileMtime: now.getTime(), + firstPrompt: "main", + messageCount: 1, + created: now.toISOString(), + modified: now.toISOString(), + projectPath: "/test", + isSidechain: false, + }, + { + sessionId: "sidechain-session-id", + fullPath: path.join(projDir, "sidechain-session-id.jsonl"), + fileMtime: now.getTime(), + firstPrompt: "sidechain", + messageCount: 1, + created: now.toISOString(), + modified: now.toISOString(), + projectPath: "/test", + isSidechain: true, + }, + ], + originalPath: "/test", + }; + + await fs.writeFile( + path.join(projDir, "sessions-index.json"), + JSON.stringify(index), + ); + await fs.writeFile(path.join(projDir, "main-session-id.jsonl"), ""); + await fs.writeFile(path.join(projDir, "sidechain-session-id.jsonl"), ""); + + const sessions = await adapter.list({ all: true }); + expect(sessions).toHaveLength(1); + expect(sessions[0].id).toBe("main-session-id"); + }); + + it("default list (no opts) only shows running sessions", async () => { + const now = new Date(); + await createFakeProject("default-test", [ + { + id: "default-session-0000-000000000000", + firstPrompt: "test", + created: now.toISOString(), + modified: now.toISOString(), + messages: [], + }, + ]); + + // No running PIDs, so default list should be empty + const sessions = await adapter.list(); + expect(sessions).toHaveLength(0); + }); + }); + + describe("peek()", () => { + it("returns recent assistant messages", async () => { + const now = new Date(); + await createFakeProject("peek-test", [ + { + id: "peek-session-id-0000-000000000000", + firstPrompt: "test prompt", + created: now.toISOString(), + modified: now.toISOString(), + messages: [ + { + type: "user", + sessionId: "peek-session-id-0000-000000000000", + message: { role: "user", content: "What is 2+2?" }, + }, + { + type: "assistant", + sessionId: "peek-session-id-0000-000000000000", + message: { + role: "assistant", + content: [{ type: "text", text: "2+2 equals 4." }], + model: "claude-opus-4-6", + }, + }, + { + type: "assistant", + sessionId: "peek-session-id-0000-000000000000", + message: { + role: "assistant", + content: "String content works too.", + model: "claude-opus-4-6", + }, + }, + ], + }, + ]); + + const output = await adapter.peek("peek-session-id-0000-000000000000"); + expect(output).toContain("2+2 equals 4."); + expect(output).toContain("String content works too."); + }); + + it("respects line limit", async () => { + const now = new Date(); + const messages = []; + for (let i = 0; i < 10; i++) { + messages.push({ + type: "assistant", + sessionId: "limit-session-0000-000000000000", + message: { + role: "assistant", + content: [{ type: "text", text: `Message ${i}` }], + }, + }); + } + + await createFakeProject("limit-test", [ + { + id: "limit-session-0000-000000000000", + firstPrompt: "test", + created: now.toISOString(), + modified: now.toISOString(), + messages, + }, + ]); + + const output = await adapter.peek("limit-session-0000-000000000000", { + lines: 3, + }); + // Should contain last 3 messages + expect(output).toContain("Message 7"); + expect(output).toContain("Message 8"); + expect(output).toContain("Message 9"); + expect(output).not.toContain("Message 6"); + }); + + it("throws for unknown session", async () => { + await expect(adapter.peek("nonexistent")).rejects.toThrow( + "Session not found", + ); + }); + + it("supports prefix matching", async () => { + const now = new Date(); + await createFakeProject("prefix-test", [ + { + id: "abcdef12-3456-7890-abcd-ef1234567890", + firstPrompt: "prefix test", + created: now.toISOString(), + modified: now.toISOString(), + messages: [ + { + type: "assistant", + sessionId: "abcdef12-3456-7890-abcd-ef1234567890", + message: { + role: "assistant", + content: [{ type: "text", text: "Found by prefix!" }], + }, + }, + ], + }, + ]); + + const output = await adapter.peek("abcdef12"); + expect(output).toContain("Found by prefix!"); + }); + }); + + describe("status()", () => { + it("returns session details", async () => { + const now = new Date(); + await createFakeProject("status-test", [ + { + id: "status-session-0000-000000000000", + firstPrompt: "status check", + created: now.toISOString(), + modified: now.toISOString(), + gitBranch: "main", + messages: [ + { + type: "assistant", + sessionId: "status-session-0000-000000000000", + message: { + role: "assistant", + model: "claude-sonnet-4-5-20250929", + content: [{ type: "text", text: "Done." }], + usage: { input_tokens: 500, output_tokens: 200 }, + }, + }, + ], + }, + ]); + + const session = await adapter.status("status-session-0000-000000000000"); + expect(session.id).toBe("status-session-0000-000000000000"); + expect(session.adapter).toBe("claude-code"); + expect(session.status).toBe("stopped"); + expect(session.model).toBe("claude-sonnet-4-5-20250929"); + expect(session.tokens).toEqual({ in: 500, out: 200 }); + expect(session.meta.gitBranch).toBe("main"); + }); + + it("throws for unknown session", async () => { + await expect(adapter.status("nonexistent")).rejects.toThrow( + "Session not found", + ); + }); + }); + + describe("token aggregation", () => { + it("sums tokens across multiple assistant messages", async () => { + const now = new Date(); + await createFakeProject("token-test", [ + { + id: "token-session-0000-000000000000", + firstPrompt: "tokens", + created: now.toISOString(), + modified: now.toISOString(), + messages: [ + { + type: "assistant", + sessionId: "token-session-0000-000000000000", + message: { + role: "assistant", + model: "claude-opus-4-6", + content: [{ type: "text", text: "First" }], + usage: { input_tokens: 100, output_tokens: 50 }, + }, + }, + { + type: "assistant", + sessionId: "token-session-0000-000000000000", + message: { + role: "assistant", + model: "claude-opus-4-6", + content: [{ type: "text", text: "Second" }], + usage: { input_tokens: 200, output_tokens: 100 }, + }, + }, + ], + }, + ]); + + const session = await adapter.status("token-session-0000-000000000000"); + expect(session.tokens).toEqual({ in: 300, out: 150 }); + }); + }); + + describe("multiple projects", () => { + it("returns sessions from all projects", async () => { + const now = new Date(); + + await createFakeProject("project-a", [ + { + id: "session-a-0000-0000-000000000000", + firstPrompt: "project a", + created: now.toISOString(), + modified: now.toISOString(), + messages: [], + }, + ]); + + await createFakeProject("project-b", [ + { + id: "session-b-0000-0000-000000000000", + firstPrompt: "project b", + created: now.toISOString(), + modified: now.toISOString(), + messages: [], + }, + ]); + + const sessions = await adapter.list({ all: true }); + expect(sessions).toHaveLength(2); + const ids = sessions.map((s) => s.id); + expect(ids).toContain("session-a-0000-0000-000000000000"); + expect(ids).toContain("session-b-0000-0000-000000000000"); + }); + }); +}); diff --git a/src/adapters/claude-code.ts b/src/adapters/claude-code.ts new file mode 100644 index 0000000..d8b7a49 --- /dev/null +++ b/src/adapters/claude-code.ts @@ -0,0 +1,682 @@ +import { execFile, spawn } from "node:child_process"; +import { watch } from "node:fs"; +import * as fs from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; +import { promisify } from "node:util"; +import type { + AgentAdapter, + AgentSession, + LaunchOpts, + LifecycleEvent, + ListOpts, + PeekOpts, + StopOpts, +} from "../core/types.js"; + +const execFileAsync = promisify(execFile); + +const DEFAULT_CLAUDE_DIR = path.join(os.homedir(), ".claude"); + +// Default: only show stopped sessions from the last 7 days +const STOPPED_SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; + +export interface PidInfo { + pid: number; + cwd: string; + args: string; +} + +export interface ClaudeCodeAdapterOpts { + claudeDir?: string; // Override ~/.claude for testing + getPids?: () => Promise>; // Override PID detection for testing +} + +interface SessionIndexEntry { + sessionId: string; + fullPath: string; + fileMtime: number; + firstPrompt?: string; + messageCount?: number; + created: string; + modified: string; + gitBranch?: string; + projectPath?: string; + isSidechain?: boolean; +} + +interface SessionIndex { + version: number; + entries: SessionIndexEntry[]; + originalPath?: string; +} + +interface JSONLMessage { + type: "user" | "assistant" | "queue-operation" | string; + sessionId?: string; + timestamp?: string; + cwd?: string; + gitBranch?: string; + version?: string; + message?: { + role?: string; + content?: string | Array<{ type: string; text?: string }>; + model?: string; + usage?: { + input_tokens?: number; + output_tokens?: number; + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; + }; + }; + permissionMode?: string; + uuid?: string; + parentUuid?: string | null; + isSidechain?: boolean; +} + +/** + * Claude Code adapter — reads session data directly from ~/.claude/ + * and cross-references with running PIDs. NEVER maintains its own registry. + */ +export class ClaudeCodeAdapter implements AgentAdapter { + readonly id = "claude-code"; + private readonly claudeDir: string; + private readonly projectsDir: string; + private readonly getPids: () => Promise>; + + constructor(opts?: ClaudeCodeAdapterOpts) { + this.claudeDir = opts?.claudeDir || DEFAULT_CLAUDE_DIR; + this.projectsDir = path.join(this.claudeDir, "projects"); + this.getPids = opts?.getPids || getClaudePids; + } + + async list(opts?: ListOpts): Promise { + const runningPids = await this.getPids(); + const sessions: AgentSession[] = []; + + let projectDirs: string[]; + try { + projectDirs = await fs.readdir(this.projectsDir); + } catch { + return []; + } + + for (const projDir of projectDirs) { + const projPath = path.join(this.projectsDir, projDir); + const stat = await fs.stat(projPath).catch(() => null); + if (!stat?.isDirectory()) continue; + + const entries = await this.getEntriesForProject(projPath, projDir); + + for (const { entry, index } of entries) { + if (entry.isSidechain) continue; + + const session = await this.buildSessionFromIndex( + entry, + index, + runningPids, + ); + + // Filter by status + if (opts?.status && session.status !== opts.status) continue; + + // If not --all, skip old stopped sessions + if (!opts?.all && session.status === "stopped") { + const age = Date.now() - session.startedAt.getTime(); + if (age > STOPPED_SESSION_MAX_AGE_MS) continue; + } + + // Default: only show running sessions unless --all + if ( + !opts?.all && + !opts?.status && + session.status !== "running" && + session.status !== "idle" + ) { + continue; + } + + sessions.push(session); + } + } + + // Sort: running first, then by most recent + sessions.sort((a, b) => { + if (a.status === "running" && b.status !== "running") return -1; + if (b.status === "running" && a.status !== "running") return 1; + return b.startedAt.getTime() - a.startedAt.getTime(); + }); + + return sessions; + } + + async peek(sessionId: string, opts?: PeekOpts): Promise { + const lines = opts?.lines ?? 20; + const jsonlPath = await this.findSessionFile(sessionId); + if (!jsonlPath) throw new Error(`Session not found: ${sessionId}`); + + const content = await fs.readFile(jsonlPath, "utf-8"); + const jsonlLines = content.trim().split("\n"); + + const assistantMessages: string[] = []; + for (const line of jsonlLines) { + try { + const msg = JSON.parse(line) as JSONLMessage; + if (msg.type === "assistant" && msg.message?.content) { + const text = extractTextContent(msg.message.content); + if (text) assistantMessages.push(text); + } + } catch { + // skip malformed lines + } + } + + // Take last N messages + const recent = assistantMessages.slice(-lines); + return recent.join("\n---\n"); + } + + async status(sessionId: string): Promise { + const runningPids = await this.getPids(); + const entry = await this.findIndexEntry(sessionId); + if (!entry) + throw new Error(`Session not found: ${sessionId}`); + + return this.buildSessionFromIndex(entry.entry, entry.index, runningPids); + } + + async launch(opts: LaunchOpts): Promise { + const args = [ + "--dangerously-skip-permissions", + "--print", + "--verbose", + "--output-format", + "stream-json", + ]; + + if (opts.model) { + args.push("--model", opts.model); + } + + args.push("-p", opts.prompt); + + const env = { ...process.env, ...opts.env }; + + const child = spawn("claude", args, { + cwd: opts.cwd || process.cwd(), + env, + stdio: ["pipe", "pipe", "pipe"], + detached: true, + }); + + // Unref so the parent process can exit + child.unref(); + + const session: AgentSession = { + id: `pending-${child.pid}`, + adapter: this.id, + status: "running", + startedAt: new Date(), + cwd: opts.cwd || process.cwd(), + model: opts.model, + prompt: opts.prompt.slice(0, 200), + pid: child.pid, + meta: { + adapterOpts: opts.adapterOpts, + spec: opts.spec, + }, + }; + + return session; + } + + async stop(sessionId: string, opts?: StopOpts): Promise { + const pid = await this.findPidForSession(sessionId); + if (!pid) throw new Error(`No running process for session: ${sessionId}`); + + if (opts?.force) { + // SIGINT first, then SIGKILL after 5s + process.kill(pid, "SIGINT"); + await sleep(5000); + try { + process.kill(pid, "SIGKILL"); + } catch { + // Already dead — good + } + } else { + process.kill(pid, "SIGTERM"); + } + } + + async resume(sessionId: string, message: string): Promise { + const args = [ + "--dangerously-skip-permissions", + "--print", + "--verbose", + "--output-format", + "stream-json", + "--continue", + sessionId, + "-p", + message, + ]; + + const session = await this.status(sessionId).catch(() => null); + const cwd = session?.cwd || process.cwd(); + + const child = spawn("claude", args, { + cwd, + stdio: ["pipe", "pipe", "pipe"], + detached: true, + }); + + child.unref(); + } + + async *events(): AsyncIterable { + // Track known sessions to detect transitions + let knownSessions = new Map(); + + // Initial snapshot + const initial = await this.list({ all: true }); + for (const s of initial) { + knownSessions.set(s.id, s); + } + + // Poll + fs.watch hybrid + const watcher = watch(this.projectsDir, { recursive: true }); + + try { + while (true) { + await sleep(5000); + + const current = await this.list({ all: true }); + const currentMap = new Map(current.map((s) => [s.id, s])); + + // Detect new sessions + for (const [id, session] of currentMap) { + const prev = knownSessions.get(id); + if (!prev) { + yield { + type: "session.started", + adapter: this.id, + sessionId: id, + session, + timestamp: new Date(), + }; + } else if (prev.status === "running" && session.status === "stopped") { + yield { + type: "session.stopped", + adapter: this.id, + sessionId: id, + session, + timestamp: new Date(), + }; + } else if ( + prev.status === "running" && + session.status === "idle" + ) { + yield { + type: "session.idle", + adapter: this.id, + sessionId: id, + session, + timestamp: new Date(), + }; + } + } + + knownSessions = currentMap; + } + } finally { + watcher.close(); + } + } + + // --- Private helpers --- + + /** + * Get session entries for a project — uses sessions-index.json when available, + * falls back to scanning .jsonl files for projects without an index + * (e.g. currently running sessions that haven't been indexed yet). + */ + private async getEntriesForProject( + projPath: string, + projDirName: string, + ): Promise> { + // Try index first + const indexPath = path.join(projPath, "sessions-index.json"); + try { + const raw = await fs.readFile(indexPath, "utf-8"); + const index = JSON.parse(raw) as SessionIndex; + return index.entries.map((entry) => ({ entry, index })); + } catch { + // No index — fall back to scanning .jsonl files + } + + // We'll determine originalPath from the JSONL content below + let originalPath: string | undefined; + + const results: Array<{ entry: SessionIndexEntry; index: SessionIndex }> = []; + + let files: string[]; + try { + files = await fs.readdir(projPath); + } catch { + return []; + } + + for (const file of files) { + if (!file.endsWith(".jsonl")) continue; + const sessionId = file.replace(".jsonl", ""); + const fullPath = path.join(projPath, file); + + let fileStat; + try { + fileStat = await fs.stat(fullPath); + } catch { + continue; + } + + // Read first few lines for prompt and cwd + let firstPrompt = ""; + let sessionCwd = ""; + try { + const content = await fs.readFile(fullPath, "utf-8"); + for (const l of content.split("\n").slice(0, 20)) { + try { + const msg = JSON.parse(l); + if (msg.cwd && !sessionCwd) sessionCwd = msg.cwd; + if (msg.type === "user" && msg.message?.content && !firstPrompt) { + const c = msg.message.content; + firstPrompt = typeof c === "string" ? c : ""; + } + if (sessionCwd && firstPrompt) break; + } catch { + // skip + } + } + } catch { + // skip + } + + if (!originalPath && sessionCwd) { + originalPath = sessionCwd; + } + + const entryPath = sessionCwd || originalPath; + + const index: SessionIndex = { + version: 1, + entries: [], + originalPath: entryPath, + }; + + const entry: SessionIndexEntry = { + sessionId, + fullPath, + fileMtime: fileStat.mtimeMs, + firstPrompt, + created: fileStat.birthtime.toISOString(), + modified: fileStat.mtime.toISOString(), + projectPath: entryPath, + isSidechain: false, + }; + + results.push({ entry, index }); + } + + return results; + } + + private async buildSessionFromIndex( + entry: SessionIndexEntry, + index: SessionIndex, + runningPids: Map, + ): Promise { + const isRunning = await this.isSessionRunning( + entry, + index, + runningPids, + ); + + // Parse JSONL for token/model info (read last few lines for efficiency) + const { model, tokens } = await this.parseSessionTail(entry.fullPath); + + return { + id: entry.sessionId, + adapter: this.id, + status: isRunning ? "running" : "stopped", + startedAt: new Date(entry.created), + stoppedAt: isRunning ? undefined : new Date(entry.modified), + cwd: index.originalPath || entry.projectPath, + model, + prompt: entry.firstPrompt?.slice(0, 200), + tokens, + pid: isRunning + ? this.findMatchingPid(entry, index, runningPids) + : undefined, + meta: { + projectDir: index.originalPath || entry.projectPath, + gitBranch: entry.gitBranch, + messageCount: entry.messageCount, + }, + }; + } + + private async isSessionRunning( + entry: SessionIndexEntry, + index: SessionIndex, + runningPids: Map, + ): Promise { + const projectPath = index.originalPath || entry.projectPath; + if (!projectPath) return false; + + for (const [, info] of runningPids) { + if (info.cwd === projectPath) return true; + // Also check if the session ID appears in the command args + if (info.args.includes(entry.sessionId)) return true; + } + + // Fallback: check if JSONL was modified very recently (last 60s) + try { + const stat = await fs.stat(entry.fullPath); + const age = Date.now() - stat.mtimeMs; + if (age < 60_000) { + // Double-check: is there any claude process running? + return runningPids.size > 0; + } + } catch { + // file doesn't exist + } + + return false; + } + + private findMatchingPid( + entry: SessionIndexEntry, + index: SessionIndex, + runningPids: Map, + ): number | undefined { + const projectPath = index.originalPath || entry.projectPath; + + for (const [pid, info] of runningPids) { + if (info.cwd === projectPath) return pid; + if (info.args.includes(entry.sessionId)) return pid; + } + + return undefined; + } + + private async parseSessionTail( + jsonlPath: string, + ): Promise<{ model?: string; tokens?: { in: number; out: number } }> { + try { + const content = await fs.readFile(jsonlPath, "utf-8"); + const lines = content.trim().split("\n"); + + let model: string | undefined; + let totalIn = 0; + let totalOut = 0; + + // Read from the end for efficiency — last 100 lines + const tail = lines.slice(-100); + for (const line of tail) { + try { + const msg = JSON.parse(line) as JSONLMessage; + if (msg.type === "assistant" && msg.message) { + if (msg.message.model) model = msg.message.model; + if (msg.message.usage) { + totalIn += msg.message.usage.input_tokens || 0; + totalOut += msg.message.usage.output_tokens || 0; + } + } + } catch { + // skip + } + } + + // Also scan first few lines for model if we didn't find it + if (!model) { + const head = lines.slice(0, 20); + for (const line of head) { + try { + const msg = JSON.parse(line) as JSONLMessage; + if (msg.type === "assistant" && msg.message?.model) { + model = msg.message.model; + break; + } + } catch { + // skip + } + } + } + + return { + model, + tokens: totalIn || totalOut ? { in: totalIn, out: totalOut } : undefined, + }; + } catch { + return {}; + } + } + + private async findSessionFile( + sessionId: string, + ): Promise { + const entry = await this.findIndexEntry(sessionId); + if (!entry) return null; + try { + await fs.access(entry.entry.fullPath); + return entry.entry.fullPath; + } catch { + return null; + } + } + + private async findIndexEntry( + sessionId: string, + ): Promise<{ entry: SessionIndexEntry; index: SessionIndex } | null> { + let projectDirs: string[]; + try { + projectDirs = await fs.readdir(this.projectsDir); + } catch { + return null; + } + + for (const projDir of projectDirs) { + const projPath = path.join(this.projectsDir, projDir); + const stat = await fs.stat(projPath).catch(() => null); + if (!stat?.isDirectory()) continue; + + const entries = await this.getEntriesForProject(projPath, projDir); + // Support prefix matching for short IDs + const match = entries.find( + ({ entry: e }) => + e.sessionId === sessionId || e.sessionId.startsWith(sessionId), + ); + if (match) return match; + } + + return null; + } + + private async findPidForSession( + sessionId: string, + ): Promise { + const session = await this.status(sessionId); + return session.pid ?? null; + } +} + +// --- Utility functions --- + +async function getClaudePids(): Promise> { + const pids = new Map(); + + try { + const { stdout } = await execFileAsync("ps", [ + "aux", + ]); + + for (const line of stdout.split("\n")) { + if (!line.includes("claude") || line.includes("grep")) continue; + + // Extract PID (second field) and command (everything after 10th field) + const fields = line.trim().split(/\s+/); + if (fields.length < 11) continue; + const pid = parseInt(fields[1], 10); + const command = fields.slice(10).join(" "); + + // Only match lines where the command starts with "claude --" + // This excludes wrappers (tclsh, bash, screen, login) and + // interactive claude sessions (just "claude" with no flags) + if (!command.startsWith("claude --")) continue; + if (pid === process.pid) continue; + + // Try to extract working directory from lsof + let cwd = ""; + try { + const { stdout: lsofOut } = await execFileAsync("/usr/sbin/lsof", [ + "-p", + pid.toString(), + "-Fn", + ]); + // lsof output: "fcwd\nn/actual/path\n..." — find fcwd line, then next n line + const lsofLines = lsofOut.split("\n"); + for (let i = 0; i < lsofLines.length; i++) { + if (lsofLines[i] === "fcwd" && lsofLines[i + 1]?.startsWith("n")) { + cwd = lsofLines[i + 1].slice(1); // strip leading "n" + break; + } + } + } catch { + // lsof might fail — that's fine + } + + pids.set(pid, { pid, cwd, args: command }); + } + } catch { + // ps failed — return empty + } + + return pids; +} + +function extractTextContent( + content: string | Array<{ type: string; text?: string }>, +): string { + if (typeof content === "string") return content; + if (Array.isArray(content)) { + return content + .filter((b) => b.type === "text" && b.text) + .map((b) => b.text!) + .join("\n"); + } + return ""; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..cd63aad --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,270 @@ +#!/usr/bin/env node + +import { Command } from "commander"; +import { ClaudeCodeAdapter } from "./adapters/claude-code.js"; +import type { AgentAdapter, AgentSession, ListOpts } from "./core/types.js"; + +const adapters: Record = { + "claude-code": new ClaudeCodeAdapter(), +}; + +function getAdapter(name?: string): AgentAdapter { + if (!name) { + // Default to claude-code (only adapter for now) + return adapters["claude-code"]; + } + const adapter = adapters[name]; + if (!adapter) { + console.error(`Unknown adapter: ${name}`); + process.exit(1); + } + return adapter; +} + +function getAllAdapters(): AgentAdapter[] { + return Object.values(adapters); +} + +// --- Formatters --- + +function formatSession(s: AgentSession): Record { + return { + ID: s.id.slice(0, 8), + Status: s.status, + Model: s.model || "-", + CWD: s.cwd ? shortenPath(s.cwd) : "-", + PID: s.pid?.toString() || "-", + Started: timeAgo(s.startedAt), + Prompt: (s.prompt || "-").slice(0, 60), + }; +} + +function shortenPath(p: string): string { + const home = process.env.HOME || ""; + if (p.startsWith(home)) return "~" + p.slice(home.length); + return p; +} + +function timeAgo(date: Date): string { + const seconds = Math.floor((Date.now() - date.getTime()) / 1000); + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +function printTable(rows: Record[]): void { + if (rows.length === 0) { + console.log("No sessions found."); + return; + } + + const keys = Object.keys(rows[0]); + const widths = keys.map((k) => + Math.max(k.length, ...rows.map((r) => (r[k] || "").length)), + ); + + // Header + const header = keys.map((k, i) => k.padEnd(widths[i])).join(" "); + console.log(header); + console.log(widths.map((w) => "-".repeat(w)).join(" ")); + + // Rows + for (const row of rows) { + const line = keys.map((k, i) => (row[k] || "").padEnd(widths[i])).join(" "); + console.log(line); + } +} + +function printJson(data: unknown): void { + console.log(JSON.stringify(data, null, 2)); +} + +function sessionToJson(s: AgentSession): Record { + return { + id: s.id, + adapter: s.adapter, + status: s.status, + startedAt: s.startedAt.toISOString(), + stoppedAt: s.stoppedAt?.toISOString(), + cwd: s.cwd, + model: s.model, + prompt: s.prompt, + tokens: s.tokens, + cost: s.cost, + pid: s.pid, + meta: s.meta, + }; +} + +// --- CLI --- + +const program = new Command(); + +program + .name("agent-ctl") + .description("Universal agent supervision interface") + .version("0.1.0"); + +// list +program + .command("list") + .description("List agent sessions") + .option("--adapter ", "Filter by adapter") + .option("--status ", "Filter by status (running|stopped|idle|error)") + .option("-a, --all", "Include stopped sessions (last 7 days)") + .option("--json", "Output as JSON") + .action(async (opts) => { + const listOpts: ListOpts = { + status: opts.status, + all: opts.all, + }; + + let sessions: AgentSession[] = []; + + if (opts.adapter) { + const adapter = getAdapter(opts.adapter); + sessions = await adapter.list(listOpts); + } else { + // All adapters + for (const adapter of getAllAdapters()) { + const s = await adapter.list(listOpts); + sessions.push(...s); + } + } + + if (opts.json) { + printJson(sessions.map(sessionToJson)); + } else { + printTable(sessions.map(formatSession)); + } + }); + +// status +program + .command("status ") + .description("Show detailed session status") + .option("--adapter ", "Adapter to use") + .option("--json", "Output as JSON") + .action(async (id: string, opts) => { + const adapter = getAdapter(opts.adapter); + try { + const session = await adapter.status(id); + if (opts.json) { + printJson(sessionToJson(session)); + } else { + const fmt = formatSession(session); + for (const [k, v] of Object.entries(fmt)) { + console.log(`${k.padEnd(10)} ${v}`); + } + if (session.tokens) { + console.log(`Tokens in: ${session.tokens.in}, out: ${session.tokens.out}`); + } + } + } catch (err) { + console.error((err as Error).message); + process.exit(1); + } + }); + +// peek +program + .command("peek ") + .description("Peek at recent output from a session") + .option("-n, --lines ", "Number of recent messages", "20") + .option("--adapter ", "Adapter to use") + .action(async (id: string, opts) => { + const adapter = getAdapter(opts.adapter); + try { + const output = await adapter.peek(id, { + lines: parseInt(opts.lines, 10), + }); + console.log(output); + } catch (err) { + console.error((err as Error).message); + process.exit(1); + } + }); + +// stop +program + .command("stop ") + .description("Stop a running session") + .option("--force", "Force kill (SIGINT then SIGKILL)") + .option("--adapter ", "Adapter to use") + .action(async (id: string, opts) => { + const adapter = getAdapter(opts.adapter); + try { + await adapter.stop(id, { force: opts.force }); + console.log(`Stopped session ${id.slice(0, 8)}`); + } catch (err) { + console.error((err as Error).message); + process.exit(1); + } + }); + +// resume +program + .command("resume ") + .description("Resume a session with a new message") + .option("--adapter ", "Adapter to use") + .action(async (id: string, message: string, opts) => { + const adapter = getAdapter(opts.adapter); + try { + await adapter.resume(id, message); + console.log(`Resumed session ${id.slice(0, 8)}`); + } catch (err) { + console.error((err as Error).message); + process.exit(1); + } + }); + +// launch +program + .command("launch ") + .description("Launch a new agent session") + .requiredOption("-p, --prompt ", "Prompt to send") + .option("--spec ", "Spec file path") + .option("--cwd ", "Working directory") + .option("--model ", "Model to use (e.g. sonnet, opus)") + .action(async (adapterName: string, opts) => { + const adapter = getAdapter(adapterName); + try { + const session = await adapter.launch({ + adapter: adapterName, + prompt: opts.prompt, + spec: opts.spec, + cwd: opts.cwd, + model: opts.model, + }); + console.log(`Launched session ${session.id.slice(0, 8)} (PID: ${session.pid})`); + } catch (err) { + console.error((err as Error).message); + process.exit(1); + } + }); + +// events +program + .command("events") + .description("Stream lifecycle events") + .option("--json", "Output as NDJSON (default)") + .action(async () => { + const adapter = getAdapter("claude-code"); + for await (const event of adapter.events()) { + const out = { + type: event.type, + adapter: event.adapter, + sessionId: event.sessionId, + timestamp: event.timestamp.toISOString(), + session: sessionToJson(event.session), + meta: event.meta, + }; + console.log(JSON.stringify(out)); + } + }); + +program.parse(); diff --git a/src/core/types.ts b/src/core/types.ts new file mode 100644 index 0000000..49d69a8 --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1,72 @@ +// agent-ctl core types — Universal Agent Supervision Interface + +export interface AgentAdapter { + id: string; // "claude-code", "openclaw", etc. + + // Discovery + list(opts?: ListOpts): Promise; + + // Read + peek(sessionId: string, opts?: PeekOpts): Promise; + status(sessionId: string): Promise; + + // Control + launch(opts: LaunchOpts): Promise; + stop(sessionId: string, opts?: StopOpts): Promise; + resume(sessionId: string, message: string): Promise; + + // Lifecycle events + events(): AsyncIterable; +} + +export interface AgentSession { + id: string; + adapter: string; + status: "running" | "idle" | "stopped" | "error"; + startedAt: Date; + stoppedAt?: Date; + cwd?: string; + spec?: string; + model?: string; + prompt?: string; + tokens?: { in: number; out: number }; + cost?: number; + pid?: number; + meta: Record; +} + +export interface LifecycleEvent { + type: + | "session.started" + | "session.stopped" + | "session.idle" + | "session.error"; + adapter: string; + sessionId: string; + session: AgentSession; + timestamp: Date; + meta?: Record; +} + +export interface ListOpts { + status?: "running" | "idle" | "stopped" | "error"; + all?: boolean; // include stopped sessions (default: running only) +} + +export interface PeekOpts { + lines?: number; +} + +export interface StopOpts { + force?: boolean; +} + +export interface LaunchOpts { + adapter: string; + prompt: string; + spec?: string; + cwd?: string; + model?: string; + env?: Record; + adapterOpts?: Record; +}