🐱 Medium-horizon agent planning MCP server
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial project setup

+9038
+46
.gitignore
··· 1 + # Dependencies 2 + node_modules/ 3 + 4 + # Build output 5 + dist/ 6 + *.tsbuildinfo 7 + 8 + # Test coverage 9 + coverage/ 10 + 11 + # Logs 12 + *.log 13 + logs/ 14 + 15 + # Environment 16 + .env 17 + .env.local 18 + .env.*.local 19 + 20 + # IDE 21 + .idea/ 22 + .vscode/ 23 + *.swp 24 + *.swo 25 + *.sublime-* 26 + 27 + # OS 28 + .DS_Store 29 + Thumbs.db 30 + Desktop.ini 31 + 32 + # pnpm 33 + .pnpm-debug.log 34 + 35 + # npm 36 + .npmrc 37 + 38 + # Native module build artifacts 39 + build/ 40 + prebuilds/ 41 + 42 + # Session data (created at runtime in app data dirs, but just in case) 43 + *.db 44 + *.db-journal 45 + *.db-wal 46 + *.db-shm
+1
.prettierrc
··· 1 + {}
+41
eslint.config.mjs
··· 1 + // @ts-check 2 + import eslint from '@eslint/js'; 3 + import tseslint from 'typescript-eslint'; 4 + 5 + export default tseslint.config( 6 + { 7 + ignores: ['dist/**', 'node_modules/**', 'coverage/**'], 8 + }, 9 + eslint.configs.recommended, 10 + ...tseslint.configs.strictTypeChecked, 11 + ...tseslint.configs.stylisticTypeChecked, 12 + { 13 + languageOptions: { 14 + parserOptions: { 15 + projectService: true, 16 + tsconfigRootDir: import.meta.dirname, 17 + }, 18 + }, 19 + rules: { 20 + // Allow unused vars with underscore prefix 21 + '@typescript-eslint/no-unused-vars': [ 22 + 'error', 23 + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, 24 + ], 25 + // Relax some strict rules for MCP server patterns 26 + '@typescript-eslint/explicit-function-return-type': 'off', 27 + '@typescript-eslint/explicit-module-boundary-types': 'off', 28 + '@typescript-eslint/no-explicit-any': 'warn', 29 + // Allow non-null assertions where needed 30 + '@typescript-eslint/no-non-null-assertion': 'warn', 31 + }, 32 + }, 33 + // Relax rules for test files 34 + { 35 + files: ['tests/**/*.ts'], 36 + rules: { 37 + // Non-null assertions are common and acceptable in tests 38 + '@typescript-eslint/no-non-null-assertion': 'off', 39 + }, 40 + }, 41 + );
+70
package.json
··· 1 + { 2 + "name": "9plan-mcp-server", 3 + "version": "1.0.0", 4 + "description": "MCP server providing session-scoped work queues for AI agent task sequencing", 5 + "type": "module", 6 + "main": "dist/index.js", 7 + "bin": { 8 + "9plan": "dist/index.js" 9 + }, 10 + "scripts": { 11 + "build": "tsgo -p tsconfig.build.json", 12 + "build:check": "tsgo --noEmit", 13 + "dev": "tsx watch src/index.ts", 14 + "start": "node dist/index.js", 15 + "test": "vitest", 16 + "test:run": "vitest run", 17 + "test:coverage": "vitest run --coverage", 18 + "lint": "eslint src tests", 19 + "lint:fix": "eslint src tests --fix", 20 + "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", 21 + "typecheck": "tsgo --noEmit", 22 + "check": "pnpm lint && pnpm build && pnpm test:run" 23 + }, 24 + "keywords": [ 25 + "mcp", 26 + "model-context-protocol", 27 + "ai", 28 + "agent", 29 + "task-queue", 30 + "planning" 31 + ], 32 + "author": "", 33 + "license": "MIT", 34 + "engines": { 35 + "node": ">=22.5.0" 36 + }, 37 + "packageManager": "pnpm@10.26.0", 38 + "dependencies": { 39 + "@karashiiro/mcp": "^0.3.0", 40 + "@modelcontextprotocol/sdk": "^1.25.2", 41 + "better-sqlite3": "^12.6.0", 42 + "env-paths": "^3.0.0", 43 + "nanoid": "^5.1.6", 44 + "pino": "^9.14.0", 45 + "unique-names-generator": "^4.7.1", 46 + "zod": "^3.25.76" 47 + }, 48 + "optionalDependencies": { 49 + "@hono/node-server": "^1.19.9", 50 + "hono": "^4.11.4" 51 + }, 52 + "pnpm": { 53 + "onlyBuiltDependencies": [ 54 + "better-sqlite3" 55 + ] 56 + }, 57 + "devDependencies": { 58 + "@eslint/js": "^9.39.2", 59 + "@types/better-sqlite3": "^7.6.13", 60 + "@types/node": "^22.19.7", 61 + "@typescript/native-preview": "7.0.0-dev.20260115.1", 62 + "eslint": "^9.39.2", 63 + "pino-pretty": "^11.3.0", 64 + "prettier": "^3.8.0", 65 + "tsx": "^4.21.0", 66 + "typescript": "^5.9.3", 67 + "typescript-eslint": "^8.53.0", 68 + "vitest": "^3.2.4" 69 + } 70 + }
+3178
pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + dependencies: 11 + '@karashiiro/mcp': 12 + specifier: ^0.3.0 13 + version: 0.3.0(@hono/node-server@1.19.9(hono@4.11.4))(@modelcontextprotocol/sdk@1.25.2(hono@4.11.4)(zod@3.25.76))(hono@4.11.4) 14 + '@modelcontextprotocol/sdk': 15 + specifier: ^1.25.2 16 + version: 1.25.2(hono@4.11.4)(zod@3.25.76) 17 + better-sqlite3: 18 + specifier: ^12.6.0 19 + version: 12.6.0 20 + env-paths: 21 + specifier: ^3.0.0 22 + version: 3.0.0 23 + nanoid: 24 + specifier: ^5.1.6 25 + version: 5.1.6 26 + pino: 27 + specifier: ^9.14.0 28 + version: 9.14.0 29 + unique-names-generator: 30 + specifier: ^4.7.1 31 + version: 4.7.1 32 + zod: 33 + specifier: ^3.25.76 34 + version: 3.25.76 35 + devDependencies: 36 + '@eslint/js': 37 + specifier: ^9.39.2 38 + version: 9.39.2 39 + '@types/better-sqlite3': 40 + specifier: ^7.6.13 41 + version: 7.6.13 42 + '@types/node': 43 + specifier: ^22.19.7 44 + version: 22.19.7 45 + '@typescript/native-preview': 46 + specifier: 7.0.0-dev.20260115.1 47 + version: 7.0.0-dev.20260115.1 48 + eslint: 49 + specifier: ^9.39.2 50 + version: 9.39.2 51 + pino-pretty: 52 + specifier: ^11.3.0 53 + version: 11.3.0 54 + prettier: 55 + specifier: ^3.8.0 56 + version: 3.8.0 57 + tsx: 58 + specifier: ^4.21.0 59 + version: 4.21.0 60 + typescript: 61 + specifier: ^5.9.3 62 + version: 5.9.3 63 + typescript-eslint: 64 + specifier: ^8.53.0 65 + version: 8.53.0(eslint@9.39.2)(typescript@5.9.3) 66 + vitest: 67 + specifier: ^3.2.4 68 + version: 3.2.4(@types/node@22.19.7)(tsx@4.21.0) 69 + optionalDependencies: 70 + '@hono/node-server': 71 + specifier: ^1.19.9 72 + version: 1.19.9(hono@4.11.4) 73 + hono: 74 + specifier: ^4.11.4 75 + version: 4.11.4 76 + 77 + packages: 78 + 79 + '@esbuild/aix-ppc64@0.27.2': 80 + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} 81 + engines: {node: '>=18'} 82 + cpu: [ppc64] 83 + os: [aix] 84 + 85 + '@esbuild/android-arm64@0.27.2': 86 + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} 87 + engines: {node: '>=18'} 88 + cpu: [arm64] 89 + os: [android] 90 + 91 + '@esbuild/android-arm@0.27.2': 92 + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} 93 + engines: {node: '>=18'} 94 + cpu: [arm] 95 + os: [android] 96 + 97 + '@esbuild/android-x64@0.27.2': 98 + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} 99 + engines: {node: '>=18'} 100 + cpu: [x64] 101 + os: [android] 102 + 103 + '@esbuild/darwin-arm64@0.27.2': 104 + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} 105 + engines: {node: '>=18'} 106 + cpu: [arm64] 107 + os: [darwin] 108 + 109 + '@esbuild/darwin-x64@0.27.2': 110 + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} 111 + engines: {node: '>=18'} 112 + cpu: [x64] 113 + os: [darwin] 114 + 115 + '@esbuild/freebsd-arm64@0.27.2': 116 + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} 117 + engines: {node: '>=18'} 118 + cpu: [arm64] 119 + os: [freebsd] 120 + 121 + '@esbuild/freebsd-x64@0.27.2': 122 + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} 123 + engines: {node: '>=18'} 124 + cpu: [x64] 125 + os: [freebsd] 126 + 127 + '@esbuild/linux-arm64@0.27.2': 128 + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} 129 + engines: {node: '>=18'} 130 + cpu: [arm64] 131 + os: [linux] 132 + 133 + '@esbuild/linux-arm@0.27.2': 134 + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} 135 + engines: {node: '>=18'} 136 + cpu: [arm] 137 + os: [linux] 138 + 139 + '@esbuild/linux-ia32@0.27.2': 140 + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} 141 + engines: {node: '>=18'} 142 + cpu: [ia32] 143 + os: [linux] 144 + 145 + '@esbuild/linux-loong64@0.27.2': 146 + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} 147 + engines: {node: '>=18'} 148 + cpu: [loong64] 149 + os: [linux] 150 + 151 + '@esbuild/linux-mips64el@0.27.2': 152 + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} 153 + engines: {node: '>=18'} 154 + cpu: [mips64el] 155 + os: [linux] 156 + 157 + '@esbuild/linux-ppc64@0.27.2': 158 + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} 159 + engines: {node: '>=18'} 160 + cpu: [ppc64] 161 + os: [linux] 162 + 163 + '@esbuild/linux-riscv64@0.27.2': 164 + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} 165 + engines: {node: '>=18'} 166 + cpu: [riscv64] 167 + os: [linux] 168 + 169 + '@esbuild/linux-s390x@0.27.2': 170 + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} 171 + engines: {node: '>=18'} 172 + cpu: [s390x] 173 + os: [linux] 174 + 175 + '@esbuild/linux-x64@0.27.2': 176 + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} 177 + engines: {node: '>=18'} 178 + cpu: [x64] 179 + os: [linux] 180 + 181 + '@esbuild/netbsd-arm64@0.27.2': 182 + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} 183 + engines: {node: '>=18'} 184 + cpu: [arm64] 185 + os: [netbsd] 186 + 187 + '@esbuild/netbsd-x64@0.27.2': 188 + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} 189 + engines: {node: '>=18'} 190 + cpu: [x64] 191 + os: [netbsd] 192 + 193 + '@esbuild/openbsd-arm64@0.27.2': 194 + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} 195 + engines: {node: '>=18'} 196 + cpu: [arm64] 197 + os: [openbsd] 198 + 199 + '@esbuild/openbsd-x64@0.27.2': 200 + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} 201 + engines: {node: '>=18'} 202 + cpu: [x64] 203 + os: [openbsd] 204 + 205 + '@esbuild/openharmony-arm64@0.27.2': 206 + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} 207 + engines: {node: '>=18'} 208 + cpu: [arm64] 209 + os: [openharmony] 210 + 211 + '@esbuild/sunos-x64@0.27.2': 212 + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} 213 + engines: {node: '>=18'} 214 + cpu: [x64] 215 + os: [sunos] 216 + 217 + '@esbuild/win32-arm64@0.27.2': 218 + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} 219 + engines: {node: '>=18'} 220 + cpu: [arm64] 221 + os: [win32] 222 + 223 + '@esbuild/win32-ia32@0.27.2': 224 + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} 225 + engines: {node: '>=18'} 226 + cpu: [ia32] 227 + os: [win32] 228 + 229 + '@esbuild/win32-x64@0.27.2': 230 + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} 231 + engines: {node: '>=18'} 232 + cpu: [x64] 233 + os: [win32] 234 + 235 + '@eslint-community/eslint-utils@4.9.1': 236 + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} 237 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 238 + peerDependencies: 239 + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 240 + 241 + '@eslint-community/regexpp@4.12.2': 242 + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} 243 + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 244 + 245 + '@eslint/config-array@0.21.1': 246 + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} 247 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 248 + 249 + '@eslint/config-helpers@0.4.2': 250 + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} 251 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 252 + 253 + '@eslint/core@0.17.0': 254 + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} 255 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 256 + 257 + '@eslint/eslintrc@3.3.3': 258 + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} 259 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 260 + 261 + '@eslint/js@9.39.2': 262 + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} 263 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 264 + 265 + '@eslint/object-schema@2.1.7': 266 + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} 267 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 268 + 269 + '@eslint/plugin-kit@0.4.1': 270 + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} 271 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 272 + 273 + '@hono/node-server@1.19.9': 274 + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} 275 + engines: {node: '>=18.14.1'} 276 + peerDependencies: 277 + hono: ^4 278 + 279 + '@humanfs/core@0.19.1': 280 + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} 281 + engines: {node: '>=18.18.0'} 282 + 283 + '@humanfs/node@0.16.7': 284 + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} 285 + engines: {node: '>=18.18.0'} 286 + 287 + '@humanwhocodes/module-importer@1.0.1': 288 + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 289 + engines: {node: '>=12.22'} 290 + 291 + '@humanwhocodes/retry@0.4.3': 292 + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} 293 + engines: {node: '>=18.18'} 294 + 295 + '@jridgewell/sourcemap-codec@1.5.5': 296 + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 297 + 298 + '@karashiiro/mcp@0.3.0': 299 + resolution: {integrity: sha512-1U9yxbBewWFO9XY7TKRmOFOi4TcPMiI3uKtf+ETyARvzxq+wHjDmZXPB4+hQNTx2rHbvnvDE41HNlkszmQb40g==} 300 + peerDependencies: 301 + '@hono/node-server': ^1.19.8 302 + '@modelcontextprotocol/sdk': ^1.25.2 303 + hono: ^4.11.4 304 + peerDependenciesMeta: 305 + '@hono/node-server': 306 + optional: true 307 + hono: 308 + optional: true 309 + 310 + '@modelcontextprotocol/sdk@1.25.2': 311 + resolution: {integrity: sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==} 312 + engines: {node: '>=18'} 313 + peerDependencies: 314 + '@cfworker/json-schema': ^4.1.1 315 + zod: ^3.25 || ^4.0 316 + peerDependenciesMeta: 317 + '@cfworker/json-schema': 318 + optional: true 319 + 320 + '@pinojs/redact@0.4.0': 321 + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} 322 + 323 + '@rollup/rollup-android-arm-eabi@4.55.1': 324 + resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} 325 + cpu: [arm] 326 + os: [android] 327 + 328 + '@rollup/rollup-android-arm64@4.55.1': 329 + resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} 330 + cpu: [arm64] 331 + os: [android] 332 + 333 + '@rollup/rollup-darwin-arm64@4.55.1': 334 + resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} 335 + cpu: [arm64] 336 + os: [darwin] 337 + 338 + '@rollup/rollup-darwin-x64@4.55.1': 339 + resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} 340 + cpu: [x64] 341 + os: [darwin] 342 + 343 + '@rollup/rollup-freebsd-arm64@4.55.1': 344 + resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} 345 + cpu: [arm64] 346 + os: [freebsd] 347 + 348 + '@rollup/rollup-freebsd-x64@4.55.1': 349 + resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} 350 + cpu: [x64] 351 + os: [freebsd] 352 + 353 + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': 354 + resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} 355 + cpu: [arm] 356 + os: [linux] 357 + 358 + '@rollup/rollup-linux-arm-musleabihf@4.55.1': 359 + resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} 360 + cpu: [arm] 361 + os: [linux] 362 + 363 + '@rollup/rollup-linux-arm64-gnu@4.55.1': 364 + resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} 365 + cpu: [arm64] 366 + os: [linux] 367 + 368 + '@rollup/rollup-linux-arm64-musl@4.55.1': 369 + resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} 370 + cpu: [arm64] 371 + os: [linux] 372 + 373 + '@rollup/rollup-linux-loong64-gnu@4.55.1': 374 + resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} 375 + cpu: [loong64] 376 + os: [linux] 377 + 378 + '@rollup/rollup-linux-loong64-musl@4.55.1': 379 + resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} 380 + cpu: [loong64] 381 + os: [linux] 382 + 383 + '@rollup/rollup-linux-ppc64-gnu@4.55.1': 384 + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} 385 + cpu: [ppc64] 386 + os: [linux] 387 + 388 + '@rollup/rollup-linux-ppc64-musl@4.55.1': 389 + resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} 390 + cpu: [ppc64] 391 + os: [linux] 392 + 393 + '@rollup/rollup-linux-riscv64-gnu@4.55.1': 394 + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} 395 + cpu: [riscv64] 396 + os: [linux] 397 + 398 + '@rollup/rollup-linux-riscv64-musl@4.55.1': 399 + resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} 400 + cpu: [riscv64] 401 + os: [linux] 402 + 403 + '@rollup/rollup-linux-s390x-gnu@4.55.1': 404 + resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} 405 + cpu: [s390x] 406 + os: [linux] 407 + 408 + '@rollup/rollup-linux-x64-gnu@4.55.1': 409 + resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} 410 + cpu: [x64] 411 + os: [linux] 412 + 413 + '@rollup/rollup-linux-x64-musl@4.55.1': 414 + resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} 415 + cpu: [x64] 416 + os: [linux] 417 + 418 + '@rollup/rollup-openbsd-x64@4.55.1': 419 + resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} 420 + cpu: [x64] 421 + os: [openbsd] 422 + 423 + '@rollup/rollup-openharmony-arm64@4.55.1': 424 + resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} 425 + cpu: [arm64] 426 + os: [openharmony] 427 + 428 + '@rollup/rollup-win32-arm64-msvc@4.55.1': 429 + resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} 430 + cpu: [arm64] 431 + os: [win32] 432 + 433 + '@rollup/rollup-win32-ia32-msvc@4.55.1': 434 + resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} 435 + cpu: [ia32] 436 + os: [win32] 437 + 438 + '@rollup/rollup-win32-x64-gnu@4.55.1': 439 + resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} 440 + cpu: [x64] 441 + os: [win32] 442 + 443 + '@rollup/rollup-win32-x64-msvc@4.55.1': 444 + resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} 445 + cpu: [x64] 446 + os: [win32] 447 + 448 + '@types/better-sqlite3@7.6.13': 449 + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} 450 + 451 + '@types/chai@5.2.3': 452 + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} 453 + 454 + '@types/deep-eql@4.0.2': 455 + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} 456 + 457 + '@types/estree@1.0.8': 458 + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 459 + 460 + '@types/json-schema@7.0.15': 461 + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 462 + 463 + '@types/node@22.19.7': 464 + resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} 465 + 466 + '@typescript-eslint/eslint-plugin@8.53.0': 467 + resolution: {integrity: sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==} 468 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 469 + peerDependencies: 470 + '@typescript-eslint/parser': ^8.53.0 471 + eslint: ^8.57.0 || ^9.0.0 472 + typescript: '>=4.8.4 <6.0.0' 473 + 474 + '@typescript-eslint/parser@8.53.0': 475 + resolution: {integrity: sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==} 476 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 477 + peerDependencies: 478 + eslint: ^8.57.0 || ^9.0.0 479 + typescript: '>=4.8.4 <6.0.0' 480 + 481 + '@typescript-eslint/project-service@8.53.0': 482 + resolution: {integrity: sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==} 483 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 484 + peerDependencies: 485 + typescript: '>=4.8.4 <6.0.0' 486 + 487 + '@typescript-eslint/scope-manager@8.53.0': 488 + resolution: {integrity: sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==} 489 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 490 + 491 + '@typescript-eslint/tsconfig-utils@8.53.0': 492 + resolution: {integrity: sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==} 493 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 494 + peerDependencies: 495 + typescript: '>=4.8.4 <6.0.0' 496 + 497 + '@typescript-eslint/type-utils@8.53.0': 498 + resolution: {integrity: sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==} 499 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 500 + peerDependencies: 501 + eslint: ^8.57.0 || ^9.0.0 502 + typescript: '>=4.8.4 <6.0.0' 503 + 504 + '@typescript-eslint/types@8.53.0': 505 + resolution: {integrity: sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==} 506 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 507 + 508 + '@typescript-eslint/typescript-estree@8.53.0': 509 + resolution: {integrity: sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==} 510 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 511 + peerDependencies: 512 + typescript: '>=4.8.4 <6.0.0' 513 + 514 + '@typescript-eslint/utils@8.53.0': 515 + resolution: {integrity: sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==} 516 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 517 + peerDependencies: 518 + eslint: ^8.57.0 || ^9.0.0 519 + typescript: '>=4.8.4 <6.0.0' 520 + 521 + '@typescript-eslint/visitor-keys@8.53.0': 522 + resolution: {integrity: sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==} 523 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 524 + 525 + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260115.1': 526 + resolution: {integrity: sha512-ICAIUBa9ecoHrKTIG9WgHBUMOpdGxwYarRW0x4PRHs34cKWz1VYy50Y+NG2Ivai2WC/T12QSWom0UBZDL1/Zkw==} 527 + cpu: [arm64] 528 + os: [darwin] 529 + 530 + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260115.1': 531 + resolution: {integrity: sha512-UtnRE4OPmlxAYE42HBtcqQH/TsDZcs/7ej6K11mCD9uzCLgxxAZveI+bW43VIyPLP9dZFmX/3Pvvv8arBBwGfw==} 532 + cpu: [x64] 533 + os: [darwin] 534 + 535 + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260115.1': 536 + resolution: {integrity: sha512-sAU6b0Tlz3QahPpuNA0HOibGKruHvbFCANuuns6aR4OYd+zNi+diBBvCTyLWu0Do3ABjpCphrj4hCvs2rWfoQQ==} 537 + cpu: [arm64] 538 + os: [linux] 539 + 540 + '@typescript/native-preview-linux-arm@7.0.0-dev.20260115.1': 541 + resolution: {integrity: sha512-/divCKJhdfq1s7UG3EO2NYHHJl1EqcEIylD/pl/GbAMeKRomaKGTwwgshxv9/PoTp4AVHEKrziPDdN6sIOnFQQ==} 542 + cpu: [arm] 543 + os: [linux] 544 + 545 + '@typescript/native-preview-linux-x64@7.0.0-dev.20260115.1': 546 + resolution: {integrity: sha512-KcjKJoUtqFh0VD2zGJJZ2PM+GLlK11bZFJvvdoJj/D9nf9WSRE4jWLGE+50gyy9boeaKVlZuRnTfxP6Rt3nnrw==} 547 + cpu: [x64] 548 + os: [linux] 549 + 550 + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260115.1': 551 + resolution: {integrity: sha512-0TolySdSZ8w1adKNl4eEGht4lyAMJRTwwdsntbdcZ2sjDWVc4ikyu+GlYTYtkJM2M0w1Q7+iFMpWm6+M6hPHzg==} 552 + cpu: [arm64] 553 + os: [win32] 554 + 555 + '@typescript/native-preview-win32-x64@7.0.0-dev.20260115.1': 556 + resolution: {integrity: sha512-yhntwcToKN6aL4rcuslb60toB1SLArpzCwNmK9jnnwJ4JexViOldBu1caZxFhVXL5eQrUCvxfipGEU30MQxhtw==} 557 + cpu: [x64] 558 + os: [win32] 559 + 560 + '@typescript/native-preview@7.0.0-dev.20260115.1': 561 + resolution: {integrity: sha512-wCW5A5Olb/Iyzr6azx1lrS42DEmkUvzLqrtL+0mzKDov4l3sdxKsO5q/m8Mdqcx9WBTY07e+WgHZ6IIfjSOWrQ==} 562 + hasBin: true 563 + 564 + '@vitest/expect@3.2.4': 565 + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} 566 + 567 + '@vitest/mocker@3.2.4': 568 + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} 569 + peerDependencies: 570 + msw: ^2.4.9 571 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 572 + peerDependenciesMeta: 573 + msw: 574 + optional: true 575 + vite: 576 + optional: true 577 + 578 + '@vitest/pretty-format@3.2.4': 579 + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} 580 + 581 + '@vitest/runner@3.2.4': 582 + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} 583 + 584 + '@vitest/snapshot@3.2.4': 585 + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} 586 + 587 + '@vitest/spy@3.2.4': 588 + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} 589 + 590 + '@vitest/utils@3.2.4': 591 + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} 592 + 593 + abort-controller@3.0.0: 594 + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} 595 + engines: {node: '>=6.5'} 596 + 597 + accepts@2.0.0: 598 + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} 599 + engines: {node: '>= 0.6'} 600 + 601 + acorn-jsx@5.3.2: 602 + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 603 + peerDependencies: 604 + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 605 + 606 + acorn@8.15.0: 607 + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} 608 + engines: {node: '>=0.4.0'} 609 + hasBin: true 610 + 611 + ajv-formats@3.0.1: 612 + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} 613 + peerDependencies: 614 + ajv: ^8.0.0 615 + peerDependenciesMeta: 616 + ajv: 617 + optional: true 618 + 619 + ajv@6.12.6: 620 + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 621 + 622 + ajv@8.17.1: 623 + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} 624 + 625 + ansi-styles@4.3.0: 626 + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 627 + engines: {node: '>=8'} 628 + 629 + argparse@2.0.1: 630 + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 631 + 632 + assertion-error@2.0.1: 633 + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 634 + engines: {node: '>=12'} 635 + 636 + atomic-sleep@1.0.0: 637 + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} 638 + engines: {node: '>=8.0.0'} 639 + 640 + balanced-match@1.0.2: 641 + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 642 + 643 + base64-js@1.5.1: 644 + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} 645 + 646 + better-sqlite3@12.6.0: 647 + resolution: {integrity: sha512-FXI191x+D6UPWSze5IzZjhz+i9MK9nsuHsmTX9bXVl52k06AfZ2xql0lrgIUuzsMsJ7Vgl5kIptvDgBLIV3ZSQ==} 648 + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} 649 + 650 + bindings@1.5.0: 651 + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} 652 + 653 + bl@4.1.0: 654 + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} 655 + 656 + body-parser@2.2.2: 657 + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} 658 + engines: {node: '>=18'} 659 + 660 + brace-expansion@1.1.12: 661 + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} 662 + 663 + brace-expansion@2.0.2: 664 + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} 665 + 666 + buffer@5.7.1: 667 + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} 668 + 669 + buffer@6.0.3: 670 + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} 671 + 672 + bytes@3.1.2: 673 + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 674 + engines: {node: '>= 0.8'} 675 + 676 + cac@6.7.14: 677 + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 678 + engines: {node: '>=8'} 679 + 680 + call-bind-apply-helpers@1.0.2: 681 + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} 682 + engines: {node: '>= 0.4'} 683 + 684 + call-bound@1.0.4: 685 + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} 686 + engines: {node: '>= 0.4'} 687 + 688 + callsites@3.1.0: 689 + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 690 + engines: {node: '>=6'} 691 + 692 + chai@5.3.3: 693 + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} 694 + engines: {node: '>=18'} 695 + 696 + chalk@4.1.2: 697 + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 698 + engines: {node: '>=10'} 699 + 700 + check-error@2.1.3: 701 + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} 702 + engines: {node: '>= 16'} 703 + 704 + chownr@1.1.4: 705 + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} 706 + 707 + color-convert@2.0.1: 708 + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 709 + engines: {node: '>=7.0.0'} 710 + 711 + color-name@1.1.4: 712 + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 713 + 714 + colorette@2.0.20: 715 + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} 716 + 717 + concat-map@0.0.1: 718 + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 719 + 720 + content-disposition@1.0.1: 721 + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} 722 + engines: {node: '>=18'} 723 + 724 + content-type@1.0.5: 725 + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 726 + engines: {node: '>= 0.6'} 727 + 728 + cookie-signature@1.2.2: 729 + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} 730 + engines: {node: '>=6.6.0'} 731 + 732 + cookie@0.7.2: 733 + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 734 + engines: {node: '>= 0.6'} 735 + 736 + cors@2.8.5: 737 + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} 738 + engines: {node: '>= 0.10'} 739 + 740 + cross-spawn@7.0.6: 741 + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 742 + engines: {node: '>= 8'} 743 + 744 + dateformat@4.6.3: 745 + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} 746 + 747 + debug@4.4.3: 748 + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 749 + engines: {node: '>=6.0'} 750 + peerDependencies: 751 + supports-color: '*' 752 + peerDependenciesMeta: 753 + supports-color: 754 + optional: true 755 + 756 + decompress-response@6.0.0: 757 + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} 758 + engines: {node: '>=10'} 759 + 760 + deep-eql@5.0.2: 761 + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} 762 + engines: {node: '>=6'} 763 + 764 + deep-extend@0.6.0: 765 + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} 766 + engines: {node: '>=4.0.0'} 767 + 768 + deep-is@0.1.4: 769 + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 770 + 771 + depd@2.0.0: 772 + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 773 + engines: {node: '>= 0.8'} 774 + 775 + detect-libc@2.1.2: 776 + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} 777 + engines: {node: '>=8'} 778 + 779 + dunder-proto@1.0.1: 780 + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 781 + engines: {node: '>= 0.4'} 782 + 783 + ee-first@1.1.1: 784 + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 785 + 786 + encodeurl@2.0.0: 787 + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} 788 + engines: {node: '>= 0.8'} 789 + 790 + end-of-stream@1.4.5: 791 + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} 792 + 793 + env-paths@3.0.0: 794 + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} 795 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 796 + 797 + es-define-property@1.0.1: 798 + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 799 + engines: {node: '>= 0.4'} 800 + 801 + es-errors@1.3.0: 802 + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 803 + engines: {node: '>= 0.4'} 804 + 805 + es-module-lexer@1.7.0: 806 + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} 807 + 808 + es-object-atoms@1.1.1: 809 + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 810 + engines: {node: '>= 0.4'} 811 + 812 + esbuild@0.27.2: 813 + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} 814 + engines: {node: '>=18'} 815 + hasBin: true 816 + 817 + escape-html@1.0.3: 818 + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 819 + 820 + escape-string-regexp@4.0.0: 821 + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 822 + engines: {node: '>=10'} 823 + 824 + eslint-scope@8.4.0: 825 + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} 826 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 827 + 828 + eslint-visitor-keys@3.4.3: 829 + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 830 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 831 + 832 + eslint-visitor-keys@4.2.1: 833 + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} 834 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 835 + 836 + eslint@9.39.2: 837 + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} 838 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 839 + hasBin: true 840 + peerDependencies: 841 + jiti: '*' 842 + peerDependenciesMeta: 843 + jiti: 844 + optional: true 845 + 846 + espree@10.4.0: 847 + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} 848 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 849 + 850 + esquery@1.7.0: 851 + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} 852 + engines: {node: '>=0.10'} 853 + 854 + esrecurse@4.3.0: 855 + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 856 + engines: {node: '>=4.0'} 857 + 858 + estraverse@5.3.0: 859 + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 860 + engines: {node: '>=4.0'} 861 + 862 + estree-walker@3.0.3: 863 + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 864 + 865 + esutils@2.0.3: 866 + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 867 + engines: {node: '>=0.10.0'} 868 + 869 + etag@1.8.1: 870 + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 871 + engines: {node: '>= 0.6'} 872 + 873 + event-target-shim@5.0.1: 874 + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} 875 + engines: {node: '>=6'} 876 + 877 + events@3.3.0: 878 + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} 879 + engines: {node: '>=0.8.x'} 880 + 881 + eventsource-parser@3.0.6: 882 + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} 883 + engines: {node: '>=18.0.0'} 884 + 885 + eventsource@3.0.7: 886 + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} 887 + engines: {node: '>=18.0.0'} 888 + 889 + expand-template@2.0.3: 890 + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} 891 + engines: {node: '>=6'} 892 + 893 + expect-type@1.3.0: 894 + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} 895 + engines: {node: '>=12.0.0'} 896 + 897 + express-rate-limit@7.5.1: 898 + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} 899 + engines: {node: '>= 16'} 900 + peerDependencies: 901 + express: '>= 4.11' 902 + 903 + express@5.2.1: 904 + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} 905 + engines: {node: '>= 18'} 906 + 907 + fast-copy@3.0.2: 908 + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} 909 + 910 + fast-deep-equal@3.1.3: 911 + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 912 + 913 + fast-json-stable-stringify@2.1.0: 914 + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 915 + 916 + fast-levenshtein@2.0.6: 917 + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 918 + 919 + fast-safe-stringify@2.1.1: 920 + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} 921 + 922 + fast-uri@3.1.0: 923 + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} 924 + 925 + fdir@6.5.0: 926 + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 927 + engines: {node: '>=12.0.0'} 928 + peerDependencies: 929 + picomatch: ^3 || ^4 930 + peerDependenciesMeta: 931 + picomatch: 932 + optional: true 933 + 934 + file-entry-cache@8.0.0: 935 + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} 936 + engines: {node: '>=16.0.0'} 937 + 938 + file-uri-to-path@1.0.0: 939 + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} 940 + 941 + finalhandler@2.1.1: 942 + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} 943 + engines: {node: '>= 18.0.0'} 944 + 945 + find-up@5.0.0: 946 + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 947 + engines: {node: '>=10'} 948 + 949 + flat-cache@4.0.1: 950 + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} 951 + engines: {node: '>=16'} 952 + 953 + flatted@3.3.3: 954 + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} 955 + 956 + forwarded@0.2.0: 957 + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} 958 + engines: {node: '>= 0.6'} 959 + 960 + fresh@2.0.0: 961 + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} 962 + engines: {node: '>= 0.8'} 963 + 964 + fs-constants@1.0.0: 965 + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} 966 + 967 + fsevents@2.3.3: 968 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 969 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 970 + os: [darwin] 971 + 972 + function-bind@1.1.2: 973 + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 974 + 975 + get-intrinsic@1.3.0: 976 + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} 977 + engines: {node: '>= 0.4'} 978 + 979 + get-proto@1.0.1: 980 + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} 981 + engines: {node: '>= 0.4'} 982 + 983 + get-tsconfig@4.13.0: 984 + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} 985 + 986 + github-from-package@0.0.0: 987 + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} 988 + 989 + glob-parent@6.0.2: 990 + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 991 + engines: {node: '>=10.13.0'} 992 + 993 + globals@14.0.0: 994 + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} 995 + engines: {node: '>=18'} 996 + 997 + gopd@1.2.0: 998 + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} 999 + engines: {node: '>= 0.4'} 1000 + 1001 + has-flag@4.0.0: 1002 + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 1003 + engines: {node: '>=8'} 1004 + 1005 + has-symbols@1.1.0: 1006 + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} 1007 + engines: {node: '>= 0.4'} 1008 + 1009 + hasown@2.0.2: 1010 + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 1011 + engines: {node: '>= 0.4'} 1012 + 1013 + help-me@5.0.0: 1014 + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} 1015 + 1016 + hono@4.11.4: 1017 + resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} 1018 + engines: {node: '>=16.9.0'} 1019 + 1020 + http-errors@2.0.1: 1021 + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} 1022 + engines: {node: '>= 0.8'} 1023 + 1024 + iconv-lite@0.7.2: 1025 + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} 1026 + engines: {node: '>=0.10.0'} 1027 + 1028 + ieee754@1.2.1: 1029 + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} 1030 + 1031 + ignore@5.3.2: 1032 + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 1033 + engines: {node: '>= 4'} 1034 + 1035 + ignore@7.0.5: 1036 + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} 1037 + engines: {node: '>= 4'} 1038 + 1039 + import-fresh@3.3.1: 1040 + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} 1041 + engines: {node: '>=6'} 1042 + 1043 + imurmurhash@0.1.4: 1044 + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 1045 + engines: {node: '>=0.8.19'} 1046 + 1047 + inherits@2.0.4: 1048 + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 1049 + 1050 + ini@1.3.8: 1051 + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} 1052 + 1053 + ipaddr.js@1.9.1: 1054 + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} 1055 + engines: {node: '>= 0.10'} 1056 + 1057 + is-extglob@2.1.1: 1058 + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 1059 + engines: {node: '>=0.10.0'} 1060 + 1061 + is-glob@4.0.3: 1062 + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 1063 + engines: {node: '>=0.10.0'} 1064 + 1065 + is-promise@4.0.0: 1066 + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} 1067 + 1068 + isexe@2.0.0: 1069 + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 1070 + 1071 + jose@6.1.3: 1072 + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} 1073 + 1074 + joycon@3.1.1: 1075 + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} 1076 + engines: {node: '>=10'} 1077 + 1078 + js-tokens@9.0.1: 1079 + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} 1080 + 1081 + js-yaml@4.1.1: 1082 + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} 1083 + hasBin: true 1084 + 1085 + json-buffer@3.0.1: 1086 + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 1087 + 1088 + json-schema-traverse@0.4.1: 1089 + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 1090 + 1091 + json-schema-traverse@1.0.0: 1092 + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} 1093 + 1094 + json-schema-typed@8.0.2: 1095 + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} 1096 + 1097 + json-stable-stringify-without-jsonify@1.0.1: 1098 + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 1099 + 1100 + keyv@4.5.4: 1101 + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 1102 + 1103 + levn@0.4.1: 1104 + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 1105 + engines: {node: '>= 0.8.0'} 1106 + 1107 + locate-path@6.0.0: 1108 + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 1109 + engines: {node: '>=10'} 1110 + 1111 + lodash.merge@4.6.2: 1112 + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 1113 + 1114 + loupe@3.2.1: 1115 + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} 1116 + 1117 + magic-string@0.30.21: 1118 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 1119 + 1120 + math-intrinsics@1.1.0: 1121 + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 1122 + engines: {node: '>= 0.4'} 1123 + 1124 + media-typer@1.1.0: 1125 + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} 1126 + engines: {node: '>= 0.8'} 1127 + 1128 + merge-descriptors@2.0.0: 1129 + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} 1130 + engines: {node: '>=18'} 1131 + 1132 + mime-db@1.54.0: 1133 + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} 1134 + engines: {node: '>= 0.6'} 1135 + 1136 + mime-types@3.0.2: 1137 + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} 1138 + engines: {node: '>=18'} 1139 + 1140 + mimic-response@3.1.0: 1141 + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} 1142 + engines: {node: '>=10'} 1143 + 1144 + minimatch@3.1.2: 1145 + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 1146 + 1147 + minimatch@9.0.5: 1148 + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 1149 + engines: {node: '>=16 || 14 >=14.17'} 1150 + 1151 + minimist@1.2.8: 1152 + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 1153 + 1154 + mkdirp-classic@0.5.3: 1155 + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} 1156 + 1157 + ms@2.1.3: 1158 + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1159 + 1160 + nanoid@3.3.11: 1161 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 1162 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1163 + hasBin: true 1164 + 1165 + nanoid@5.1.6: 1166 + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} 1167 + engines: {node: ^18 || >=20} 1168 + hasBin: true 1169 + 1170 + napi-build-utils@2.0.0: 1171 + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} 1172 + 1173 + natural-compare@1.4.0: 1174 + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 1175 + 1176 + negotiator@1.0.0: 1177 + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} 1178 + engines: {node: '>= 0.6'} 1179 + 1180 + node-abi@3.85.0: 1181 + resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} 1182 + engines: {node: '>=10'} 1183 + 1184 + object-assign@4.1.1: 1185 + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 1186 + engines: {node: '>=0.10.0'} 1187 + 1188 + object-inspect@1.13.4: 1189 + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} 1190 + engines: {node: '>= 0.4'} 1191 + 1192 + on-exit-leak-free@2.1.2: 1193 + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} 1194 + engines: {node: '>=14.0.0'} 1195 + 1196 + on-finished@2.4.1: 1197 + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 1198 + engines: {node: '>= 0.8'} 1199 + 1200 + once@1.4.0: 1201 + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 1202 + 1203 + optionator@0.9.4: 1204 + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 1205 + engines: {node: '>= 0.8.0'} 1206 + 1207 + p-limit@3.1.0: 1208 + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 1209 + engines: {node: '>=10'} 1210 + 1211 + p-locate@5.0.0: 1212 + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 1213 + engines: {node: '>=10'} 1214 + 1215 + parent-module@1.0.1: 1216 + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 1217 + engines: {node: '>=6'} 1218 + 1219 + parseurl@1.3.3: 1220 + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 1221 + engines: {node: '>= 0.8'} 1222 + 1223 + path-exists@4.0.0: 1224 + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 1225 + engines: {node: '>=8'} 1226 + 1227 + path-key@3.1.1: 1228 + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 1229 + engines: {node: '>=8'} 1230 + 1231 + path-to-regexp@8.3.0: 1232 + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} 1233 + 1234 + pathe@2.0.3: 1235 + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 1236 + 1237 + pathval@2.0.1: 1238 + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} 1239 + engines: {node: '>= 14.16'} 1240 + 1241 + picocolors@1.1.1: 1242 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 1243 + 1244 + picomatch@4.0.3: 1245 + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 1246 + engines: {node: '>=12'} 1247 + 1248 + pino-abstract-transport@2.0.0: 1249 + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} 1250 + 1251 + pino-pretty@11.3.0: 1252 + resolution: {integrity: sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA==} 1253 + hasBin: true 1254 + 1255 + pino-std-serializers@7.1.0: 1256 + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} 1257 + 1258 + pino@9.14.0: 1259 + resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} 1260 + hasBin: true 1261 + 1262 + pkce-challenge@5.0.1: 1263 + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} 1264 + engines: {node: '>=16.20.0'} 1265 + 1266 + postcss@8.5.6: 1267 + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 1268 + engines: {node: ^10 || ^12 || >=14} 1269 + 1270 + prebuild-install@7.1.3: 1271 + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} 1272 + engines: {node: '>=10'} 1273 + hasBin: true 1274 + 1275 + prelude-ls@1.2.1: 1276 + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 1277 + engines: {node: '>= 0.8.0'} 1278 + 1279 + prettier@3.8.0: 1280 + resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==} 1281 + engines: {node: '>=14'} 1282 + hasBin: true 1283 + 1284 + process-warning@5.0.0: 1285 + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} 1286 + 1287 + process@0.11.10: 1288 + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} 1289 + engines: {node: '>= 0.6.0'} 1290 + 1291 + proxy-addr@2.0.7: 1292 + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 1293 + engines: {node: '>= 0.10'} 1294 + 1295 + pump@3.0.3: 1296 + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} 1297 + 1298 + punycode@2.3.1: 1299 + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1300 + engines: {node: '>=6'} 1301 + 1302 + qs@6.14.1: 1303 + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} 1304 + engines: {node: '>=0.6'} 1305 + 1306 + quick-format-unescaped@4.0.4: 1307 + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} 1308 + 1309 + range-parser@1.2.1: 1310 + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 1311 + engines: {node: '>= 0.6'} 1312 + 1313 + raw-body@3.0.2: 1314 + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} 1315 + engines: {node: '>= 0.10'} 1316 + 1317 + rc@1.2.8: 1318 + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} 1319 + hasBin: true 1320 + 1321 + readable-stream@3.6.2: 1322 + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} 1323 + engines: {node: '>= 6'} 1324 + 1325 + readable-stream@4.7.0: 1326 + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} 1327 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 1328 + 1329 + real-require@0.2.0: 1330 + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} 1331 + engines: {node: '>= 12.13.0'} 1332 + 1333 + require-from-string@2.0.2: 1334 + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 1335 + engines: {node: '>=0.10.0'} 1336 + 1337 + resolve-from@4.0.0: 1338 + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 1339 + engines: {node: '>=4'} 1340 + 1341 + resolve-pkg-maps@1.0.0: 1342 + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 1343 + 1344 + rollup@4.55.1: 1345 + resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} 1346 + engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1347 + hasBin: true 1348 + 1349 + router@2.2.0: 1350 + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} 1351 + engines: {node: '>= 18'} 1352 + 1353 + safe-buffer@5.2.1: 1354 + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 1355 + 1356 + safe-stable-stringify@2.5.0: 1357 + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} 1358 + engines: {node: '>=10'} 1359 + 1360 + safer-buffer@2.1.2: 1361 + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 1362 + 1363 + secure-json-parse@2.7.0: 1364 + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} 1365 + 1366 + semver@7.7.3: 1367 + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} 1368 + engines: {node: '>=10'} 1369 + hasBin: true 1370 + 1371 + send@1.2.1: 1372 + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} 1373 + engines: {node: '>= 18'} 1374 + 1375 + serve-static@2.2.1: 1376 + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} 1377 + engines: {node: '>= 18'} 1378 + 1379 + setprototypeof@1.2.0: 1380 + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 1381 + 1382 + shebang-command@2.0.0: 1383 + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 1384 + engines: {node: '>=8'} 1385 + 1386 + shebang-regex@3.0.0: 1387 + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1388 + engines: {node: '>=8'} 1389 + 1390 + side-channel-list@1.0.0: 1391 + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} 1392 + engines: {node: '>= 0.4'} 1393 + 1394 + side-channel-map@1.0.1: 1395 + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} 1396 + engines: {node: '>= 0.4'} 1397 + 1398 + side-channel-weakmap@1.0.2: 1399 + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} 1400 + engines: {node: '>= 0.4'} 1401 + 1402 + side-channel@1.1.0: 1403 + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} 1404 + engines: {node: '>= 0.4'} 1405 + 1406 + siginfo@2.0.0: 1407 + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 1408 + 1409 + simple-concat@1.0.1: 1410 + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} 1411 + 1412 + simple-get@4.0.1: 1413 + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} 1414 + 1415 + sonic-boom@4.2.0: 1416 + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} 1417 + 1418 + source-map-js@1.2.1: 1419 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1420 + engines: {node: '>=0.10.0'} 1421 + 1422 + split2@4.2.0: 1423 + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} 1424 + engines: {node: '>= 10.x'} 1425 + 1426 + stackback@0.0.2: 1427 + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 1428 + 1429 + statuses@2.0.2: 1430 + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} 1431 + engines: {node: '>= 0.8'} 1432 + 1433 + std-env@3.10.0: 1434 + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} 1435 + 1436 + string_decoder@1.3.0: 1437 + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} 1438 + 1439 + strip-json-comments@2.0.1: 1440 + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} 1441 + engines: {node: '>=0.10.0'} 1442 + 1443 + strip-json-comments@3.1.1: 1444 + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 1445 + engines: {node: '>=8'} 1446 + 1447 + strip-literal@3.1.0: 1448 + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} 1449 + 1450 + supports-color@7.2.0: 1451 + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1452 + engines: {node: '>=8'} 1453 + 1454 + tar-fs@2.1.4: 1455 + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} 1456 + 1457 + tar-stream@2.2.0: 1458 + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} 1459 + engines: {node: '>=6'} 1460 + 1461 + thread-stream@3.1.0: 1462 + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} 1463 + 1464 + tinybench@2.9.0: 1465 + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 1466 + 1467 + tinyexec@0.3.2: 1468 + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} 1469 + 1470 + tinyglobby@0.2.15: 1471 + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 1472 + engines: {node: '>=12.0.0'} 1473 + 1474 + tinypool@1.1.1: 1475 + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} 1476 + engines: {node: ^18.0.0 || >=20.0.0} 1477 + 1478 + tinyrainbow@2.0.0: 1479 + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} 1480 + engines: {node: '>=14.0.0'} 1481 + 1482 + tinyspy@4.0.4: 1483 + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} 1484 + engines: {node: '>=14.0.0'} 1485 + 1486 + toidentifier@1.0.1: 1487 + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 1488 + engines: {node: '>=0.6'} 1489 + 1490 + ts-api-utils@2.4.0: 1491 + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} 1492 + engines: {node: '>=18.12'} 1493 + peerDependencies: 1494 + typescript: '>=4.8.4' 1495 + 1496 + tsx@4.21.0: 1497 + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} 1498 + engines: {node: '>=18.0.0'} 1499 + hasBin: true 1500 + 1501 + tunnel-agent@0.6.0: 1502 + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} 1503 + 1504 + type-check@0.4.0: 1505 + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 1506 + engines: {node: '>= 0.8.0'} 1507 + 1508 + type-is@2.0.1: 1509 + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} 1510 + engines: {node: '>= 0.6'} 1511 + 1512 + typescript-eslint@8.53.0: 1513 + resolution: {integrity: sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==} 1514 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 1515 + peerDependencies: 1516 + eslint: ^8.57.0 || ^9.0.0 1517 + typescript: '>=4.8.4 <6.0.0' 1518 + 1519 + typescript@5.9.3: 1520 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 1521 + engines: {node: '>=14.17'} 1522 + hasBin: true 1523 + 1524 + undici-types@6.21.0: 1525 + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 1526 + 1527 + unique-names-generator@4.7.1: 1528 + resolution: {integrity: sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==} 1529 + engines: {node: '>=8'} 1530 + 1531 + unpipe@1.0.0: 1532 + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 1533 + engines: {node: '>= 0.8'} 1534 + 1535 + uri-js@4.4.1: 1536 + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 1537 + 1538 + util-deprecate@1.0.2: 1539 + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1540 + 1541 + uuid@13.0.0: 1542 + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} 1543 + hasBin: true 1544 + 1545 + vary@1.1.2: 1546 + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} 1547 + engines: {node: '>= 0.8'} 1548 + 1549 + vite-node@3.2.4: 1550 + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} 1551 + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1552 + hasBin: true 1553 + 1554 + vite@7.3.1: 1555 + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} 1556 + engines: {node: ^20.19.0 || >=22.12.0} 1557 + hasBin: true 1558 + peerDependencies: 1559 + '@types/node': ^20.19.0 || >=22.12.0 1560 + jiti: '>=1.21.0' 1561 + less: ^4.0.0 1562 + lightningcss: ^1.21.0 1563 + sass: ^1.70.0 1564 + sass-embedded: ^1.70.0 1565 + stylus: '>=0.54.8' 1566 + sugarss: ^5.0.0 1567 + terser: ^5.16.0 1568 + tsx: ^4.8.1 1569 + yaml: ^2.4.2 1570 + peerDependenciesMeta: 1571 + '@types/node': 1572 + optional: true 1573 + jiti: 1574 + optional: true 1575 + less: 1576 + optional: true 1577 + lightningcss: 1578 + optional: true 1579 + sass: 1580 + optional: true 1581 + sass-embedded: 1582 + optional: true 1583 + stylus: 1584 + optional: true 1585 + sugarss: 1586 + optional: true 1587 + terser: 1588 + optional: true 1589 + tsx: 1590 + optional: true 1591 + yaml: 1592 + optional: true 1593 + 1594 + vitest@3.2.4: 1595 + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} 1596 + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1597 + hasBin: true 1598 + peerDependencies: 1599 + '@edge-runtime/vm': '*' 1600 + '@types/debug': ^4.1.12 1601 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 1602 + '@vitest/browser': 3.2.4 1603 + '@vitest/ui': 3.2.4 1604 + happy-dom: '*' 1605 + jsdom: '*' 1606 + peerDependenciesMeta: 1607 + '@edge-runtime/vm': 1608 + optional: true 1609 + '@types/debug': 1610 + optional: true 1611 + '@types/node': 1612 + optional: true 1613 + '@vitest/browser': 1614 + optional: true 1615 + '@vitest/ui': 1616 + optional: true 1617 + happy-dom: 1618 + optional: true 1619 + jsdom: 1620 + optional: true 1621 + 1622 + which@2.0.2: 1623 + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1624 + engines: {node: '>= 8'} 1625 + hasBin: true 1626 + 1627 + why-is-node-running@2.3.0: 1628 + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 1629 + engines: {node: '>=8'} 1630 + hasBin: true 1631 + 1632 + word-wrap@1.2.5: 1633 + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 1634 + engines: {node: '>=0.10.0'} 1635 + 1636 + wrappy@1.0.2: 1637 + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1638 + 1639 + yocto-queue@0.1.0: 1640 + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 1641 + engines: {node: '>=10'} 1642 + 1643 + zod-to-json-schema@3.25.1: 1644 + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} 1645 + peerDependencies: 1646 + zod: ^3.25 || ^4 1647 + 1648 + zod@3.25.76: 1649 + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} 1650 + 1651 + snapshots: 1652 + 1653 + '@esbuild/aix-ppc64@0.27.2': 1654 + optional: true 1655 + 1656 + '@esbuild/android-arm64@0.27.2': 1657 + optional: true 1658 + 1659 + '@esbuild/android-arm@0.27.2': 1660 + optional: true 1661 + 1662 + '@esbuild/android-x64@0.27.2': 1663 + optional: true 1664 + 1665 + '@esbuild/darwin-arm64@0.27.2': 1666 + optional: true 1667 + 1668 + '@esbuild/darwin-x64@0.27.2': 1669 + optional: true 1670 + 1671 + '@esbuild/freebsd-arm64@0.27.2': 1672 + optional: true 1673 + 1674 + '@esbuild/freebsd-x64@0.27.2': 1675 + optional: true 1676 + 1677 + '@esbuild/linux-arm64@0.27.2': 1678 + optional: true 1679 + 1680 + '@esbuild/linux-arm@0.27.2': 1681 + optional: true 1682 + 1683 + '@esbuild/linux-ia32@0.27.2': 1684 + optional: true 1685 + 1686 + '@esbuild/linux-loong64@0.27.2': 1687 + optional: true 1688 + 1689 + '@esbuild/linux-mips64el@0.27.2': 1690 + optional: true 1691 + 1692 + '@esbuild/linux-ppc64@0.27.2': 1693 + optional: true 1694 + 1695 + '@esbuild/linux-riscv64@0.27.2': 1696 + optional: true 1697 + 1698 + '@esbuild/linux-s390x@0.27.2': 1699 + optional: true 1700 + 1701 + '@esbuild/linux-x64@0.27.2': 1702 + optional: true 1703 + 1704 + '@esbuild/netbsd-arm64@0.27.2': 1705 + optional: true 1706 + 1707 + '@esbuild/netbsd-x64@0.27.2': 1708 + optional: true 1709 + 1710 + '@esbuild/openbsd-arm64@0.27.2': 1711 + optional: true 1712 + 1713 + '@esbuild/openbsd-x64@0.27.2': 1714 + optional: true 1715 + 1716 + '@esbuild/openharmony-arm64@0.27.2': 1717 + optional: true 1718 + 1719 + '@esbuild/sunos-x64@0.27.2': 1720 + optional: true 1721 + 1722 + '@esbuild/win32-arm64@0.27.2': 1723 + optional: true 1724 + 1725 + '@esbuild/win32-ia32@0.27.2': 1726 + optional: true 1727 + 1728 + '@esbuild/win32-x64@0.27.2': 1729 + optional: true 1730 + 1731 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)': 1732 + dependencies: 1733 + eslint: 9.39.2 1734 + eslint-visitor-keys: 3.4.3 1735 + 1736 + '@eslint-community/regexpp@4.12.2': {} 1737 + 1738 + '@eslint/config-array@0.21.1': 1739 + dependencies: 1740 + '@eslint/object-schema': 2.1.7 1741 + debug: 4.4.3 1742 + minimatch: 3.1.2 1743 + transitivePeerDependencies: 1744 + - supports-color 1745 + 1746 + '@eslint/config-helpers@0.4.2': 1747 + dependencies: 1748 + '@eslint/core': 0.17.0 1749 + 1750 + '@eslint/core@0.17.0': 1751 + dependencies: 1752 + '@types/json-schema': 7.0.15 1753 + 1754 + '@eslint/eslintrc@3.3.3': 1755 + dependencies: 1756 + ajv: 6.12.6 1757 + debug: 4.4.3 1758 + espree: 10.4.0 1759 + globals: 14.0.0 1760 + ignore: 5.3.2 1761 + import-fresh: 3.3.1 1762 + js-yaml: 4.1.1 1763 + minimatch: 3.1.2 1764 + strip-json-comments: 3.1.1 1765 + transitivePeerDependencies: 1766 + - supports-color 1767 + 1768 + '@eslint/js@9.39.2': {} 1769 + 1770 + '@eslint/object-schema@2.1.7': {} 1771 + 1772 + '@eslint/plugin-kit@0.4.1': 1773 + dependencies: 1774 + '@eslint/core': 0.17.0 1775 + levn: 0.4.1 1776 + 1777 + '@hono/node-server@1.19.9(hono@4.11.4)': 1778 + dependencies: 1779 + hono: 4.11.4 1780 + 1781 + '@humanfs/core@0.19.1': {} 1782 + 1783 + '@humanfs/node@0.16.7': 1784 + dependencies: 1785 + '@humanfs/core': 0.19.1 1786 + '@humanwhocodes/retry': 0.4.3 1787 + 1788 + '@humanwhocodes/module-importer@1.0.1': {} 1789 + 1790 + '@humanwhocodes/retry@0.4.3': {} 1791 + 1792 + '@jridgewell/sourcemap-codec@1.5.5': {} 1793 + 1794 + '@karashiiro/mcp@0.3.0(@hono/node-server@1.19.9(hono@4.11.4))(@modelcontextprotocol/sdk@1.25.2(hono@4.11.4)(zod@3.25.76))(hono@4.11.4)': 1795 + dependencies: 1796 + '@modelcontextprotocol/sdk': 1.25.2(hono@4.11.4)(zod@3.25.76) 1797 + uuid: 13.0.0 1798 + optionalDependencies: 1799 + '@hono/node-server': 1.19.9(hono@4.11.4) 1800 + hono: 4.11.4 1801 + 1802 + '@modelcontextprotocol/sdk@1.25.2(hono@4.11.4)(zod@3.25.76)': 1803 + dependencies: 1804 + '@hono/node-server': 1.19.9(hono@4.11.4) 1805 + ajv: 8.17.1 1806 + ajv-formats: 3.0.1(ajv@8.17.1) 1807 + content-type: 1.0.5 1808 + cors: 2.8.5 1809 + cross-spawn: 7.0.6 1810 + eventsource: 3.0.7 1811 + eventsource-parser: 3.0.6 1812 + express: 5.2.1 1813 + express-rate-limit: 7.5.1(express@5.2.1) 1814 + jose: 6.1.3 1815 + json-schema-typed: 8.0.2 1816 + pkce-challenge: 5.0.1 1817 + raw-body: 3.0.2 1818 + zod: 3.25.76 1819 + zod-to-json-schema: 3.25.1(zod@3.25.76) 1820 + transitivePeerDependencies: 1821 + - hono 1822 + - supports-color 1823 + 1824 + '@pinojs/redact@0.4.0': {} 1825 + 1826 + '@rollup/rollup-android-arm-eabi@4.55.1': 1827 + optional: true 1828 + 1829 + '@rollup/rollup-android-arm64@4.55.1': 1830 + optional: true 1831 + 1832 + '@rollup/rollup-darwin-arm64@4.55.1': 1833 + optional: true 1834 + 1835 + '@rollup/rollup-darwin-x64@4.55.1': 1836 + optional: true 1837 + 1838 + '@rollup/rollup-freebsd-arm64@4.55.1': 1839 + optional: true 1840 + 1841 + '@rollup/rollup-freebsd-x64@4.55.1': 1842 + optional: true 1843 + 1844 + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': 1845 + optional: true 1846 + 1847 + '@rollup/rollup-linux-arm-musleabihf@4.55.1': 1848 + optional: true 1849 + 1850 + '@rollup/rollup-linux-arm64-gnu@4.55.1': 1851 + optional: true 1852 + 1853 + '@rollup/rollup-linux-arm64-musl@4.55.1': 1854 + optional: true 1855 + 1856 + '@rollup/rollup-linux-loong64-gnu@4.55.1': 1857 + optional: true 1858 + 1859 + '@rollup/rollup-linux-loong64-musl@4.55.1': 1860 + optional: true 1861 + 1862 + '@rollup/rollup-linux-ppc64-gnu@4.55.1': 1863 + optional: true 1864 + 1865 + '@rollup/rollup-linux-ppc64-musl@4.55.1': 1866 + optional: true 1867 + 1868 + '@rollup/rollup-linux-riscv64-gnu@4.55.1': 1869 + optional: true 1870 + 1871 + '@rollup/rollup-linux-riscv64-musl@4.55.1': 1872 + optional: true 1873 + 1874 + '@rollup/rollup-linux-s390x-gnu@4.55.1': 1875 + optional: true 1876 + 1877 + '@rollup/rollup-linux-x64-gnu@4.55.1': 1878 + optional: true 1879 + 1880 + '@rollup/rollup-linux-x64-musl@4.55.1': 1881 + optional: true 1882 + 1883 + '@rollup/rollup-openbsd-x64@4.55.1': 1884 + optional: true 1885 + 1886 + '@rollup/rollup-openharmony-arm64@4.55.1': 1887 + optional: true 1888 + 1889 + '@rollup/rollup-win32-arm64-msvc@4.55.1': 1890 + optional: true 1891 + 1892 + '@rollup/rollup-win32-ia32-msvc@4.55.1': 1893 + optional: true 1894 + 1895 + '@rollup/rollup-win32-x64-gnu@4.55.1': 1896 + optional: true 1897 + 1898 + '@rollup/rollup-win32-x64-msvc@4.55.1': 1899 + optional: true 1900 + 1901 + '@types/better-sqlite3@7.6.13': 1902 + dependencies: 1903 + '@types/node': 22.19.7 1904 + 1905 + '@types/chai@5.2.3': 1906 + dependencies: 1907 + '@types/deep-eql': 4.0.2 1908 + assertion-error: 2.0.1 1909 + 1910 + '@types/deep-eql@4.0.2': {} 1911 + 1912 + '@types/estree@1.0.8': {} 1913 + 1914 + '@types/json-schema@7.0.15': {} 1915 + 1916 + '@types/node@22.19.7': 1917 + dependencies: 1918 + undici-types: 6.21.0 1919 + 1920 + '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': 1921 + dependencies: 1922 + '@eslint-community/regexpp': 4.12.2 1923 + '@typescript-eslint/parser': 8.53.0(eslint@9.39.2)(typescript@5.9.3) 1924 + '@typescript-eslint/scope-manager': 8.53.0 1925 + '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2)(typescript@5.9.3) 1926 + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2)(typescript@5.9.3) 1927 + '@typescript-eslint/visitor-keys': 8.53.0 1928 + eslint: 9.39.2 1929 + ignore: 7.0.5 1930 + natural-compare: 1.4.0 1931 + ts-api-utils: 2.4.0(typescript@5.9.3) 1932 + typescript: 5.9.3 1933 + transitivePeerDependencies: 1934 + - supports-color 1935 + 1936 + '@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3)': 1937 + dependencies: 1938 + '@typescript-eslint/scope-manager': 8.53.0 1939 + '@typescript-eslint/types': 8.53.0 1940 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) 1941 + '@typescript-eslint/visitor-keys': 8.53.0 1942 + debug: 4.4.3 1943 + eslint: 9.39.2 1944 + typescript: 5.9.3 1945 + transitivePeerDependencies: 1946 + - supports-color 1947 + 1948 + '@typescript-eslint/project-service@8.53.0(typescript@5.9.3)': 1949 + dependencies: 1950 + '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) 1951 + '@typescript-eslint/types': 8.53.0 1952 + debug: 4.4.3 1953 + typescript: 5.9.3 1954 + transitivePeerDependencies: 1955 + - supports-color 1956 + 1957 + '@typescript-eslint/scope-manager@8.53.0': 1958 + dependencies: 1959 + '@typescript-eslint/types': 8.53.0 1960 + '@typescript-eslint/visitor-keys': 8.53.0 1961 + 1962 + '@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.9.3)': 1963 + dependencies: 1964 + typescript: 5.9.3 1965 + 1966 + '@typescript-eslint/type-utils@8.53.0(eslint@9.39.2)(typescript@5.9.3)': 1967 + dependencies: 1968 + '@typescript-eslint/types': 8.53.0 1969 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) 1970 + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2)(typescript@5.9.3) 1971 + debug: 4.4.3 1972 + eslint: 9.39.2 1973 + ts-api-utils: 2.4.0(typescript@5.9.3) 1974 + typescript: 5.9.3 1975 + transitivePeerDependencies: 1976 + - supports-color 1977 + 1978 + '@typescript-eslint/types@8.53.0': {} 1979 + 1980 + '@typescript-eslint/typescript-estree@8.53.0(typescript@5.9.3)': 1981 + dependencies: 1982 + '@typescript-eslint/project-service': 8.53.0(typescript@5.9.3) 1983 + '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) 1984 + '@typescript-eslint/types': 8.53.0 1985 + '@typescript-eslint/visitor-keys': 8.53.0 1986 + debug: 4.4.3 1987 + minimatch: 9.0.5 1988 + semver: 7.7.3 1989 + tinyglobby: 0.2.15 1990 + ts-api-utils: 2.4.0(typescript@5.9.3) 1991 + typescript: 5.9.3 1992 + transitivePeerDependencies: 1993 + - supports-color 1994 + 1995 + '@typescript-eslint/utils@8.53.0(eslint@9.39.2)(typescript@5.9.3)': 1996 + dependencies: 1997 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) 1998 + '@typescript-eslint/scope-manager': 8.53.0 1999 + '@typescript-eslint/types': 8.53.0 2000 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) 2001 + eslint: 9.39.2 2002 + typescript: 5.9.3 2003 + transitivePeerDependencies: 2004 + - supports-color 2005 + 2006 + '@typescript-eslint/visitor-keys@8.53.0': 2007 + dependencies: 2008 + '@typescript-eslint/types': 8.53.0 2009 + eslint-visitor-keys: 4.2.1 2010 + 2011 + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260115.1': 2012 + optional: true 2013 + 2014 + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260115.1': 2015 + optional: true 2016 + 2017 + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260115.1': 2018 + optional: true 2019 + 2020 + '@typescript/native-preview-linux-arm@7.0.0-dev.20260115.1': 2021 + optional: true 2022 + 2023 + '@typescript/native-preview-linux-x64@7.0.0-dev.20260115.1': 2024 + optional: true 2025 + 2026 + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260115.1': 2027 + optional: true 2028 + 2029 + '@typescript/native-preview-win32-x64@7.0.0-dev.20260115.1': 2030 + optional: true 2031 + 2032 + '@typescript/native-preview@7.0.0-dev.20260115.1': 2033 + optionalDependencies: 2034 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260115.1 2035 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260115.1 2036 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260115.1 2037 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260115.1 2038 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260115.1 2039 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260115.1 2040 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260115.1 2041 + 2042 + '@vitest/expect@3.2.4': 2043 + dependencies: 2044 + '@types/chai': 5.2.3 2045 + '@vitest/spy': 3.2.4 2046 + '@vitest/utils': 3.2.4 2047 + chai: 5.3.3 2048 + tinyrainbow: 2.0.0 2049 + 2050 + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.7)(tsx@4.21.0))': 2051 + dependencies: 2052 + '@vitest/spy': 3.2.4 2053 + estree-walker: 3.0.3 2054 + magic-string: 0.30.21 2055 + optionalDependencies: 2056 + vite: 7.3.1(@types/node@22.19.7)(tsx@4.21.0) 2057 + 2058 + '@vitest/pretty-format@3.2.4': 2059 + dependencies: 2060 + tinyrainbow: 2.0.0 2061 + 2062 + '@vitest/runner@3.2.4': 2063 + dependencies: 2064 + '@vitest/utils': 3.2.4 2065 + pathe: 2.0.3 2066 + strip-literal: 3.1.0 2067 + 2068 + '@vitest/snapshot@3.2.4': 2069 + dependencies: 2070 + '@vitest/pretty-format': 3.2.4 2071 + magic-string: 0.30.21 2072 + pathe: 2.0.3 2073 + 2074 + '@vitest/spy@3.2.4': 2075 + dependencies: 2076 + tinyspy: 4.0.4 2077 + 2078 + '@vitest/utils@3.2.4': 2079 + dependencies: 2080 + '@vitest/pretty-format': 3.2.4 2081 + loupe: 3.2.1 2082 + tinyrainbow: 2.0.0 2083 + 2084 + abort-controller@3.0.0: 2085 + dependencies: 2086 + event-target-shim: 5.0.1 2087 + 2088 + accepts@2.0.0: 2089 + dependencies: 2090 + mime-types: 3.0.2 2091 + negotiator: 1.0.0 2092 + 2093 + acorn-jsx@5.3.2(acorn@8.15.0): 2094 + dependencies: 2095 + acorn: 8.15.0 2096 + 2097 + acorn@8.15.0: {} 2098 + 2099 + ajv-formats@3.0.1(ajv@8.17.1): 2100 + optionalDependencies: 2101 + ajv: 8.17.1 2102 + 2103 + ajv@6.12.6: 2104 + dependencies: 2105 + fast-deep-equal: 3.1.3 2106 + fast-json-stable-stringify: 2.1.0 2107 + json-schema-traverse: 0.4.1 2108 + uri-js: 4.4.1 2109 + 2110 + ajv@8.17.1: 2111 + dependencies: 2112 + fast-deep-equal: 3.1.3 2113 + fast-uri: 3.1.0 2114 + json-schema-traverse: 1.0.0 2115 + require-from-string: 2.0.2 2116 + 2117 + ansi-styles@4.3.0: 2118 + dependencies: 2119 + color-convert: 2.0.1 2120 + 2121 + argparse@2.0.1: {} 2122 + 2123 + assertion-error@2.0.1: {} 2124 + 2125 + atomic-sleep@1.0.0: {} 2126 + 2127 + balanced-match@1.0.2: {} 2128 + 2129 + base64-js@1.5.1: {} 2130 + 2131 + better-sqlite3@12.6.0: 2132 + dependencies: 2133 + bindings: 1.5.0 2134 + prebuild-install: 7.1.3 2135 + 2136 + bindings@1.5.0: 2137 + dependencies: 2138 + file-uri-to-path: 1.0.0 2139 + 2140 + bl@4.1.0: 2141 + dependencies: 2142 + buffer: 5.7.1 2143 + inherits: 2.0.4 2144 + readable-stream: 3.6.2 2145 + 2146 + body-parser@2.2.2: 2147 + dependencies: 2148 + bytes: 3.1.2 2149 + content-type: 1.0.5 2150 + debug: 4.4.3 2151 + http-errors: 2.0.1 2152 + iconv-lite: 0.7.2 2153 + on-finished: 2.4.1 2154 + qs: 6.14.1 2155 + raw-body: 3.0.2 2156 + type-is: 2.0.1 2157 + transitivePeerDependencies: 2158 + - supports-color 2159 + 2160 + brace-expansion@1.1.12: 2161 + dependencies: 2162 + balanced-match: 1.0.2 2163 + concat-map: 0.0.1 2164 + 2165 + brace-expansion@2.0.2: 2166 + dependencies: 2167 + balanced-match: 1.0.2 2168 + 2169 + buffer@5.7.1: 2170 + dependencies: 2171 + base64-js: 1.5.1 2172 + ieee754: 1.2.1 2173 + 2174 + buffer@6.0.3: 2175 + dependencies: 2176 + base64-js: 1.5.1 2177 + ieee754: 1.2.1 2178 + 2179 + bytes@3.1.2: {} 2180 + 2181 + cac@6.7.14: {} 2182 + 2183 + call-bind-apply-helpers@1.0.2: 2184 + dependencies: 2185 + es-errors: 1.3.0 2186 + function-bind: 1.1.2 2187 + 2188 + call-bound@1.0.4: 2189 + dependencies: 2190 + call-bind-apply-helpers: 1.0.2 2191 + get-intrinsic: 1.3.0 2192 + 2193 + callsites@3.1.0: {} 2194 + 2195 + chai@5.3.3: 2196 + dependencies: 2197 + assertion-error: 2.0.1 2198 + check-error: 2.1.3 2199 + deep-eql: 5.0.2 2200 + loupe: 3.2.1 2201 + pathval: 2.0.1 2202 + 2203 + chalk@4.1.2: 2204 + dependencies: 2205 + ansi-styles: 4.3.0 2206 + supports-color: 7.2.0 2207 + 2208 + check-error@2.1.3: {} 2209 + 2210 + chownr@1.1.4: {} 2211 + 2212 + color-convert@2.0.1: 2213 + dependencies: 2214 + color-name: 1.1.4 2215 + 2216 + color-name@1.1.4: {} 2217 + 2218 + colorette@2.0.20: {} 2219 + 2220 + concat-map@0.0.1: {} 2221 + 2222 + content-disposition@1.0.1: {} 2223 + 2224 + content-type@1.0.5: {} 2225 + 2226 + cookie-signature@1.2.2: {} 2227 + 2228 + cookie@0.7.2: {} 2229 + 2230 + cors@2.8.5: 2231 + dependencies: 2232 + object-assign: 4.1.1 2233 + vary: 1.1.2 2234 + 2235 + cross-spawn@7.0.6: 2236 + dependencies: 2237 + path-key: 3.1.1 2238 + shebang-command: 2.0.0 2239 + which: 2.0.2 2240 + 2241 + dateformat@4.6.3: {} 2242 + 2243 + debug@4.4.3: 2244 + dependencies: 2245 + ms: 2.1.3 2246 + 2247 + decompress-response@6.0.0: 2248 + dependencies: 2249 + mimic-response: 3.1.0 2250 + 2251 + deep-eql@5.0.2: {} 2252 + 2253 + deep-extend@0.6.0: {} 2254 + 2255 + deep-is@0.1.4: {} 2256 + 2257 + depd@2.0.0: {} 2258 + 2259 + detect-libc@2.1.2: {} 2260 + 2261 + dunder-proto@1.0.1: 2262 + dependencies: 2263 + call-bind-apply-helpers: 1.0.2 2264 + es-errors: 1.3.0 2265 + gopd: 1.2.0 2266 + 2267 + ee-first@1.1.1: {} 2268 + 2269 + encodeurl@2.0.0: {} 2270 + 2271 + end-of-stream@1.4.5: 2272 + dependencies: 2273 + once: 1.4.0 2274 + 2275 + env-paths@3.0.0: {} 2276 + 2277 + es-define-property@1.0.1: {} 2278 + 2279 + es-errors@1.3.0: {} 2280 + 2281 + es-module-lexer@1.7.0: {} 2282 + 2283 + es-object-atoms@1.1.1: 2284 + dependencies: 2285 + es-errors: 1.3.0 2286 + 2287 + esbuild@0.27.2: 2288 + optionalDependencies: 2289 + '@esbuild/aix-ppc64': 0.27.2 2290 + '@esbuild/android-arm': 0.27.2 2291 + '@esbuild/android-arm64': 0.27.2 2292 + '@esbuild/android-x64': 0.27.2 2293 + '@esbuild/darwin-arm64': 0.27.2 2294 + '@esbuild/darwin-x64': 0.27.2 2295 + '@esbuild/freebsd-arm64': 0.27.2 2296 + '@esbuild/freebsd-x64': 0.27.2 2297 + '@esbuild/linux-arm': 0.27.2 2298 + '@esbuild/linux-arm64': 0.27.2 2299 + '@esbuild/linux-ia32': 0.27.2 2300 + '@esbuild/linux-loong64': 0.27.2 2301 + '@esbuild/linux-mips64el': 0.27.2 2302 + '@esbuild/linux-ppc64': 0.27.2 2303 + '@esbuild/linux-riscv64': 0.27.2 2304 + '@esbuild/linux-s390x': 0.27.2 2305 + '@esbuild/linux-x64': 0.27.2 2306 + '@esbuild/netbsd-arm64': 0.27.2 2307 + '@esbuild/netbsd-x64': 0.27.2 2308 + '@esbuild/openbsd-arm64': 0.27.2 2309 + '@esbuild/openbsd-x64': 0.27.2 2310 + '@esbuild/openharmony-arm64': 0.27.2 2311 + '@esbuild/sunos-x64': 0.27.2 2312 + '@esbuild/win32-arm64': 0.27.2 2313 + '@esbuild/win32-ia32': 0.27.2 2314 + '@esbuild/win32-x64': 0.27.2 2315 + 2316 + escape-html@1.0.3: {} 2317 + 2318 + escape-string-regexp@4.0.0: {} 2319 + 2320 + eslint-scope@8.4.0: 2321 + dependencies: 2322 + esrecurse: 4.3.0 2323 + estraverse: 5.3.0 2324 + 2325 + eslint-visitor-keys@3.4.3: {} 2326 + 2327 + eslint-visitor-keys@4.2.1: {} 2328 + 2329 + eslint@9.39.2: 2330 + dependencies: 2331 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2) 2332 + '@eslint-community/regexpp': 4.12.2 2333 + '@eslint/config-array': 0.21.1 2334 + '@eslint/config-helpers': 0.4.2 2335 + '@eslint/core': 0.17.0 2336 + '@eslint/eslintrc': 3.3.3 2337 + '@eslint/js': 9.39.2 2338 + '@eslint/plugin-kit': 0.4.1 2339 + '@humanfs/node': 0.16.7 2340 + '@humanwhocodes/module-importer': 1.0.1 2341 + '@humanwhocodes/retry': 0.4.3 2342 + '@types/estree': 1.0.8 2343 + ajv: 6.12.6 2344 + chalk: 4.1.2 2345 + cross-spawn: 7.0.6 2346 + debug: 4.4.3 2347 + escape-string-regexp: 4.0.0 2348 + eslint-scope: 8.4.0 2349 + eslint-visitor-keys: 4.2.1 2350 + espree: 10.4.0 2351 + esquery: 1.7.0 2352 + esutils: 2.0.3 2353 + fast-deep-equal: 3.1.3 2354 + file-entry-cache: 8.0.0 2355 + find-up: 5.0.0 2356 + glob-parent: 6.0.2 2357 + ignore: 5.3.2 2358 + imurmurhash: 0.1.4 2359 + is-glob: 4.0.3 2360 + json-stable-stringify-without-jsonify: 1.0.1 2361 + lodash.merge: 4.6.2 2362 + minimatch: 3.1.2 2363 + natural-compare: 1.4.0 2364 + optionator: 0.9.4 2365 + transitivePeerDependencies: 2366 + - supports-color 2367 + 2368 + espree@10.4.0: 2369 + dependencies: 2370 + acorn: 8.15.0 2371 + acorn-jsx: 5.3.2(acorn@8.15.0) 2372 + eslint-visitor-keys: 4.2.1 2373 + 2374 + esquery@1.7.0: 2375 + dependencies: 2376 + estraverse: 5.3.0 2377 + 2378 + esrecurse@4.3.0: 2379 + dependencies: 2380 + estraverse: 5.3.0 2381 + 2382 + estraverse@5.3.0: {} 2383 + 2384 + estree-walker@3.0.3: 2385 + dependencies: 2386 + '@types/estree': 1.0.8 2387 + 2388 + esutils@2.0.3: {} 2389 + 2390 + etag@1.8.1: {} 2391 + 2392 + event-target-shim@5.0.1: {} 2393 + 2394 + events@3.3.0: {} 2395 + 2396 + eventsource-parser@3.0.6: {} 2397 + 2398 + eventsource@3.0.7: 2399 + dependencies: 2400 + eventsource-parser: 3.0.6 2401 + 2402 + expand-template@2.0.3: {} 2403 + 2404 + expect-type@1.3.0: {} 2405 + 2406 + express-rate-limit@7.5.1(express@5.2.1): 2407 + dependencies: 2408 + express: 5.2.1 2409 + 2410 + express@5.2.1: 2411 + dependencies: 2412 + accepts: 2.0.0 2413 + body-parser: 2.2.2 2414 + content-disposition: 1.0.1 2415 + content-type: 1.0.5 2416 + cookie: 0.7.2 2417 + cookie-signature: 1.2.2 2418 + debug: 4.4.3 2419 + depd: 2.0.0 2420 + encodeurl: 2.0.0 2421 + escape-html: 1.0.3 2422 + etag: 1.8.1 2423 + finalhandler: 2.1.1 2424 + fresh: 2.0.0 2425 + http-errors: 2.0.1 2426 + merge-descriptors: 2.0.0 2427 + mime-types: 3.0.2 2428 + on-finished: 2.4.1 2429 + once: 1.4.0 2430 + parseurl: 1.3.3 2431 + proxy-addr: 2.0.7 2432 + qs: 6.14.1 2433 + range-parser: 1.2.1 2434 + router: 2.2.0 2435 + send: 1.2.1 2436 + serve-static: 2.2.1 2437 + statuses: 2.0.2 2438 + type-is: 2.0.1 2439 + vary: 1.1.2 2440 + transitivePeerDependencies: 2441 + - supports-color 2442 + 2443 + fast-copy@3.0.2: {} 2444 + 2445 + fast-deep-equal@3.1.3: {} 2446 + 2447 + fast-json-stable-stringify@2.1.0: {} 2448 + 2449 + fast-levenshtein@2.0.6: {} 2450 + 2451 + fast-safe-stringify@2.1.1: {} 2452 + 2453 + fast-uri@3.1.0: {} 2454 + 2455 + fdir@6.5.0(picomatch@4.0.3): 2456 + optionalDependencies: 2457 + picomatch: 4.0.3 2458 + 2459 + file-entry-cache@8.0.0: 2460 + dependencies: 2461 + flat-cache: 4.0.1 2462 + 2463 + file-uri-to-path@1.0.0: {} 2464 + 2465 + finalhandler@2.1.1: 2466 + dependencies: 2467 + debug: 4.4.3 2468 + encodeurl: 2.0.0 2469 + escape-html: 1.0.3 2470 + on-finished: 2.4.1 2471 + parseurl: 1.3.3 2472 + statuses: 2.0.2 2473 + transitivePeerDependencies: 2474 + - supports-color 2475 + 2476 + find-up@5.0.0: 2477 + dependencies: 2478 + locate-path: 6.0.0 2479 + path-exists: 4.0.0 2480 + 2481 + flat-cache@4.0.1: 2482 + dependencies: 2483 + flatted: 3.3.3 2484 + keyv: 4.5.4 2485 + 2486 + flatted@3.3.3: {} 2487 + 2488 + forwarded@0.2.0: {} 2489 + 2490 + fresh@2.0.0: {} 2491 + 2492 + fs-constants@1.0.0: {} 2493 + 2494 + fsevents@2.3.3: 2495 + optional: true 2496 + 2497 + function-bind@1.1.2: {} 2498 + 2499 + get-intrinsic@1.3.0: 2500 + dependencies: 2501 + call-bind-apply-helpers: 1.0.2 2502 + es-define-property: 1.0.1 2503 + es-errors: 1.3.0 2504 + es-object-atoms: 1.1.1 2505 + function-bind: 1.1.2 2506 + get-proto: 1.0.1 2507 + gopd: 1.2.0 2508 + has-symbols: 1.1.0 2509 + hasown: 2.0.2 2510 + math-intrinsics: 1.1.0 2511 + 2512 + get-proto@1.0.1: 2513 + dependencies: 2514 + dunder-proto: 1.0.1 2515 + es-object-atoms: 1.1.1 2516 + 2517 + get-tsconfig@4.13.0: 2518 + dependencies: 2519 + resolve-pkg-maps: 1.0.0 2520 + 2521 + github-from-package@0.0.0: {} 2522 + 2523 + glob-parent@6.0.2: 2524 + dependencies: 2525 + is-glob: 4.0.3 2526 + 2527 + globals@14.0.0: {} 2528 + 2529 + gopd@1.2.0: {} 2530 + 2531 + has-flag@4.0.0: {} 2532 + 2533 + has-symbols@1.1.0: {} 2534 + 2535 + hasown@2.0.2: 2536 + dependencies: 2537 + function-bind: 1.1.2 2538 + 2539 + help-me@5.0.0: {} 2540 + 2541 + hono@4.11.4: {} 2542 + 2543 + http-errors@2.0.1: 2544 + dependencies: 2545 + depd: 2.0.0 2546 + inherits: 2.0.4 2547 + setprototypeof: 1.2.0 2548 + statuses: 2.0.2 2549 + toidentifier: 1.0.1 2550 + 2551 + iconv-lite@0.7.2: 2552 + dependencies: 2553 + safer-buffer: 2.1.2 2554 + 2555 + ieee754@1.2.1: {} 2556 + 2557 + ignore@5.3.2: {} 2558 + 2559 + ignore@7.0.5: {} 2560 + 2561 + import-fresh@3.3.1: 2562 + dependencies: 2563 + parent-module: 1.0.1 2564 + resolve-from: 4.0.0 2565 + 2566 + imurmurhash@0.1.4: {} 2567 + 2568 + inherits@2.0.4: {} 2569 + 2570 + ini@1.3.8: {} 2571 + 2572 + ipaddr.js@1.9.1: {} 2573 + 2574 + is-extglob@2.1.1: {} 2575 + 2576 + is-glob@4.0.3: 2577 + dependencies: 2578 + is-extglob: 2.1.1 2579 + 2580 + is-promise@4.0.0: {} 2581 + 2582 + isexe@2.0.0: {} 2583 + 2584 + jose@6.1.3: {} 2585 + 2586 + joycon@3.1.1: {} 2587 + 2588 + js-tokens@9.0.1: {} 2589 + 2590 + js-yaml@4.1.1: 2591 + dependencies: 2592 + argparse: 2.0.1 2593 + 2594 + json-buffer@3.0.1: {} 2595 + 2596 + json-schema-traverse@0.4.1: {} 2597 + 2598 + json-schema-traverse@1.0.0: {} 2599 + 2600 + json-schema-typed@8.0.2: {} 2601 + 2602 + json-stable-stringify-without-jsonify@1.0.1: {} 2603 + 2604 + keyv@4.5.4: 2605 + dependencies: 2606 + json-buffer: 3.0.1 2607 + 2608 + levn@0.4.1: 2609 + dependencies: 2610 + prelude-ls: 1.2.1 2611 + type-check: 0.4.0 2612 + 2613 + locate-path@6.0.0: 2614 + dependencies: 2615 + p-locate: 5.0.0 2616 + 2617 + lodash.merge@4.6.2: {} 2618 + 2619 + loupe@3.2.1: {} 2620 + 2621 + magic-string@0.30.21: 2622 + dependencies: 2623 + '@jridgewell/sourcemap-codec': 1.5.5 2624 + 2625 + math-intrinsics@1.1.0: {} 2626 + 2627 + media-typer@1.1.0: {} 2628 + 2629 + merge-descriptors@2.0.0: {} 2630 + 2631 + mime-db@1.54.0: {} 2632 + 2633 + mime-types@3.0.2: 2634 + dependencies: 2635 + mime-db: 1.54.0 2636 + 2637 + mimic-response@3.1.0: {} 2638 + 2639 + minimatch@3.1.2: 2640 + dependencies: 2641 + brace-expansion: 1.1.12 2642 + 2643 + minimatch@9.0.5: 2644 + dependencies: 2645 + brace-expansion: 2.0.2 2646 + 2647 + minimist@1.2.8: {} 2648 + 2649 + mkdirp-classic@0.5.3: {} 2650 + 2651 + ms@2.1.3: {} 2652 + 2653 + nanoid@3.3.11: {} 2654 + 2655 + nanoid@5.1.6: {} 2656 + 2657 + napi-build-utils@2.0.0: {} 2658 + 2659 + natural-compare@1.4.0: {} 2660 + 2661 + negotiator@1.0.0: {} 2662 + 2663 + node-abi@3.85.0: 2664 + dependencies: 2665 + semver: 7.7.3 2666 + 2667 + object-assign@4.1.1: {} 2668 + 2669 + object-inspect@1.13.4: {} 2670 + 2671 + on-exit-leak-free@2.1.2: {} 2672 + 2673 + on-finished@2.4.1: 2674 + dependencies: 2675 + ee-first: 1.1.1 2676 + 2677 + once@1.4.0: 2678 + dependencies: 2679 + wrappy: 1.0.2 2680 + 2681 + optionator@0.9.4: 2682 + dependencies: 2683 + deep-is: 0.1.4 2684 + fast-levenshtein: 2.0.6 2685 + levn: 0.4.1 2686 + prelude-ls: 1.2.1 2687 + type-check: 0.4.0 2688 + word-wrap: 1.2.5 2689 + 2690 + p-limit@3.1.0: 2691 + dependencies: 2692 + yocto-queue: 0.1.0 2693 + 2694 + p-locate@5.0.0: 2695 + dependencies: 2696 + p-limit: 3.1.0 2697 + 2698 + parent-module@1.0.1: 2699 + dependencies: 2700 + callsites: 3.1.0 2701 + 2702 + parseurl@1.3.3: {} 2703 + 2704 + path-exists@4.0.0: {} 2705 + 2706 + path-key@3.1.1: {} 2707 + 2708 + path-to-regexp@8.3.0: {} 2709 + 2710 + pathe@2.0.3: {} 2711 + 2712 + pathval@2.0.1: {} 2713 + 2714 + picocolors@1.1.1: {} 2715 + 2716 + picomatch@4.0.3: {} 2717 + 2718 + pino-abstract-transport@2.0.0: 2719 + dependencies: 2720 + split2: 4.2.0 2721 + 2722 + pino-pretty@11.3.0: 2723 + dependencies: 2724 + colorette: 2.0.20 2725 + dateformat: 4.6.3 2726 + fast-copy: 3.0.2 2727 + fast-safe-stringify: 2.1.1 2728 + help-me: 5.0.0 2729 + joycon: 3.1.1 2730 + minimist: 1.2.8 2731 + on-exit-leak-free: 2.1.2 2732 + pino-abstract-transport: 2.0.0 2733 + pump: 3.0.3 2734 + readable-stream: 4.7.0 2735 + secure-json-parse: 2.7.0 2736 + sonic-boom: 4.2.0 2737 + strip-json-comments: 3.1.1 2738 + 2739 + pino-std-serializers@7.1.0: {} 2740 + 2741 + pino@9.14.0: 2742 + dependencies: 2743 + '@pinojs/redact': 0.4.0 2744 + atomic-sleep: 1.0.0 2745 + on-exit-leak-free: 2.1.2 2746 + pino-abstract-transport: 2.0.0 2747 + pino-std-serializers: 7.1.0 2748 + process-warning: 5.0.0 2749 + quick-format-unescaped: 4.0.4 2750 + real-require: 0.2.0 2751 + safe-stable-stringify: 2.5.0 2752 + sonic-boom: 4.2.0 2753 + thread-stream: 3.1.0 2754 + 2755 + pkce-challenge@5.0.1: {} 2756 + 2757 + postcss@8.5.6: 2758 + dependencies: 2759 + nanoid: 3.3.11 2760 + picocolors: 1.1.1 2761 + source-map-js: 1.2.1 2762 + 2763 + prebuild-install@7.1.3: 2764 + dependencies: 2765 + detect-libc: 2.1.2 2766 + expand-template: 2.0.3 2767 + github-from-package: 0.0.0 2768 + minimist: 1.2.8 2769 + mkdirp-classic: 0.5.3 2770 + napi-build-utils: 2.0.0 2771 + node-abi: 3.85.0 2772 + pump: 3.0.3 2773 + rc: 1.2.8 2774 + simple-get: 4.0.1 2775 + tar-fs: 2.1.4 2776 + tunnel-agent: 0.6.0 2777 + 2778 + prelude-ls@1.2.1: {} 2779 + 2780 + prettier@3.8.0: {} 2781 + 2782 + process-warning@5.0.0: {} 2783 + 2784 + process@0.11.10: {} 2785 + 2786 + proxy-addr@2.0.7: 2787 + dependencies: 2788 + forwarded: 0.2.0 2789 + ipaddr.js: 1.9.1 2790 + 2791 + pump@3.0.3: 2792 + dependencies: 2793 + end-of-stream: 1.4.5 2794 + once: 1.4.0 2795 + 2796 + punycode@2.3.1: {} 2797 + 2798 + qs@6.14.1: 2799 + dependencies: 2800 + side-channel: 1.1.0 2801 + 2802 + quick-format-unescaped@4.0.4: {} 2803 + 2804 + range-parser@1.2.1: {} 2805 + 2806 + raw-body@3.0.2: 2807 + dependencies: 2808 + bytes: 3.1.2 2809 + http-errors: 2.0.1 2810 + iconv-lite: 0.7.2 2811 + unpipe: 1.0.0 2812 + 2813 + rc@1.2.8: 2814 + dependencies: 2815 + deep-extend: 0.6.0 2816 + ini: 1.3.8 2817 + minimist: 1.2.8 2818 + strip-json-comments: 2.0.1 2819 + 2820 + readable-stream@3.6.2: 2821 + dependencies: 2822 + inherits: 2.0.4 2823 + string_decoder: 1.3.0 2824 + util-deprecate: 1.0.2 2825 + 2826 + readable-stream@4.7.0: 2827 + dependencies: 2828 + abort-controller: 3.0.0 2829 + buffer: 6.0.3 2830 + events: 3.3.0 2831 + process: 0.11.10 2832 + string_decoder: 1.3.0 2833 + 2834 + real-require@0.2.0: {} 2835 + 2836 + require-from-string@2.0.2: {} 2837 + 2838 + resolve-from@4.0.0: {} 2839 + 2840 + resolve-pkg-maps@1.0.0: {} 2841 + 2842 + rollup@4.55.1: 2843 + dependencies: 2844 + '@types/estree': 1.0.8 2845 + optionalDependencies: 2846 + '@rollup/rollup-android-arm-eabi': 4.55.1 2847 + '@rollup/rollup-android-arm64': 4.55.1 2848 + '@rollup/rollup-darwin-arm64': 4.55.1 2849 + '@rollup/rollup-darwin-x64': 4.55.1 2850 + '@rollup/rollup-freebsd-arm64': 4.55.1 2851 + '@rollup/rollup-freebsd-x64': 4.55.1 2852 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 2853 + '@rollup/rollup-linux-arm-musleabihf': 4.55.1 2854 + '@rollup/rollup-linux-arm64-gnu': 4.55.1 2855 + '@rollup/rollup-linux-arm64-musl': 4.55.1 2856 + '@rollup/rollup-linux-loong64-gnu': 4.55.1 2857 + '@rollup/rollup-linux-loong64-musl': 4.55.1 2858 + '@rollup/rollup-linux-ppc64-gnu': 4.55.1 2859 + '@rollup/rollup-linux-ppc64-musl': 4.55.1 2860 + '@rollup/rollup-linux-riscv64-gnu': 4.55.1 2861 + '@rollup/rollup-linux-riscv64-musl': 4.55.1 2862 + '@rollup/rollup-linux-s390x-gnu': 4.55.1 2863 + '@rollup/rollup-linux-x64-gnu': 4.55.1 2864 + '@rollup/rollup-linux-x64-musl': 4.55.1 2865 + '@rollup/rollup-openbsd-x64': 4.55.1 2866 + '@rollup/rollup-openharmony-arm64': 4.55.1 2867 + '@rollup/rollup-win32-arm64-msvc': 4.55.1 2868 + '@rollup/rollup-win32-ia32-msvc': 4.55.1 2869 + '@rollup/rollup-win32-x64-gnu': 4.55.1 2870 + '@rollup/rollup-win32-x64-msvc': 4.55.1 2871 + fsevents: 2.3.3 2872 + 2873 + router@2.2.0: 2874 + dependencies: 2875 + debug: 4.4.3 2876 + depd: 2.0.0 2877 + is-promise: 4.0.0 2878 + parseurl: 1.3.3 2879 + path-to-regexp: 8.3.0 2880 + transitivePeerDependencies: 2881 + - supports-color 2882 + 2883 + safe-buffer@5.2.1: {} 2884 + 2885 + safe-stable-stringify@2.5.0: {} 2886 + 2887 + safer-buffer@2.1.2: {} 2888 + 2889 + secure-json-parse@2.7.0: {} 2890 + 2891 + semver@7.7.3: {} 2892 + 2893 + send@1.2.1: 2894 + dependencies: 2895 + debug: 4.4.3 2896 + encodeurl: 2.0.0 2897 + escape-html: 1.0.3 2898 + etag: 1.8.1 2899 + fresh: 2.0.0 2900 + http-errors: 2.0.1 2901 + mime-types: 3.0.2 2902 + ms: 2.1.3 2903 + on-finished: 2.4.1 2904 + range-parser: 1.2.1 2905 + statuses: 2.0.2 2906 + transitivePeerDependencies: 2907 + - supports-color 2908 + 2909 + serve-static@2.2.1: 2910 + dependencies: 2911 + encodeurl: 2.0.0 2912 + escape-html: 1.0.3 2913 + parseurl: 1.3.3 2914 + send: 1.2.1 2915 + transitivePeerDependencies: 2916 + - supports-color 2917 + 2918 + setprototypeof@1.2.0: {} 2919 + 2920 + shebang-command@2.0.0: 2921 + dependencies: 2922 + shebang-regex: 3.0.0 2923 + 2924 + shebang-regex@3.0.0: {} 2925 + 2926 + side-channel-list@1.0.0: 2927 + dependencies: 2928 + es-errors: 1.3.0 2929 + object-inspect: 1.13.4 2930 + 2931 + side-channel-map@1.0.1: 2932 + dependencies: 2933 + call-bound: 1.0.4 2934 + es-errors: 1.3.0 2935 + get-intrinsic: 1.3.0 2936 + object-inspect: 1.13.4 2937 + 2938 + side-channel-weakmap@1.0.2: 2939 + dependencies: 2940 + call-bound: 1.0.4 2941 + es-errors: 1.3.0 2942 + get-intrinsic: 1.3.0 2943 + object-inspect: 1.13.4 2944 + side-channel-map: 1.0.1 2945 + 2946 + side-channel@1.1.0: 2947 + dependencies: 2948 + es-errors: 1.3.0 2949 + object-inspect: 1.13.4 2950 + side-channel-list: 1.0.0 2951 + side-channel-map: 1.0.1 2952 + side-channel-weakmap: 1.0.2 2953 + 2954 + siginfo@2.0.0: {} 2955 + 2956 + simple-concat@1.0.1: {} 2957 + 2958 + simple-get@4.0.1: 2959 + dependencies: 2960 + decompress-response: 6.0.0 2961 + once: 1.4.0 2962 + simple-concat: 1.0.1 2963 + 2964 + sonic-boom@4.2.0: 2965 + dependencies: 2966 + atomic-sleep: 1.0.0 2967 + 2968 + source-map-js@1.2.1: {} 2969 + 2970 + split2@4.2.0: {} 2971 + 2972 + stackback@0.0.2: {} 2973 + 2974 + statuses@2.0.2: {} 2975 + 2976 + std-env@3.10.0: {} 2977 + 2978 + string_decoder@1.3.0: 2979 + dependencies: 2980 + safe-buffer: 5.2.1 2981 + 2982 + strip-json-comments@2.0.1: {} 2983 + 2984 + strip-json-comments@3.1.1: {} 2985 + 2986 + strip-literal@3.1.0: 2987 + dependencies: 2988 + js-tokens: 9.0.1 2989 + 2990 + supports-color@7.2.0: 2991 + dependencies: 2992 + has-flag: 4.0.0 2993 + 2994 + tar-fs@2.1.4: 2995 + dependencies: 2996 + chownr: 1.1.4 2997 + mkdirp-classic: 0.5.3 2998 + pump: 3.0.3 2999 + tar-stream: 2.2.0 3000 + 3001 + tar-stream@2.2.0: 3002 + dependencies: 3003 + bl: 4.1.0 3004 + end-of-stream: 1.4.5 3005 + fs-constants: 1.0.0 3006 + inherits: 2.0.4 3007 + readable-stream: 3.6.2 3008 + 3009 + thread-stream@3.1.0: 3010 + dependencies: 3011 + real-require: 0.2.0 3012 + 3013 + tinybench@2.9.0: {} 3014 + 3015 + tinyexec@0.3.2: {} 3016 + 3017 + tinyglobby@0.2.15: 3018 + dependencies: 3019 + fdir: 6.5.0(picomatch@4.0.3) 3020 + picomatch: 4.0.3 3021 + 3022 + tinypool@1.1.1: {} 3023 + 3024 + tinyrainbow@2.0.0: {} 3025 + 3026 + tinyspy@4.0.4: {} 3027 + 3028 + toidentifier@1.0.1: {} 3029 + 3030 + ts-api-utils@2.4.0(typescript@5.9.3): 3031 + dependencies: 3032 + typescript: 5.9.3 3033 + 3034 + tsx@4.21.0: 3035 + dependencies: 3036 + esbuild: 0.27.2 3037 + get-tsconfig: 4.13.0 3038 + optionalDependencies: 3039 + fsevents: 2.3.3 3040 + 3041 + tunnel-agent@0.6.0: 3042 + dependencies: 3043 + safe-buffer: 5.2.1 3044 + 3045 + type-check@0.4.0: 3046 + dependencies: 3047 + prelude-ls: 1.2.1 3048 + 3049 + type-is@2.0.1: 3050 + dependencies: 3051 + content-type: 1.0.5 3052 + media-typer: 1.1.0 3053 + mime-types: 3.0.2 3054 + 3055 + typescript-eslint@8.53.0(eslint@9.39.2)(typescript@5.9.3): 3056 + dependencies: 3057 + '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) 3058 + '@typescript-eslint/parser': 8.53.0(eslint@9.39.2)(typescript@5.9.3) 3059 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) 3060 + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2)(typescript@5.9.3) 3061 + eslint: 9.39.2 3062 + typescript: 5.9.3 3063 + transitivePeerDependencies: 3064 + - supports-color 3065 + 3066 + typescript@5.9.3: {} 3067 + 3068 + undici-types@6.21.0: {} 3069 + 3070 + unique-names-generator@4.7.1: {} 3071 + 3072 + unpipe@1.0.0: {} 3073 + 3074 + uri-js@4.4.1: 3075 + dependencies: 3076 + punycode: 2.3.1 3077 + 3078 + util-deprecate@1.0.2: {} 3079 + 3080 + uuid@13.0.0: {} 3081 + 3082 + vary@1.1.2: {} 3083 + 3084 + vite-node@3.2.4(@types/node@22.19.7)(tsx@4.21.0): 3085 + dependencies: 3086 + cac: 6.7.14 3087 + debug: 4.4.3 3088 + es-module-lexer: 1.7.0 3089 + pathe: 2.0.3 3090 + vite: 7.3.1(@types/node@22.19.7)(tsx@4.21.0) 3091 + transitivePeerDependencies: 3092 + - '@types/node' 3093 + - jiti 3094 + - less 3095 + - lightningcss 3096 + - sass 3097 + - sass-embedded 3098 + - stylus 3099 + - sugarss 3100 + - supports-color 3101 + - terser 3102 + - tsx 3103 + - yaml 3104 + 3105 + vite@7.3.1(@types/node@22.19.7)(tsx@4.21.0): 3106 + dependencies: 3107 + esbuild: 0.27.2 3108 + fdir: 6.5.0(picomatch@4.0.3) 3109 + picomatch: 4.0.3 3110 + postcss: 8.5.6 3111 + rollup: 4.55.1 3112 + tinyglobby: 0.2.15 3113 + optionalDependencies: 3114 + '@types/node': 22.19.7 3115 + fsevents: 2.3.3 3116 + tsx: 4.21.0 3117 + 3118 + vitest@3.2.4(@types/node@22.19.7)(tsx@4.21.0): 3119 + dependencies: 3120 + '@types/chai': 5.2.3 3121 + '@vitest/expect': 3.2.4 3122 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.7)(tsx@4.21.0)) 3123 + '@vitest/pretty-format': 3.2.4 3124 + '@vitest/runner': 3.2.4 3125 + '@vitest/snapshot': 3.2.4 3126 + '@vitest/spy': 3.2.4 3127 + '@vitest/utils': 3.2.4 3128 + chai: 5.3.3 3129 + debug: 4.4.3 3130 + expect-type: 1.3.0 3131 + magic-string: 0.30.21 3132 + pathe: 2.0.3 3133 + picomatch: 4.0.3 3134 + std-env: 3.10.0 3135 + tinybench: 2.9.0 3136 + tinyexec: 0.3.2 3137 + tinyglobby: 0.2.15 3138 + tinypool: 1.1.1 3139 + tinyrainbow: 2.0.0 3140 + vite: 7.3.1(@types/node@22.19.7)(tsx@4.21.0) 3141 + vite-node: 3.2.4(@types/node@22.19.7)(tsx@4.21.0) 3142 + why-is-node-running: 2.3.0 3143 + optionalDependencies: 3144 + '@types/node': 22.19.7 3145 + transitivePeerDependencies: 3146 + - jiti 3147 + - less 3148 + - lightningcss 3149 + - msw 3150 + - sass 3151 + - sass-embedded 3152 + - stylus 3153 + - sugarss 3154 + - supports-color 3155 + - terser 3156 + - tsx 3157 + - yaml 3158 + 3159 + which@2.0.2: 3160 + dependencies: 3161 + isexe: 2.0.0 3162 + 3163 + why-is-node-running@2.3.0: 3164 + dependencies: 3165 + siginfo: 2.0.0 3166 + stackback: 0.0.2 3167 + 3168 + word-wrap@1.2.5: {} 3169 + 3170 + wrappy@1.0.2: {} 3171 + 3172 + yocto-queue@0.1.0: {} 3173 + 3174 + zod-to-json-schema@3.25.1(zod@3.25.76): 3175 + dependencies: 3176 + zod: 3.25.76 3177 + 3178 + zod@3.25.76: {}
+163
scripts/smoke-test.ts
··· 1 + #!/usr/bin/env tsx 2 + /** 3 + * Smoke test for 9plan MCP server 4 + * 5 + * Spawns the server and sends actual MCP protocol messages to verify 6 + * the tools work correctly. 7 + */ 8 + 9 + import { spawn } from 'node:child_process'; 10 + import { createInterface } from 'node:readline'; 11 + import { join } from 'node:path'; 12 + 13 + let messageId = 1; 14 + 15 + function createMessage(method: string, params?: Record<string, unknown>) { 16 + return JSON.stringify({ 17 + jsonrpc: '2.0', 18 + id: messageId++, 19 + method, 20 + params, 21 + }); 22 + } 23 + 24 + async function runSmokeTest() { 25 + console.log('🚀 Starting 9plan MCP server smoke test...\n'); 26 + 27 + // Spawn the server 28 + const serverPath = join(import.meta.dirname, '..', 'dist', 'index.js'); 29 + const server = spawn('node', [serverPath], { 30 + stdio: ['pipe', 'pipe', 'pipe'], 31 + }); 32 + 33 + const responses: Record<number, unknown> = {}; 34 + let currentResolve: ((value: unknown) => void) | null = null; 35 + 36 + // Read responses from server 37 + const rl = createInterface({ input: server.stdout }); 38 + rl.on('line', (line) => { 39 + try { 40 + const msg = JSON.parse(line); 41 + if (msg.id) { 42 + responses[msg.id] = msg; 43 + if (currentResolve) { 44 + currentResolve(msg); 45 + currentResolve = null; 46 + } 47 + } 48 + } catch { 49 + // Ignore non-JSON lines 50 + } 51 + }); 52 + 53 + // Helper to send message and wait for response 54 + async function send(method: string, params?: Record<string, unknown>): Promise<unknown> { 55 + const msg = createMessage(method, params); 56 + 57 + return new Promise((resolve, reject) => { 58 + currentResolve = resolve; 59 + server.stdin.write(msg + '\n'); 60 + 61 + // Timeout after 10 seconds 62 + setTimeout(() => { 63 + if (currentResolve === resolve) { 64 + currentResolve = null; 65 + reject(new Error(`Timeout waiting for response to ${method}`)); 66 + } 67 + }, 10000); 68 + }); 69 + } 70 + 71 + try { 72 + // Step 1: Initialize 73 + console.log('📡 Initializing MCP connection...'); 74 + await send('initialize', { 75 + protocolVersion: '2024-11-05', 76 + capabilities: {}, 77 + clientInfo: { name: 'smoke-test', version: '1.0.0' }, 78 + }); 79 + console.log('✅ Initialize successful!\n'); 80 + 81 + // Step 2: List tools 82 + console.log('🔧 Listing available tools...'); 83 + const toolsResult = await send('tools/list', {}) as { result?: { tools?: Array<{ name: string }> } }; 84 + const tools = toolsResult.result?.tools || []; 85 + console.log(`✅ Found ${tools.length} tools:`); 86 + tools.forEach((t: { name: string }) => console.log(` - ${t.name}`)); 87 + console.log(); 88 + 89 + // Step 3: Create a session 90 + console.log('🎮 Creating a new session...'); 91 + const createResult = await send('tools/call', { 92 + name: '9plan_session_create', 93 + arguments: { task_description: 'Smoke test session' }, 94 + }) as { result?: { content?: Array<{ text?: string }> } }; 95 + const createText = createResult.result?.content?.[0]?.text || ''; 96 + console.log('✅ Session created!'); 97 + console.log(` ${createText.split('\n')[0]}`); 98 + console.log(); 99 + 100 + // Step 4: Add a plan to the queue 101 + console.log('📝 Adding a plan to the queue...'); 102 + const addResult = await send('tools/call', { 103 + name: '9plan_queue_add', 104 + arguments: { 105 + context: 'Testing the 9plan MCP server', 106 + goal: 'Verify all tools work correctly', 107 + approach: '1. Create session\n2. Add plans\n3. Complete plans', 108 + success_criteria: 'All operations succeed without errors', 109 + }, 110 + }) as { result?: { content?: Array<{ text?: string }> } }; 111 + const addText = addResult.result?.content?.[0]?.text || ''; 112 + console.log('✅ Plan added!'); 113 + console.log(` ${addText.split('\n').slice(0, 3).join('\n ')}`); 114 + console.log(); 115 + 116 + // Step 5: Pull the plan 117 + console.log('📤 Pulling plan from queue...'); 118 + const pullResult = await send('tools/call', { 119 + name: '9plan_queue_pull', 120 + arguments: {}, 121 + }) as { result?: { content?: Array<{ text?: string }> } }; 122 + const pullText = pullResult.result?.content?.[0]?.text || ''; 123 + console.log('✅ Plan pulled!'); 124 + console.log(` ${pullText.split('\n').slice(0, 3).join('\n ')}`); 125 + console.log(); 126 + 127 + // Step 6: Complete the plan 128 + console.log('🎯 Completing the plan...'); 129 + const completeResult = await send('tools/call', { 130 + name: '9plan_plan_complete', 131 + arguments: { 132 + outcome: 'All smoke tests passed successfully!', 133 + }, 134 + }) as { result?: { content?: Array<{ text?: string }> } }; 135 + const completeText = completeResult.result?.content?.[0]?.text || ''; 136 + console.log('✅ Plan completed!'); 137 + console.log(` ${completeText.split('\n').slice(0, 3).join('\n ')}`); 138 + console.log(); 139 + 140 + // Step 7: Search history 141 + console.log('🔍 Searching history...'); 142 + const searchResult = await send('tools/call', { 143 + name: '9plan_history_search', 144 + arguments: { query: 'verify tools' }, 145 + }) as { result?: { content?: Array<{ text?: string }> } }; 146 + const searchText = searchResult.result?.content?.[0]?.text || ''; 147 + console.log('✅ History search worked!'); 148 + console.log(` ${searchText.split('\n').slice(0, 5).join('\n ')}`); 149 + console.log(); 150 + 151 + console.log('═'.repeat(50)); 152 + console.log('🎉 ALL SMOKE TESTS PASSED! The server is working! 🎉'); 153 + console.log('═'.repeat(50)); 154 + 155 + } catch (error) { 156 + console.error('❌ Smoke test failed:', error); 157 + process.exit(1); 158 + } finally { 159 + server.kill(); 160 + } 161 + } 162 + 163 + runSmokeTest().catch(console.error);
+127
src/container.ts
··· 1 + /** 2 + * Dependency injection container 3 + * 4 + * Simple factory functions for wiring up production dependencies. 5 + * No DI framework needed - just straightforward function composition. 6 + */ 7 + 8 + import { join } from "node:path"; 9 + import { existsSync, mkdirSync, readdirSync } from "node:fs"; 10 + import { logger } from "./logger.js"; 11 + import { env } from "./env.js"; 12 + import { SessionStore } from "./db/session-store.js"; 13 + import { PlanFileHandlerImpl } from "./files/plan-files.js"; 14 + import { generatePlanId } from "./generators/plan-id.js"; 15 + import { generateSessionName } from "./generators/session-name.js"; 16 + import { NinePlanError } from "./types.js"; 17 + import type { PlanFileHandler } from "./types.js"; 18 + 19 + /** 20 + * Create a PlanFileHandler for a session 21 + */ 22 + export function createPlanFileHandler(sessionPath: string): PlanFileHandler { 23 + return new PlanFileHandlerImpl(sessionPath, { logger }); 24 + } 25 + 26 + /** 27 + * Create a SessionStore for a session 28 + */ 29 + export function createSessionStore( 30 + sessionName: string, 31 + sessionPath: string, 32 + ): SessionStore { 33 + return new SessionStore(sessionName, sessionPath, { 34 + logger, 35 + generatePlanId, 36 + planFiles: createPlanFileHandler(sessionPath), 37 + }); 38 + } 39 + 40 + /** 41 + * Get the path to a session directory 42 + */ 43 + export function getSessionPath(sessionName: string): string { 44 + return join(env.NINEPLAN_SESSIONS_PATH, sessionName); 45 + } 46 + 47 + /** 48 + * Check if a session exists 49 + */ 50 + export function sessionExists(sessionName: string): boolean { 51 + const path = getSessionPath(sessionName); 52 + return existsSync(path); 53 + } 54 + 55 + /** 56 + * Create a new session with a unique name 57 + */ 58 + export function createNewSession(taskDescription?: string): { 59 + sessionName: string; 60 + store: SessionStore; 61 + } { 62 + // Ensure sessions directory exists 63 + if (!existsSync(env.NINEPLAN_SESSIONS_PATH)) { 64 + mkdirSync(env.NINEPLAN_SESSIONS_PATH, { recursive: true }); 65 + } 66 + 67 + // Generate unique name with collision checking 68 + let sessionName: string; 69 + let attempts = 0; 70 + const maxAttempts = 100; 71 + 72 + do { 73 + sessionName = generateSessionName(); 74 + attempts++; 75 + if (attempts >= maxAttempts) { 76 + throw new NinePlanError( 77 + "DATABASE_ERROR", 78 + "Failed to generate unique session name after 100 attempts", 79 + "This is extremely unlikely. Please report this issue.", 80 + ); 81 + } 82 + } while (sessionExists(sessionName)); 83 + 84 + // Create session directory and store 85 + const sessionPath = getSessionPath(sessionName); 86 + mkdirSync(sessionPath, { recursive: true }); 87 + 88 + const store = createSessionStore(sessionName, sessionPath); 89 + store.createSession(taskDescription); 90 + 91 + logger.info({ sessionName, sessionPath }, "New session created"); 92 + 93 + return { sessionName, store }; 94 + } 95 + 96 + /** 97 + * Resume an existing session 98 + */ 99 + export function resumeSession(sessionName: string): SessionStore { 100 + if (!sessionExists(sessionName)) { 101 + throw new NinePlanError( 102 + "SESSION_NOT_FOUND", 103 + `Session not found: ${sessionName}`, 104 + `Check the session name and try again. Session names are case-sensitive.\nAvailable sessions can be found in: ${env.NINEPLAN_SESSIONS_PATH}`, 105 + ); 106 + } 107 + 108 + const sessionPath = getSessionPath(sessionName); 109 + const store = createSessionStore(sessionName, sessionPath); 110 + 111 + logger.info({ sessionName }, "Session resumed"); 112 + 113 + return store; 114 + } 115 + 116 + /** 117 + * List all available sessions 118 + */ 119 + export function listSessions(): string[] { 120 + if (!existsSync(env.NINEPLAN_SESSIONS_PATH)) { 121 + return []; 122 + } 123 + 124 + return readdirSync(env.NINEPLAN_SESSIONS_PATH, { withFileTypes: true }) 125 + .filter((dirent) => dirent.isDirectory()) 126 + .map((dirent) => dirent.name); 127 + }
+68
src/db/schema.sql
··· 1 + -- 9plan SQLite Schema 2 + -- Requires SQLite with FTS5 support (built into Node.js 22.5.0+) 3 + 4 + PRAGMA foreign_keys = ON; 5 + 6 + -- Sessions table 7 + CREATE TABLE IF NOT EXISTS sessions ( 8 + name TEXT PRIMARY KEY, 9 + task_description TEXT, 10 + created_at TEXT DEFAULT (datetime('now')) 11 + ) STRICT; 12 + 13 + -- Plans table with all lifecycle states 14 + CREATE TABLE IF NOT EXISTS plans ( 15 + id TEXT PRIMARY KEY, 16 + session_name TEXT NOT NULL REFERENCES sessions(name) ON DELETE CASCADE, 17 + status TEXT NOT NULL CHECK (status IN ('queued', 'active', 'completed', 'discarded')), 18 + queue_position INTEGER, 19 + 20 + goal TEXT NOT NULL, 21 + context TEXT, 22 + inputs TEXT, 23 + outputs TEXT, 24 + approach TEXT, 25 + success_criteria TEXT, 26 + notes TEXT, 27 + outcome TEXT, 28 + 29 + created_at TEXT DEFAULT (datetime('now')), 30 + completed_at TEXT 31 + ) STRICT; 32 + 33 + -- Index for efficient queue queries (queued plans ordered by position) 34 + CREATE INDEX IF NOT EXISTS idx_plans_queue 35 + ON plans(session_name, status, queue_position) 36 + WHERE status = 'queued'; 37 + 38 + -- Index for finding the active plan quickly 39 + CREATE INDEX IF NOT EXISTS idx_plans_active 40 + ON plans(session_name, status) 41 + WHERE status = 'active'; 42 + 43 + -- FTS5 virtual table for history search 44 + -- Searches across goal, context, inputs, outputs, and outcome 45 + CREATE VIRTUAL TABLE IF NOT EXISTS plans_fts USING fts5( 46 + id, goal, context, inputs, outputs, outcome, 47 + content='plans', content_rowid='rowid' 48 + ); 49 + 50 + -- Trigger: Keep FTS in sync on INSERT 51 + CREATE TRIGGER IF NOT EXISTS plans_ai AFTER INSERT ON plans BEGIN 52 + INSERT INTO plans_fts(rowid, id, goal, context, inputs, outputs, outcome) 53 + VALUES (new.rowid, new.id, new.goal, new.context, new.inputs, new.outputs, new.outcome); 54 + END; 55 + 56 + -- Trigger: Keep FTS in sync on DELETE 57 + CREATE TRIGGER IF NOT EXISTS plans_ad AFTER DELETE ON plans BEGIN 58 + INSERT INTO plans_fts(plans_fts, rowid, id, goal, context, inputs, outputs, outcome) 59 + VALUES ('delete', old.rowid, old.id, old.goal, old.context, old.inputs, old.outputs, old.outcome); 60 + END; 61 + 62 + -- Trigger: Keep FTS in sync on UPDATE 63 + CREATE TRIGGER IF NOT EXISTS plans_au AFTER UPDATE ON plans BEGIN 64 + INSERT INTO plans_fts(plans_fts, rowid, id, goal, context, inputs, outputs, outcome) 65 + VALUES ('delete', old.rowid, old.id, old.goal, old.context, old.inputs, old.outputs, old.outcome); 66 + INSERT INTO plans_fts(rowid, id, goal, context, inputs, outputs, outcome) 67 + VALUES (new.rowid, new.id, new.goal, new.context, new.inputs, new.outputs, new.outcome); 68 + END;
+576
src/db/session-store.ts
··· 1 + /** 2 + * Session store - SQLite-backed session and plan management 3 + * 4 + * Handles all database operations for sessions, plans, queue management, 5 + * and history search via FTS5. 6 + */ 7 + 8 + import Database from "better-sqlite3"; 9 + import { existsSync, mkdirSync } from "node:fs"; 10 + import { join } from "node:path"; 11 + import type { Logger } from "pino"; 12 + import type { 13 + Plan, 14 + PlanInput, 15 + PlanContent, 16 + QueuedPlan, 17 + SessionState, 18 + HistoryMatch, 19 + SessionStoreDeps, 20 + PlanFileHandler, 21 + } from "../types.js"; 22 + import { NinePlanError } from "../types.js"; 23 + 24 + // Inline schema to avoid file read issues after compilation 25 + const SCHEMA = ` 26 + -- 9plan SQLite Schema 27 + PRAGMA foreign_keys = ON; 28 + 29 + -- Sessions table 30 + CREATE TABLE IF NOT EXISTS sessions ( 31 + name TEXT PRIMARY KEY, 32 + task_description TEXT, 33 + created_at TEXT DEFAULT (datetime('now')) 34 + ) STRICT; 35 + 36 + -- Plans table with all lifecycle states 37 + CREATE TABLE IF NOT EXISTS plans ( 38 + id TEXT PRIMARY KEY, 39 + session_name TEXT NOT NULL REFERENCES sessions(name) ON DELETE CASCADE, 40 + status TEXT NOT NULL CHECK (status IN ('queued', 'active', 'completed', 'discarded')), 41 + queue_position INTEGER, 42 + 43 + goal TEXT NOT NULL, 44 + context TEXT, 45 + inputs TEXT, 46 + outputs TEXT, 47 + approach TEXT, 48 + success_criteria TEXT, 49 + notes TEXT, 50 + outcome TEXT, 51 + 52 + created_at TEXT DEFAULT (datetime('now')), 53 + completed_at TEXT 54 + ) STRICT; 55 + 56 + -- Index for efficient queue queries 57 + CREATE INDEX IF NOT EXISTS idx_plans_queue 58 + ON plans(session_name, status, queue_position) 59 + WHERE status = 'queued'; 60 + 61 + -- Index for finding the active plan quickly 62 + CREATE INDEX IF NOT EXISTS idx_plans_active 63 + ON plans(session_name, status) 64 + WHERE status = 'active'; 65 + 66 + -- FTS5 virtual table for history search 67 + CREATE VIRTUAL TABLE IF NOT EXISTS plans_fts USING fts5( 68 + id, goal, context, inputs, outputs, outcome, 69 + content='plans', content_rowid='rowid' 70 + ); 71 + 72 + -- Trigger: Keep FTS in sync on INSERT 73 + CREATE TRIGGER IF NOT EXISTS plans_ai AFTER INSERT ON plans BEGIN 74 + INSERT INTO plans_fts(rowid, id, goal, context, inputs, outputs, outcome) 75 + VALUES (new.rowid, new.id, new.goal, new.context, new.inputs, new.outputs, new.outcome); 76 + END; 77 + 78 + -- Trigger: Keep FTS in sync on DELETE 79 + CREATE TRIGGER IF NOT EXISTS plans_ad AFTER DELETE ON plans BEGIN 80 + INSERT INTO plans_fts(plans_fts, rowid, id, goal, context, inputs, outputs, outcome) 81 + VALUES ('delete', old.rowid, old.id, old.goal, old.context, old.inputs, old.outputs, old.outcome); 82 + END; 83 + 84 + -- Trigger: Keep FTS in sync on UPDATE 85 + CREATE TRIGGER IF NOT EXISTS plans_au AFTER UPDATE ON plans BEGIN 86 + INSERT INTO plans_fts(plans_fts, rowid, id, goal, context, inputs, outputs, outcome) 87 + VALUES ('delete', old.rowid, old.id, old.goal, old.context, old.inputs, old.outputs, old.outcome); 88 + INSERT INTO plans_fts(rowid, id, goal, context, inputs, outputs, outcome) 89 + VALUES (new.rowid, new.id, new.goal, new.context, new.inputs, new.outputs, new.outcome); 90 + END; 91 + `; 92 + 93 + /** 94 + * Session store - manages a single session's data 95 + */ 96 + export class SessionStore { 97 + private readonly db: Database.Database; 98 + private readonly log: Logger; 99 + private readonly generatePlanId: () => string; 100 + private readonly planFiles: PlanFileHandler; 101 + 102 + constructor( 103 + private readonly sessionName: string, 104 + private readonly sessionPath: string, 105 + deps: SessionStoreDeps, 106 + ) { 107 + this.log = deps.logger.child({ session: sessionName }); 108 + this.generatePlanId = deps.generatePlanId; 109 + this.planFiles = deps.planFiles; 110 + 111 + // Ensure session directory exists 112 + if (!existsSync(sessionPath)) { 113 + mkdirSync(sessionPath, { recursive: true }); 114 + } 115 + 116 + // Initialize database 117 + const dbPath = join(sessionPath, "session.db"); 118 + this.db = new Database(dbPath); 119 + this.initializeSchema(); 120 + } 121 + 122 + /** 123 + * Initialize the database schema 124 + */ 125 + private initializeSchema(): void { 126 + this.db.exec(SCHEMA); 127 + this.log.debug("Database schema initialized"); 128 + } 129 + 130 + /** 131 + * Create the session record (call once when creating a new session) 132 + */ 133 + createSession(taskDescription?: string): void { 134 + const stmt = this.db.prepare( 135 + "INSERT INTO sessions (name, task_description) VALUES (?, ?)", 136 + ); 137 + stmt.run(this.sessionName, taskDescription ?? null); 138 + this.log.info({ taskDescription }, "Session created"); 139 + } 140 + 141 + /** 142 + * Get the session's task description 143 + */ 144 + getTaskDescription(): string | null { 145 + const stmt = this.db.prepare( 146 + "SELECT task_description FROM sessions WHERE name = ?", 147 + ); 148 + const row = stmt.get(this.sessionName) as 149 + | { task_description: string | null } 150 + | undefined; 151 + return row?.task_description ?? null; 152 + } 153 + 154 + /** 155 + * Get session path 156 + */ 157 + getSessionPath(): string { 158 + return this.sessionPath; 159 + } 160 + 161 + // =========================================================================== 162 + // Queue Operations 163 + // =========================================================================== 164 + 165 + /** 166 + * Add a plan to the queue 167 + */ 168 + addPlan(input: PlanInput, position: "front" | "back" = "back"): Plan { 169 + const id = this.generatePlanId(); 170 + const queuePosition = this.getNextQueuePosition(position); 171 + 172 + // Shift existing plans if inserting at front 173 + if (position === "front") { 174 + this.shiftQueuePositions(); 175 + } 176 + 177 + // Insert into database 178 + const stmt = this.db.prepare(` 179 + INSERT INTO plans ( 180 + id, session_name, status, queue_position, 181 + goal, context, inputs, outputs, approach, success_criteria, notes 182 + ) VALUES (?, ?, 'queued', ?, ?, ?, ?, ?, ?, ?, ?) 183 + `); 184 + 185 + stmt.run( 186 + id, 187 + this.sessionName, 188 + queuePosition, 189 + input.goal, 190 + input.context, 191 + input.inputs ?? null, 192 + input.outputs ?? null, 193 + input.approach, 194 + input.successCriteria, 195 + null, // notes start empty 196 + ); 197 + 198 + // Write plan file 199 + const content: PlanContent = { 200 + context: input.context, 201 + goal: input.goal, 202 + inputs: input.inputs ?? "", 203 + outputs: input.outputs ?? "", 204 + approach: input.approach, 205 + successCriteria: input.successCriteria, 206 + notes: "", 207 + }; 208 + this.planFiles.write(id, content); 209 + 210 + this.log.info({ planId: id, goal: input.goal, position }, "Plan added"); 211 + 212 + const plan = this.getPlanById(id); 213 + if (!plan) { 214 + throw new NinePlanError( 215 + "DATABASE_ERROR", 216 + `Failed to retrieve plan ${id} after creation`, 217 + "This should not happen. Please check the database.", 218 + ); 219 + } 220 + return plan; 221 + } 222 + 223 + /** 224 + * Pull the front plan from queue and make it active 225 + */ 226 + pullPlan(): Plan { 227 + // Check for existing active plan 228 + const active = this.getActivePlan(); 229 + if (active) { 230 + throw new NinePlanError( 231 + "PLAN_ALREADY_ACTIVE", 232 + `Plan ${active.id} is already active`, 233 + "Complete, defer, or discard the active plan before pulling another.", 234 + ); 235 + } 236 + 237 + // Get front of queue 238 + const front = this.db 239 + .prepare( 240 + `SELECT id FROM plans 241 + WHERE session_name = ? AND status = 'queued' 242 + ORDER BY queue_position ASC 243 + LIMIT 1`, 244 + ) 245 + .get(this.sessionName) as { id: string } | undefined; 246 + 247 + if (!front) { 248 + throw new NinePlanError( 249 + "QUEUE_EMPTY", 250 + "The queue is empty", 251 + "Use 9plan_history_search to review completed work.", 252 + ); 253 + } 254 + 255 + // Mark as active 256 + this.db 257 + .prepare( 258 + `UPDATE plans SET status = 'active', queue_position = NULL WHERE id = ?`, 259 + ) 260 + .run(front.id); 261 + 262 + // Renumber remaining queue 263 + this.renumberQueue(); 264 + 265 + const plan = this.getPlanById(front.id); 266 + if (!plan) { 267 + throw new NinePlanError( 268 + "DATABASE_ERROR", 269 + `Failed to retrieve plan ${front.id} after pulling`, 270 + "This should not happen. Please check the database.", 271 + ); 272 + } 273 + this.log.info({ planId: plan.id, goal: plan.goal }, "Plan pulled"); 274 + 275 + return plan; 276 + } 277 + 278 + // =========================================================================== 279 + // Plan Lifecycle 280 + // =========================================================================== 281 + 282 + /** 283 + * Complete the active plan 284 + */ 285 + completePlan(outcome: string): void { 286 + const active = this.requireActivePlan(); 287 + 288 + // Update database 289 + this.db 290 + .prepare( 291 + `UPDATE plans 292 + SET status = 'completed', outcome = ?, completed_at = datetime('now') 293 + WHERE id = ?`, 294 + ) 295 + .run(outcome, active.id); 296 + 297 + // Delete plan file (content now lives in database/FTS) 298 + this.planFiles.delete(active.id); 299 + 300 + this.log.info({ planId: active.id, goal: active.goal }, "Plan completed"); 301 + } 302 + 303 + /** 304 + * Defer the active plan back to queue 305 + */ 306 + deferPlan(reason: string, position: "front" | "back" = "back"): void { 307 + const active = this.requireActivePlan(); 308 + 309 + // Append reason to notes 310 + this.planFiles.appendToNotes(active.id, `Deferred: ${reason}`); 311 + 312 + // Update notes in database too 313 + const content = this.planFiles.read(active.id); 314 + const queuePosition = this.getNextQueuePosition(position); 315 + 316 + // Shift if inserting at front 317 + if (position === "front") { 318 + this.shiftQueuePositions(); 319 + } 320 + 321 + // Move back to queued status 322 + this.db 323 + .prepare( 324 + `UPDATE plans 325 + SET status = 'queued', queue_position = ?, notes = ? 326 + WHERE id = ?`, 327 + ) 328 + .run(queuePosition, content.notes, active.id); 329 + 330 + this.log.info({ planId: active.id, reason, position }, "Plan deferred"); 331 + } 332 + 333 + /** 334 + * Discard the active plan (no history record) 335 + */ 336 + discardPlan(reason: string): void { 337 + const active = this.requireActivePlan(); 338 + 339 + // Mark as discarded (NOT completed, won't appear in history search) 340 + this.db 341 + .prepare(`UPDATE plans SET status = 'discarded' WHERE id = ?`) 342 + .run(active.id); 343 + 344 + // Delete plan file 345 + this.planFiles.delete(active.id); 346 + 347 + this.log.info({ planId: active.id, reason }, "Plan discarded"); 348 + } 349 + 350 + // =========================================================================== 351 + // History Operations 352 + // =========================================================================== 353 + 354 + /** 355 + * Search completed plans using FTS5 356 + */ 357 + searchHistory(query: string, maxResults = 10): HistoryMatch[] { 358 + // Use FTS5 MATCH with BM25 ranking 359 + const stmt = this.db.prepare(` 360 + SELECT 361 + plans.id, 362 + plans.goal, 363 + plans.outcome, 364 + bm25(plans_fts) as score 365 + FROM plans_fts 366 + JOIN plans ON plans_fts.id = plans.id 367 + WHERE plans.session_name = ? 368 + AND plans.status = 'completed' 369 + AND plans_fts MATCH ? 370 + ORDER BY score 371 + LIMIT ? 372 + `); 373 + 374 + const rows = stmt.all(this.sessionName, query, maxResults) as { 375 + id: string; 376 + goal: string; 377 + outcome: string | null; 378 + score: number; 379 + }[]; 380 + 381 + return rows.map((row) => ({ 382 + id: row.id, 383 + goal: row.goal, 384 + outcome: row.outcome, 385 + relevanceScore: Math.abs(row.score), // BM25 returns negative scores 386 + })); 387 + } 388 + 389 + /** 390 + * Get a specific completed plan by ID 391 + */ 392 + getHistoryPlan(planId: string): Plan | null { 393 + const stmt = this.db.prepare( 394 + `SELECT * FROM plans WHERE id = ? AND session_name = ? AND status = 'completed'`, 395 + ); 396 + const row = stmt.get(planId, this.sessionName); 397 + return row ? this.rowToPlan(row) : null; 398 + } 399 + 400 + // =========================================================================== 401 + // State Queries 402 + // =========================================================================== 403 + 404 + /** 405 + * Get the currently active plan 406 + */ 407 + getActivePlan(): Plan | null { 408 + const stmt = this.db.prepare( 409 + `SELECT * FROM plans WHERE session_name = ? AND status = 'active'`, 410 + ); 411 + const row = stmt.get(this.sessionName); 412 + return row ? this.rowToPlan(row) : null; 413 + } 414 + 415 + /** 416 + * Get the queue contents 417 + */ 418 + getQueue(): QueuedPlan[] { 419 + const stmt = this.db.prepare(` 420 + SELECT id, goal, queue_position 421 + FROM plans 422 + WHERE session_name = ? AND status = 'queued' 423 + ORDER BY queue_position ASC 424 + `); 425 + 426 + const rows = stmt.all(this.sessionName) as { 427 + id: string; 428 + goal: string; 429 + queue_position: number; 430 + }[]; 431 + 432 + return rows.map((row) => ({ 433 + id: row.id, 434 + goal: row.goal, 435 + queuePosition: row.queue_position, 436 + })); 437 + } 438 + 439 + /** 440 + * Get count of completed plans 441 + */ 442 + getCompletedCount(): number { 443 + const stmt = this.db.prepare( 444 + `SELECT COUNT(*) as count FROM plans WHERE session_name = ? AND status = 'completed'`, 445 + ); 446 + const row = stmt.get(this.sessionName) as { count: number }; 447 + return row.count; 448 + } 449 + 450 + /** 451 + * Get full session state for resume 452 + */ 453 + getState(): SessionState { 454 + const activePlan = this.getActivePlan(); 455 + 456 + return { 457 + sessionName: this.sessionName, 458 + sessionPath: this.sessionPath, 459 + taskDescription: this.getTaskDescription(), 460 + queue: this.getQueue(), 461 + activePlan: activePlan 462 + ? { 463 + id: activePlan.id, 464 + goal: activePlan.goal, 465 + filePath: this.planFiles.getFilePath(activePlan.id), 466 + } 467 + : null, 468 + completedCount: this.getCompletedCount(), 469 + }; 470 + } 471 + 472 + // =========================================================================== 473 + // Helpers 474 + // =========================================================================== 475 + 476 + /** 477 + * Get a plan by ID 478 + */ 479 + private getPlanById(id: string): Plan | null { 480 + const stmt = this.db.prepare( 481 + `SELECT * FROM plans WHERE id = ? AND session_name = ?`, 482 + ); 483 + const row = stmt.get(id, this.sessionName); 484 + return row ? this.rowToPlan(row) : null; 485 + } 486 + 487 + /** 488 + * Get the active plan, throwing if none exists 489 + */ 490 + private requireActivePlan(): Plan { 491 + const active = this.getActivePlan(); 492 + if (!active) { 493 + throw new NinePlanError( 494 + "NO_ACTIVE_PLAN", 495 + "No plan is currently active", 496 + "Use 9plan_queue_pull to get a plan first.", 497 + ); 498 + } 499 + return active; 500 + } 501 + 502 + /** 503 + * Get the next queue position 504 + */ 505 + private getNextQueuePosition(position: "front" | "back"): number { 506 + if (position === "front") { 507 + return 1; 508 + } 509 + 510 + const stmt = this.db.prepare(` 511 + SELECT MAX(queue_position) as max_pos 512 + FROM plans 513 + WHERE session_name = ? AND status = 'queued' 514 + `); 515 + const row = stmt.get(this.sessionName) as { max_pos: number | null }; 516 + return (row.max_pos ?? 0) + 1; 517 + } 518 + 519 + /** 520 + * Shift all queue positions up by 1 (for front insertion) 521 + */ 522 + private shiftQueuePositions(): void { 523 + this.db 524 + .prepare( 525 + `UPDATE plans 526 + SET queue_position = queue_position + 1 527 + WHERE session_name = ? AND status = 'queued'`, 528 + ) 529 + .run(this.sessionName); 530 + } 531 + 532 + /** 533 + * Renumber queue to be contiguous starting at 1 534 + */ 535 + private renumberQueue(): void { 536 + const queue = this.getQueue(); 537 + for (let i = 0; i < queue.length; i++) { 538 + const plan = queue[i]; 539 + if (plan) { 540 + this.db 541 + .prepare(`UPDATE plans SET queue_position = ? WHERE id = ?`) 542 + .run(i + 1, plan.id); 543 + } 544 + } 545 + } 546 + 547 + /** 548 + * Convert a database row to a Plan object 549 + */ 550 + private rowToPlan(row: unknown): Plan { 551 + const r = row as Record<string, unknown>; 552 + return { 553 + id: r.id as string, 554 + sessionName: r.session_name as string, 555 + status: r.status as Plan["status"], 556 + queuePosition: r.queue_position as number | null, 557 + goal: r.goal as string, 558 + context: r.context as string | null, 559 + inputs: r.inputs as string | null, 560 + outputs: r.outputs as string | null, 561 + approach: r.approach as string | null, 562 + successCriteria: r.success_criteria as string | null, 563 + notes: r.notes as string | null, 564 + outcome: r.outcome as string | null, 565 + createdAt: r.created_at as string, 566 + completedAt: r.completed_at as string | null, 567 + }; 568 + } 569 + 570 + /** 571 + * Close the database connection 572 + */ 573 + close(): void { 574 + this.db.close(); 575 + } 576 + }
+37
src/env.ts
··· 1 + /** 2 + * Environment configuration with Zod validation 3 + */ 4 + 5 + import { z } from "zod"; 6 + import envPaths from "env-paths"; 7 + import { join } from "node:path"; 8 + 9 + // Get cross-platform app directories 10 + const paths = envPaths("9plan", { suffix: "" }); 11 + 12 + // Environment schema 13 + const envSchema = z.object({ 14 + // Session storage location 15 + NINEPLAN_SESSIONS_PATH: z.string().default(join(paths.data, "sessions")), 16 + 17 + // Logging level 18 + NINEPLAN_LOG_LEVEL: z 19 + .enum(["debug", "info", "warn", "error"]) 20 + .default("info"), 21 + 22 + // Transport mode 23 + NINEPLAN_TRANSPORT: z.enum(["stdio", "http"]).default("stdio"), 24 + 25 + // HTTP settings (only used when transport is 'http') 26 + NINEPLAN_HTTP_PORT: z.coerce.number().default(8080), 27 + NINEPLAN_HTTP_HOST: z.string().default("127.0.0.1"), 28 + }); 29 + 30 + // Parse and export environment 31 + export const env = envSchema.parse(process.env); 32 + 33 + // Export type for use in other modules 34 + export type Env = z.infer<typeof envSchema>; 35 + 36 + // Export paths for convenience 37 + export { paths as appPaths };
+165
src/files/plan-files.ts
··· 1 + /** 2 + * Plan file handler 3 + * 4 + * Manages plan files in the session's plans/ directory. 5 + * Plan files are human-readable text with markdown-like sections. 6 + */ 7 + 8 + import { 9 + readFileSync, 10 + writeFileSync, 11 + unlinkSync, 12 + existsSync, 13 + mkdirSync, 14 + } from "node:fs"; 15 + import { join, dirname } from "node:path"; 16 + import type { Logger } from "pino"; 17 + import type { PlanContent, PlanFileHandlerDeps } from "../types.js"; 18 + 19 + /** 20 + * Handles reading, writing, and managing plan files 21 + */ 22 + export class PlanFileHandlerImpl { 23 + private readonly plansDir: string; 24 + private readonly log: Logger; 25 + 26 + constructor(sessionPath: string, deps: PlanFileHandlerDeps) { 27 + this.plansDir = join(sessionPath, "plans"); 28 + this.log = deps.logger.child({ component: "PlanFileHandler" }); 29 + 30 + // Ensure plans directory exists 31 + if (!existsSync(this.plansDir)) { 32 + mkdirSync(this.plansDir, { recursive: true }); 33 + } 34 + } 35 + 36 + /** 37 + * Get the full file path for a plan ID 38 + */ 39 + getFilePath(planId: string): string { 40 + return join(this.plansDir, `${planId}.txt`); 41 + } 42 + 43 + /** 44 + * Write a plan to disk 45 + */ 46 + write(planId: string, content: PlanContent): void { 47 + const filePath = this.getFilePath(planId); 48 + const fileContent = this.formatPlanContent(content); 49 + 50 + // Ensure parent directory exists 51 + const dir = dirname(filePath); 52 + if (!existsSync(dir)) { 53 + mkdirSync(dir, { recursive: true }); 54 + } 55 + 56 + writeFileSync(filePath, fileContent, "utf-8"); 57 + this.log.debug({ planId, filePath }, "Plan file written"); 58 + } 59 + 60 + /** 61 + * Read a plan from disk 62 + */ 63 + read(planId: string): PlanContent { 64 + const filePath = this.getFilePath(planId); 65 + const fileContent = readFileSync(filePath, "utf-8"); 66 + return this.parsePlanContent(fileContent); 67 + } 68 + 69 + /** 70 + * Delete a plan file 71 + */ 72 + delete(planId: string): void { 73 + const filePath = this.getFilePath(planId); 74 + if (existsSync(filePath)) { 75 + unlinkSync(filePath); 76 + this.log.debug({ planId, filePath }, "Plan file deleted"); 77 + } 78 + } 79 + 80 + /** 81 + * Check if a plan file exists 82 + */ 83 + exists(planId: string): boolean { 84 + return existsSync(this.getFilePath(planId)); 85 + } 86 + 87 + /** 88 + * Append a note to the plan's Notes section with timestamp 89 + */ 90 + appendToNotes(planId: string, note: string): void { 91 + const content = this.read(planId); 92 + const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19); 93 + const newNote = `[${timestamp}] ${note}`; 94 + 95 + content.notes = content.notes ? `${content.notes}\n${newNote}` : newNote; 96 + 97 + this.write(planId, content); 98 + this.log.debug({ planId, note: newNote }, "Note appended to plan"); 99 + } 100 + 101 + /** 102 + * Format plan content as file text 103 + */ 104 + private formatPlanContent(content: PlanContent): string { 105 + return `# Context 106 + ${content.context} 107 + 108 + # Goal 109 + ${content.goal} 110 + 111 + # Inputs 112 + ${content.inputs || "(none)"} 113 + 114 + # Outputs 115 + ${content.outputs || "(none)"} 116 + 117 + # Approach 118 + ${content.approach} 119 + 120 + # Success Criteria 121 + ${content.successCriteria} 122 + 123 + # Notes 124 + ${content.notes || "(none)"} 125 + `; 126 + } 127 + 128 + /** 129 + * Parse file text into plan content 130 + */ 131 + private parsePlanContent(text: string): PlanContent { 132 + const sections: Record<string, string> = {}; 133 + 134 + // Split by section headers 135 + const sectionPattern = 136 + /^# (Context|Goal|Inputs|Outputs|Approach|Success Criteria|Notes)\s*$/gm; 137 + const matches = [...text.matchAll(sectionPattern)]; 138 + 139 + for (let i = 0; i < matches.length; i++) { 140 + const match = matches[i]; 141 + if (!match) continue; 142 + 143 + const sectionName = match[1]; 144 + if (!sectionName) continue; 145 + 146 + const startIndex = (match.index || 0) + match[0].length; 147 + const endIndex = matches[i + 1]?.index ?? text.length; 148 + const content = text.slice(startIndex, endIndex).trim(); 149 + 150 + // Normalize section names 151 + const key = sectionName.toLowerCase().replace(" ", ""); 152 + sections[key] = content === "(none)" ? "" : content; 153 + } 154 + 155 + return { 156 + context: sections.context ?? "", 157 + goal: sections.goal ?? "", 158 + inputs: sections.inputs ?? "", 159 + outputs: sections.outputs ?? "", 160 + approach: sections.approach ?? "", 161 + successCriteria: sections.successcriteria ?? "", 162 + notes: sections.notes ?? "", 163 + }; 164 + } 165 + }
+23
src/generators/plan-id.ts
··· 1 + /** 2 + * Plan ID generator 3 + * 4 + * Generates short, alphanumeric IDs like "k7f3m" using nanoid 5 + * with a custom alphabet (lowercase letters and digits only). 6 + */ 7 + 8 + import { customAlphabet } from "nanoid"; 9 + 10 + // Lowercase alphanumeric alphabet for human-friendly IDs 11 + const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"; 12 + 13 + // Create ID generator with 5-character default length 14 + const nanoid = customAlphabet(alphabet, 5); 15 + 16 + /** 17 + * Generate a unique 5-character alphanumeric plan ID 18 + * 19 + * @returns A string like "k7f3m" 20 + */ 21 + export function generatePlanId(): string { 22 + return nanoid(); 23 + }
+27
src/generators/session-name.ts
··· 1 + /** 2 + * Session name generator 3 + * 4 + * Generates human-memorable three-word identifiers like "amber-quiet-river" 5 + * using unique-names-generator with adjective-color-animal pattern. 6 + */ 7 + 8 + import { 9 + uniqueNamesGenerator, 10 + adjectives, 11 + colors, 12 + animals, 13 + } from "unique-names-generator"; 14 + 15 + /** 16 + * Generate a unique three-word session name 17 + * 18 + * @returns A hyphenated lowercase string like "amber-quiet-river" 19 + */ 20 + export function generateSessionName(): string { 21 + return uniqueNamesGenerator({ 22 + dictionaries: [adjectives, colors, animals], 23 + separator: "-", 24 + length: 3, 25 + style: "lowerCase", 26 + }); 27 + }
+47
src/index.ts
··· 1 + #!/usr/bin/env node 2 + /** 3 + * 9plan MCP Server - Entry Point 4 + * 5 + * Session-scoped work queue for AI agent task sequencing. 6 + */ 7 + 8 + import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 9 + import { createServer } from "./server.js"; 10 + import { logger } from "./logger.js"; 11 + 12 + /** 13 + * Main entry point 14 + */ 15 + async function main(): Promise<void> { 16 + try { 17 + // Create server 18 + const server = createServer(); 19 + 20 + // Create stdio transport 21 + const transport = new StdioServerTransport(); 22 + 23 + // Connect server to transport 24 + await server.connect(transport); 25 + 26 + logger.info("9plan MCP server running on stdio"); 27 + 28 + // Handle graceful shutdown 29 + const shutdown = () => { 30 + logger.info("Shutting down..."); 31 + void server.close().then(() => process.exit(0)); 32 + }; 33 + 34 + process.on("SIGINT", shutdown); 35 + process.on("SIGTERM", shutdown); 36 + } catch (error) { 37 + logger.error({ error }, "Fatal error starting server"); 38 + process.exit(1); 39 + } 40 + } 41 + 42 + // Run 43 + main().catch((error: unknown) => { 44 + // Using console.error for fatal errors before logger is ready 45 + console.error("Fatal error:", error); 46 + process.exit(1); 47 + });
+32
src/logger.ts
··· 1 + /** 2 + * Pino logger configuration 3 + * 4 + * In development mode, uses pino-pretty for readable output. 5 + * In production, outputs structured JSON. 6 + */ 7 + 8 + import pino from "pino"; 9 + import { env } from "./env.js"; 10 + 11 + // Create logger with appropriate transport based on environment 12 + export const logger = 13 + process.env.NODE_ENV === "development" 14 + ? pino({ 15 + level: env.NINEPLAN_LOG_LEVEL, 16 + transport: { 17 + target: "pino-pretty", 18 + options: { 19 + colorize: true, 20 + translateTime: "SYS:standard", 21 + ignore: "pid,hostname", 22 + }, 23 + }, 24 + }) 25 + : pino({ 26 + level: env.NINEPLAN_LOG_LEVEL, 27 + // In production, just use base JSON logger 28 + base: { name: "9plan" }, 29 + }); 30 + 31 + // Re-export Logger type for convenience 32 + export type { Logger } from "pino";
+97
src/prompts/bootstrap.ts
··· 1 + /** 2 + * Bootstrap prompt 3 + * 4 + * Guides initial session setup and planning decomposition. 5 + */ 6 + 7 + import { z } from "zod"; 8 + 9 + // Prompt argument schema 10 + export const bootstrapArgumentSchema = { 11 + task: z 12 + .string() 13 + .optional() 14 + .describe("Optional initial task description to start with"), 15 + }; 16 + 17 + // Prompt content 18 + export const BOOTSTRAP_PROMPT_CONTENT = `You are helping the user set up a new 9plan session for a complex task. 19 + 20 + ## WORKFLOW 21 + 22 + 1. **CLARIFY**: Ask questions to understand scope, components, and dependencies 23 + - What is the overall goal? 24 + - What are the major components or phases? 25 + - What are the dependencies between them? 26 + - What does success look like? 27 + 28 + 2. **CREATE SESSION**: Use 9plan_session_create with a clear task description 29 + 30 + 3. **DECOMPOSE**: Break the task into discrete, self-contained plans 31 + - Each plan should be executable without additional context 32 + - Identify cross-component dependencies NOW (this is your only chance!) 33 + - Plans should have specific, measurable goals 34 + 35 + 4. **CAPTURE DEPENDENCIES**: For each plan, specify: 36 + - **Inputs**: What this plan needs from other plans (by description, not ID) 37 + - **Outputs**: What this plan produces that others may consume 38 + 39 + 5. **ENQUEUE**: Add plans with 9plan_queue_add 40 + - Respect dependency order (dependencies must complete first) 41 + - Use "back" position for normal work 42 + - IMPORTANT: Adding A, B, C at "front" gives [C, B, A] - add in REVERSE order if using front 43 + 44 + 6. **START WORK**: Pull first plan with 9plan_queue_pull 45 + 46 + ## PLAN QUALITY CHECKLIST 47 + 48 + Before adding a plan, verify: 49 + □ Goal is specific and measurable (not vague) 50 + □ Approach has concrete, actionable steps 51 + □ Success criteria are observable (you'll know when done) 52 + □ Inputs reference dependencies by description (e.g., "auth module from Authentication work") 53 + □ Outputs are specific enough to search for later 54 + 55 + ## DEPENDENCY RESOLUTION 56 + 57 + Plans reference dependencies by description, not by ID: 58 + - At execution time, use 9plan_history_search to find matching completed outputs 59 + - Example: If plan needs "auth module", search history for "auth module authentication" 60 + 61 + ## DECOMPOSITION PATTERN 62 + 63 + When a plan is too large: 64 + 1. Add subplans at front in REVERSE order (so they execute in correct order) 65 + 2. Defer parent to back with 9plan_plan_defer 66 + 3. Record child plan IDs in parent's Notes 67 + 4. When parent is re-pulled later, use 9plan_history_get to aggregate child outcomes 68 + 69 + ## EXAMPLE DECOMPOSITION 70 + 71 + Starting task: "Build Ghost-powered blog app" 72 + 73 + Plans to add (in order they should execute): 74 + 1. Ghost API Client - Create authenticated client 75 + 2. Cache System - Add response caching 76 + 3. Display Layer - Build UI components 77 + 78 + Since we want execution order [1, 2, 3], add plans at back: 79 + - 9plan_queue_add(Ghost API Client, position: back) → [1] 80 + - 9plan_queue_add(Cache System, position: back) → [1, 2] 81 + - 9plan_queue_add(Display Layer, position: back) → [1, 2, 3] 82 + 83 + Now ready to pull!`; 84 + 85 + // Prompt configuration 86 + export const bootstrapPromptConfig = { 87 + name: "bootstrap", 88 + description: 89 + "Set up a new 9plan session for a complex task. Guides you through clarifying requirements, decomposing into plans, and capturing dependencies.", 90 + arguments: [ 91 + { 92 + name: "task", 93 + description: "Optional initial task description to start with", 94 + required: false, 95 + }, 96 + ], 97 + };
+290
src/server.ts
··· 1 + /** 2 + * MCP Server factory 3 + * 4 + * Creates and configures the 9plan MCP server with all tools and prompts. 5 + * Manages session state across tool calls. 6 + */ 7 + 8 + import { z } from "zod"; 9 + import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 10 + import type { SessionStore } from "./db/session-store.js"; 11 + import { 12 + createNewSession, 13 + resumeSession, 14 + getSessionPath, 15 + } from "./container.js"; 16 + import { logger } from "./logger.js"; 17 + import { NinePlanError, formatResponse } from "./types.js"; 18 + 19 + // Import all tools 20 + import { 21 + sessionCreateToolConfig, 22 + handleSessionResume, 23 + sessionResumeToolConfig, 24 + handleQueueAdd, 25 + queueAddToolConfig, 26 + handleQueuePull, 27 + queuePullToolConfig, 28 + handlePlanDefer, 29 + planDeferToolConfig, 30 + handlePlanComplete, 31 + planCompleteToolConfig, 32 + handlePlanDiscard, 33 + planDiscardToolConfig, 34 + handleHistorySearch, 35 + historySearchToolConfig, 36 + handleHistoryGet, 37 + historyGetToolConfig, 38 + } from "./tools/index.js"; 39 + 40 + // Import prompts 41 + import { BOOTSTRAP_PROMPT_CONTENT } from "./prompts/bootstrap.js"; 42 + 43 + /* eslint-disable @typescript-eslint/no-unnecessary-condition */ 44 + 45 + /** 46 + * Session state manager 47 + * Tracks the currently active session across tool calls 48 + */ 49 + class SessionManager { 50 + private currentStore: SessionStore | null = null; 51 + private currentSessionName: string | null = null; 52 + 53 + /** 54 + * Set the current session 55 + */ 56 + setSession(sessionName: string, store: SessionStore): void { 57 + // Close previous session if exists 58 + this.close(); 59 + this.currentSessionName = sessionName; 60 + this.currentStore = store; 61 + logger.info({ sessionName }, "Session activated"); 62 + } 63 + 64 + /** 65 + * Get the current session store, throwing if none active 66 + */ 67 + getStore(): SessionStore { 68 + if (!this.currentStore || !this.currentSessionName) { 69 + throw new NinePlanError( 70 + "SESSION_NOT_FOUND", 71 + "No session is currently active", 72 + "Use 9plan_session_create to create a new session, or 9plan_session_resume to load an existing one.", 73 + ); 74 + } 75 + return this.currentStore; 76 + } 77 + 78 + /** 79 + * Get the current session name 80 + */ 81 + getSessionName(): string { 82 + if (!this.currentSessionName) { 83 + throw new NinePlanError( 84 + "SESSION_NOT_FOUND", 85 + "No session is currently active", 86 + "Use 9plan_session_create to create a new session, or 9plan_session_resume to load an existing one.", 87 + ); 88 + } 89 + return this.currentSessionName; 90 + } 91 + 92 + /** 93 + * Check if a session is active 94 + */ 95 + hasSession(): boolean { 96 + return this.currentStore !== null && this.currentSessionName !== null; 97 + } 98 + 99 + /** 100 + * Close the current session 101 + */ 102 + close(): void { 103 + if (this.currentStore) { 104 + this.currentStore.close(); 105 + logger.info({ sessionName: this.currentSessionName }, "Session closed"); 106 + } 107 + this.currentStore = null; 108 + this.currentSessionName = null; 109 + } 110 + } 111 + 112 + /** 113 + * Create the MCP server with all tools registered 114 + */ 115 + export function createServer(): McpServer { 116 + const server = new McpServer({ 117 + name: "9plan", 118 + version: "1.0.0", 119 + }); 120 + 121 + const sessionManager = new SessionManager(); 122 + 123 + // Register session create tool - handles session creation internally 124 + server.registerTool( 125 + "9plan_session_create", 126 + sessionCreateToolConfig, 127 + (args) => { 128 + const taskDescription = args.task_description; 129 + const { sessionName, store } = createNewSession(taskDescription); 130 + const sessionPath = getSessionPath(sessionName); 131 + 132 + sessionManager.setSession(sessionName, store); 133 + 134 + const response = formatResponse( 135 + sessionName, 136 + `Session created: ${sessionName} 137 + Directory: ${sessionPath} 138 + 139 + The session is ready. Use 9plan_queue_add to add plans, then 9plan_queue_pull to begin work.`, 140 + ); 141 + 142 + return { 143 + content: [{ type: "text" as const, text: response }], 144 + }; 145 + }, 146 + ); 147 + 148 + // Register session resume tool 149 + server.registerTool( 150 + "9plan_session_resume", 151 + sessionResumeToolConfig, 152 + (args) => { 153 + const sessionName = args.session_name; 154 + const store = resumeSession(sessionName); 155 + sessionManager.setSession(sessionName, store); 156 + return handleSessionResume({ session_name: sessionName }); 157 + }, 158 + ); 159 + 160 + // Register queue tools 161 + server.registerTool("9plan_queue_add", queueAddToolConfig, (args) => { 162 + const store = sessionManager.getStore(); 163 + const sessionName = sessionManager.getSessionName(); 164 + const queueAddArgs = { 165 + context: args.context, 166 + goal: args.goal, 167 + approach: args.approach, 168 + success_criteria: args.success_criteria, 169 + ...(args.inputs !== undefined && { inputs: args.inputs }), 170 + ...(args.outputs !== undefined && { outputs: args.outputs }), 171 + ...(args.position !== undefined && { 172 + position: args.position, 173 + }), 174 + }; 175 + return handleQueueAdd(store, sessionName, queueAddArgs); 176 + }); 177 + 178 + server.registerTool( 179 + "9plan_queue_pull", 180 + queuePullToolConfig, 181 + (_args) => { 182 + const store = sessionManager.getStore(); 183 + const sessionName = sessionManager.getSessionName(); 184 + return handleQueuePull(store, sessionName, {}); 185 + }, 186 + ); 187 + 188 + // Register plan lifecycle tools 189 + server.registerTool("9plan_plan_defer", planDeferToolConfig, (args) => { 190 + const store = sessionManager.getStore(); 191 + const sessionName = sessionManager.getSessionName(); 192 + const deferArgs = { 193 + reason: args.reason, 194 + ...(args.position !== undefined && { 195 + position: args.position, 196 + }), 197 + }; 198 + return handlePlanDefer(store, sessionName, deferArgs); 199 + }); 200 + 201 + server.registerTool( 202 + "9plan_plan_complete", 203 + planCompleteToolConfig, 204 + (args) => { 205 + const store = sessionManager.getStore(); 206 + const sessionName = sessionManager.getSessionName(); 207 + return handlePlanComplete(store, sessionName, { 208 + outcome: args.outcome, 209 + }); 210 + }, 211 + ); 212 + 213 + server.registerTool( 214 + "9plan_plan_discard", 215 + planDiscardToolConfig, 216 + (args) => { 217 + const store = sessionManager.getStore(); 218 + const sessionName = sessionManager.getSessionName(); 219 + return handlePlanDiscard(store, sessionName, { 220 + reason: args.reason, 221 + }); 222 + }, 223 + ); 224 + 225 + // Register history tools 226 + server.registerTool( 227 + "9plan_history_search", 228 + historySearchToolConfig, 229 + (args) => { 230 + const store = sessionManager.getStore(); 231 + const sessionName = sessionManager.getSessionName(); 232 + const searchArgs = { 233 + query: args.query, 234 + ...(args.max_results !== undefined && { 235 + max_results: args.max_results, 236 + }), 237 + }; 238 + return handleHistorySearch(store, sessionName, searchArgs); 239 + }, 240 + ); 241 + 242 + server.registerTool( 243 + "9plan_history_get", 244 + historyGetToolConfig, 245 + (args) => { 246 + const store = sessionManager.getStore(); 247 + const sessionName = sessionManager.getSessionName(); 248 + return handleHistoryGet(store, sessionName, { 249 + plan_id: args.plan_id, 250 + }); 251 + }, 252 + ); 253 + 254 + // Register bootstrap prompt 255 + server.registerPrompt( 256 + "bootstrap", 257 + { 258 + description: 259 + "Set up a new 9plan session for a complex task. Guides you through clarifying requirements, decomposing into plans, and capturing dependencies.", 260 + argsSchema: { 261 + task: z 262 + .string() 263 + .optional() 264 + .describe("Optional initial task description to start with"), 265 + }, 266 + }, 267 + (args) => { 268 + const taskArg = args.task; 269 + const taskText = taskArg 270 + ? `\n\nThe user has provided an initial task: "${taskArg}"\n\nStart by clarifying the task and then proceed with the workflow.` 271 + : ""; 272 + 273 + return { 274 + messages: [ 275 + { 276 + role: "user" as const, 277 + content: { 278 + type: "text" as const, 279 + text: BOOTSTRAP_PROMPT_CONTENT + taskText, 280 + }, 281 + }, 282 + ], 283 + }; 284 + }, 285 + ); 286 + 287 + logger.info("9plan MCP server initialized with 9 tools and 1 prompt"); 288 + 289 + return server; 290 + }
+70
src/tools/history-get.ts
··· 1 + /** 2 + * 9plan_history_get tool 3 + * 4 + * Retrieves a specific completed plan by ID. 5 + */ 6 + 7 + import { z } from "zod"; 8 + import type { SessionStoreInterface } from "../types.js"; 9 + import { formatResponse, NinePlanError } from "../types.js"; 10 + 11 + // Input schema 12 + export const historyGetInputSchema = { 13 + plan_id: z 14 + .string() 15 + .min(1) 16 + .describe("The ID of the completed plan to retrieve (e.g., k7f3m)"), 17 + }; 18 + 19 + // Input type 20 + export interface HistoryGetInput { 21 + plan_id: string; 22 + } 23 + 24 + /** 25 + * Handle history get tool call 26 + */ 27 + export function handleHistoryGet( 28 + store: SessionStoreInterface, 29 + sessionName: string, 30 + args: HistoryGetInput, 31 + ): { content: { type: "text"; text: string }[] } { 32 + const plan = store.getHistoryPlan(args.plan_id); 33 + 34 + if (!plan) { 35 + const error = new NinePlanError( 36 + "PLAN_NOT_FOUND", 37 + `Plan not found: ${args.plan_id}`, 38 + "The plan may not have been completed yet (check queue), or may have been discarded.\nUse 9plan_session_resume to see current queue, or 9plan_history_search to find related plans.", 39 + ); 40 + return { 41 + content: [{ type: "text", text: error.format(sessionName) }], 42 + }; 43 + } 44 + 45 + // Format the plan content 46 + let content = `Plan ${plan.id} (completed)\n\n`; 47 + 48 + content += `# Context\n${plan.context ?? "(none)"}\n\n`; 49 + content += `# Goal\n${plan.goal}\n\n`; 50 + content += `# Inputs\n${plan.inputs ?? "(none)"}\n\n`; 51 + content += `# Outputs\n${plan.outputs ?? "(none)"}\n\n`; 52 + content += `# Approach\n${plan.approach ?? "(none)"}\n\n`; 53 + content += `# Success Criteria\n${plan.successCriteria ?? "(none)"}\n\n`; 54 + content += `# Notes\n${plan.notes ?? "(none)"}\n\n`; 55 + content += `# Outcome\n${plan.outcome ?? "(none)"}`; 56 + 57 + const response = formatResponse(sessionName, content); 58 + 59 + return { 60 + content: [{ type: "text", text: response }], 61 + }; 62 + } 63 + 64 + // Tool configuration for registration 65 + export const historyGetToolConfig = { 66 + title: "Get History Plan", 67 + description: 68 + "Retrieves a specific completed plan by ID. Returns the full plan content including outcome. Used for dependency resolution and aggregating child outcomes after decomposition.", 69 + inputSchema: historyGetInputSchema, 70 + };
+93
src/tools/history-search.ts
··· 1 + /** 2 + * 9plan_history_search tool 3 + * 4 + * Searches completed plans for outputs matching a query. 5 + * Uses FTS5 with BM25 ranking. 6 + */ 7 + 8 + import { z } from "zod"; 9 + import type { SessionStoreInterface } from "../types.js"; 10 + import { formatResponse } from "../types.js"; 11 + 12 + // Input schema 13 + export const historySearchInputSchema = { 14 + query: z 15 + .string() 16 + .min(1, "Query is required") 17 + .describe( 18 + "Search terms to match against completed plan goals, outputs, and outcomes", 19 + ), 20 + max_results: z 21 + .number() 22 + .int() 23 + .min(1) 24 + .max(50) 25 + .optional() 26 + .default(10) 27 + .describe("Maximum number of results to return (default: 10, max: 50)"), 28 + }; 29 + 30 + // Input type 31 + export interface HistorySearchInput { 32 + query: string; 33 + max_results?: number; 34 + } 35 + 36 + /** 37 + * Handle history search tool call 38 + */ 39 + export function handleHistorySearch( 40 + store: SessionStoreInterface, 41 + sessionName: string, 42 + args: HistorySearchInput, 43 + ): { content: { type: "text"; text: string }[] } { 44 + const matches = store.searchHistory(args.query, args.max_results ?? 10); 45 + 46 + if (matches.length === 0) { 47 + const response = formatResponse( 48 + sessionName, 49 + `No matching plans found for: "${args.query}" 50 + 51 + Try different search terms, or check if the relevant work has been completed yet.`, 52 + ); 53 + return { 54 + content: [{ type: "text", text: response }], 55 + }; 56 + } 57 + 58 + // Format results 59 + let resultText = `Found ${String(matches.length)} matching plan${matches.length === 1 ? "" : "s"}:\n\n`; 60 + 61 + for (let i = 0; i < matches.length; i++) { 62 + const match = matches[i]; 63 + if (!match) continue; 64 + 65 + resultText += `${String(i + 1)}. Plan ${match.id}\n`; 66 + resultText += ` Goal: ${match.goal}\n`; 67 + if (match.outcome) { 68 + // Truncate long outcomes for the list view 69 + const outcomePreview = 70 + match.outcome.length > 200 71 + ? match.outcome.slice(0, 200) + "..." 72 + : match.outcome; 73 + resultText += ` Outcome: ${outcomePreview}\n`; 74 + } 75 + resultText += "\n"; 76 + } 77 + 78 + resultText += "Use 9plan_history_get with the plan ID for full details."; 79 + 80 + const response = formatResponse(sessionName, resultText); 81 + 82 + return { 83 + content: [{ type: "text", text: response }], 84 + }; 85 + } 86 + 87 + // Tool configuration for registration 88 + export const historySearchToolConfig = { 89 + title: "Search History", 90 + description: 91 + "Searches completed plans for outputs matching a query. Uses full-text search across goals, outputs, and outcomes. Primary mechanism for resolving plan dependencies.", 92 + inputSchema: historySearchInputSchema, 93 + };
+67
src/tools/index.ts
··· 1 + /** 2 + * Tool registration aggregator 3 + * 4 + * Exports all tool handlers and configurations for server registration. 5 + */ 6 + 7 + // Session tools 8 + export { sessionCreateToolConfig } from "./session-create.js"; 9 + 10 + export { 11 + handleSessionResume, 12 + sessionResumeToolConfig, 13 + sessionResumeInputSchema, 14 + type SessionResumeInput, 15 + } from "./session-resume.js"; 16 + 17 + // Queue tools 18 + export { 19 + handleQueueAdd, 20 + queueAddToolConfig, 21 + queueAddInputSchema, 22 + type QueueAddInput, 23 + } from "./queue-add.js"; 24 + 25 + export { 26 + handleQueuePull, 27 + queuePullToolConfig, 28 + queuePullInputSchema, 29 + type QueuePullInput, 30 + } from "./queue-pull.js"; 31 + 32 + // Plan lifecycle tools 33 + export { 34 + handlePlanDefer, 35 + planDeferToolConfig, 36 + planDeferInputSchema, 37 + type PlanDeferInput, 38 + } from "./plan-defer.js"; 39 + 40 + export { 41 + handlePlanComplete, 42 + planCompleteToolConfig, 43 + planCompleteInputSchema, 44 + type PlanCompleteInput, 45 + } from "./plan-complete.js"; 46 + 47 + export { 48 + handlePlanDiscard, 49 + planDiscardToolConfig, 50 + planDiscardInputSchema, 51 + type PlanDiscardInput, 52 + } from "./plan-discard.js"; 53 + 54 + // History tools 55 + export { 56 + handleHistorySearch, 57 + historySearchToolConfig, 58 + historySearchInputSchema, 59 + type HistorySearchInput, 60 + } from "./history-search.js"; 61 + 62 + export { 63 + handleHistoryGet, 64 + historyGetToolConfig, 65 + historyGetInputSchema, 66 + type HistoryGetInput, 67 + } from "./history-get.js";
+89
src/tools/plan-complete.ts
··· 1 + /** 2 + * 9plan_plan_complete tool 3 + * 4 + * Marks the active plan as done, storing the outcome in history. 5 + * The plan becomes searchable via 9plan_history_search. 6 + */ 7 + 8 + import { z } from "zod"; 9 + import type { SessionStoreInterface } from "../types.js"; 10 + import { formatResponse, NinePlanError } from "../types.js"; 11 + 12 + // Input schema 13 + export const planCompleteInputSchema = { 14 + outcome: z 15 + .string() 16 + .min(1, "Outcome is required") 17 + .describe( 18 + "Summary of what was accomplished, including outputs produced. This will be indexed for history search.", 19 + ), 20 + }; 21 + 22 + // Input type 23 + export interface PlanCompleteInput { 24 + outcome: string; 25 + } 26 + 27 + /** 28 + * Handle plan complete tool call 29 + */ 30 + export function handlePlanComplete( 31 + store: SessionStoreInterface, 32 + sessionName: string, 33 + args: PlanCompleteInput, 34 + ): { content: { type: "text"; text: string }[] } { 35 + try { 36 + const activePlan = store.getActivePlan(); 37 + if (!activePlan) { 38 + throw new NinePlanError( 39 + "NO_ACTIVE_PLAN", 40 + "No plan is currently active", 41 + "Use 9plan_queue_pull to get a plan first.", 42 + ); 43 + } 44 + 45 + const planId = activePlan.id; 46 + store.completePlan(args.outcome); 47 + 48 + // Get updated queue info 49 + const queue = store.getQueue(); 50 + const queueStatus = 51 + queue.length > 0 52 + ? `${String(queue.length)} plans remaining` 53 + : "Queue is now empty!"; 54 + 55 + const nextAction = 56 + queue.length > 0 57 + ? "Next: Use 9plan_queue_pull to continue work." 58 + : "Task complete! Use 9plan_history_search to review what was accomplished."; 59 + 60 + const response = formatResponse( 61 + sessionName, 62 + `Plan ${planId} completed and indexed. 63 + 64 + The plan's content and outcome are now searchable via 9plan_history_search. 65 + 66 + Queue status: ${queueStatus} 67 + ${nextAction}`, 68 + ); 69 + 70 + return { 71 + content: [{ type: "text", text: response }], 72 + }; 73 + } catch (error) { 74 + if (error instanceof NinePlanError) { 75 + return { 76 + content: [{ type: "text", text: error.format(sessionName) }], 77 + }; 78 + } 79 + throw error; 80 + } 81 + } 82 + 83 + // Tool configuration for registration 84 + export const planCompleteToolConfig = { 85 + title: "Complete Plan", 86 + description: 87 + "Marks the active plan as done. The outcome is stored and indexed for future searches via 9plan_history_search. The plan file is deleted (content lives in database).", 88 + inputSchema: planCompleteInputSchema, 89 + };
+94
src/tools/plan-defer.ts
··· 1 + /** 2 + * 9plan_plan_defer tool 3 + * 4 + * Returns the active plan to the queue without completing it. 5 + * Appends the reason to the plan's Notes section. 6 + */ 7 + 8 + import { z } from "zod"; 9 + import type { SessionStoreInterface } from "../types.js"; 10 + import { formatResponse, NinePlanError } from "../types.js"; 11 + 12 + // Input schema 13 + export const planDeferInputSchema = { 14 + reason: z 15 + .string() 16 + .min(1, "Reason is required") 17 + .describe("Why the plan is being deferred (will be recorded in Notes)"), 18 + position: z 19 + .enum(["front", "back"]) 20 + .optional() 21 + .default("back") 22 + .describe( 23 + 'Where to place in queue: "front" to retry soon, "back" for decomposition/later', 24 + ), 25 + }; 26 + 27 + // Input type 28 + export interface PlanDeferInput { 29 + reason: string; 30 + position?: "front" | "back"; 31 + } 32 + 33 + /** 34 + * Handle plan defer tool call 35 + */ 36 + export function handlePlanDefer( 37 + store: SessionStoreInterface, 38 + sessionName: string, 39 + args: PlanDeferInput, 40 + ): { content: { type: "text"; text: string }[] } { 41 + try { 42 + const activePlan = store.getActivePlan(); 43 + if (!activePlan) { 44 + throw new NinePlanError( 45 + "NO_ACTIVE_PLAN", 46 + "No plan is currently active", 47 + "Use 9plan_queue_pull to get a plan first.", 48 + ); 49 + } 50 + 51 + const planId = activePlan.id; 52 + store.deferPlan(args.reason, args.position ?? "back"); 53 + 54 + // Get updated queue info 55 + const queue = store.getQueue(); 56 + const deferredPlan = queue.find((p) => p.id === planId); 57 + const queuePosition = deferredPlan?.queuePosition ?? queue.length; 58 + 59 + const positionText = 60 + args.position === "front" 61 + ? `${String(queuePosition)} (will be pulled next)` 62 + : `${String(queuePosition)} (will execute after current queue)`; 63 + 64 + const response = formatResponse( 65 + sessionName, 66 + `Plan ${planId} deferred to ${args.position ?? "back"} of queue. 67 + Reason recorded in Notes. 68 + 69 + Queue status: ${String(queue.length)} plans 70 + Position: ${positionText} 71 + 72 + ${args.position === "front" ? "Resolve the blocking issue, then use 9plan_queue_pull to retry." : "Use 9plan_queue_pull to continue with queued work."}`, 73 + ); 74 + 75 + return { 76 + content: [{ type: "text", text: response }], 77 + }; 78 + } catch (error) { 79 + if (error instanceof NinePlanError) { 80 + return { 81 + content: [{ type: "text", text: error.format(sessionName) }], 82 + }; 83 + } 84 + throw error; 85 + } 86 + } 87 + 88 + // Tool configuration for registration 89 + export const planDeferToolConfig = { 90 + title: "Defer Plan", 91 + description: 92 + 'Returns the active plan to the queue without completing it. Use "front" position for blocked work (retry soon), "back" for decomposition (aggregate later). The reason is appended to Notes.', 93 + inputSchema: planDeferInputSchema, 94 + };
+88
src/tools/plan-discard.ts
··· 1 + /** 2 + * 9plan_plan_discard tool 3 + * 4 + * Abandons the active plan without completing it. 5 + * The plan is NOT recorded in history and cannot be searched. 6 + */ 7 + 8 + import { z } from "zod"; 9 + import type { SessionStoreInterface } from "../types.js"; 10 + import { formatResponse, NinePlanError } from "../types.js"; 11 + 12 + // Input schema 13 + export const planDiscardInputSchema = { 14 + reason: z 15 + .string() 16 + .min(1, "Reason is required") 17 + .describe("Why the plan is being discarded (for logging only)"), 18 + }; 19 + 20 + // Input type 21 + export interface PlanDiscardInput { 22 + reason: string; 23 + } 24 + 25 + /** 26 + * Handle plan discard tool call 27 + */ 28 + export function handlePlanDiscard( 29 + store: SessionStoreInterface, 30 + sessionName: string, 31 + args: PlanDiscardInput, 32 + ): { content: { type: "text"; text: string }[] } { 33 + try { 34 + const activePlan = store.getActivePlan(); 35 + if (!activePlan) { 36 + throw new NinePlanError( 37 + "NO_ACTIVE_PLAN", 38 + "No plan is currently active", 39 + "Use 9plan_queue_pull to get a plan first.", 40 + ); 41 + } 42 + 43 + const planId = activePlan.id; 44 + store.discardPlan(args.reason); 45 + 46 + // Get updated queue info 47 + const queue = store.getQueue(); 48 + const queueStatus = 49 + queue.length > 0 50 + ? `${String(queue.length)} plans remaining` 51 + : "Queue is now empty!"; 52 + 53 + const nextAction = 54 + queue.length > 0 55 + ? "Next: Use 9plan_queue_pull to continue work." 56 + : "Task complete! Use 9plan_history_search to review what was accomplished."; 57 + 58 + const response = formatResponse( 59 + sessionName, 60 + `Plan ${planId} discarded. 61 + Reason: ${args.reason} 62 + 63 + The plan has been removed and will not appear in history. 64 + 65 + Queue status: ${queueStatus} 66 + ${nextAction}`, 67 + ); 68 + 69 + return { 70 + content: [{ type: "text", text: response }], 71 + }; 72 + } catch (error) { 73 + if (error instanceof NinePlanError) { 74 + return { 75 + content: [{ type: "text", text: error.format(sessionName) }], 76 + }; 77 + } 78 + throw error; 79 + } 80 + } 81 + 82 + // Tool configuration for registration 83 + export const planDiscardToolConfig = { 84 + title: "Discard Plan", 85 + description: 86 + "Abandons the active plan without completing. Use when a plan becomes obsolete or was misconceived. The plan will NOT appear in history search.", 87 + inputSchema: planDiscardInputSchema, 88 + };
+112
src/tools/queue-add.ts
··· 1 + /** 2 + * 9plan_queue_add tool 3 + * 4 + * Adds a new plan to the queue with front/back positioning. 5 + */ 6 + 7 + import { z } from "zod"; 8 + import type { SessionStoreInterface } from "../types.js"; 9 + import { formatResponse, NinePlanError } from "../types.js"; 10 + 11 + // Input schema 12 + export const queueAddInputSchema = { 13 + context: z 14 + .string() 15 + .min(1, "Context is required") 16 + .describe("Where this plan fits in the overall task"), 17 + goal: z 18 + .string() 19 + .min(1, "Goal is required") 20 + .describe("What this plan accomplishes"), 21 + approach: z 22 + .string() 23 + .min(1, "Approach is required") 24 + .describe("How to accomplish the goal (detailed, actionable steps)"), 25 + success_criteria: z 26 + .string() 27 + .min(1, "Success criteria is required") 28 + .describe("Observable conditions that indicate completion"), 29 + inputs: z 30 + .string() 31 + .optional() 32 + .describe( 33 + 'Dependencies from other plans (format: "- description: source")', 34 + ), 35 + outputs: z 36 + .string() 37 + .optional() 38 + .describe( 39 + 'What this plan produces that others may consume (format: "- description: details")', 40 + ), 41 + position: z 42 + .enum(["front", "back"]) 43 + .optional() 44 + .default("back") 45 + .describe( 46 + 'Queue position: "front" for blocking work, "back" for eventual work', 47 + ), 48 + }; 49 + 50 + // Input type 51 + export interface QueueAddInput { 52 + context: string; 53 + goal: string; 54 + approach: string; 55 + success_criteria: string; 56 + inputs?: string; 57 + outputs?: string; 58 + position?: "front" | "back"; 59 + } 60 + 61 + /** 62 + * Handle queue add tool call 63 + */ 64 + export function handleQueueAdd( 65 + store: SessionStoreInterface, 66 + sessionName: string, 67 + args: QueueAddInput, 68 + ): { content: { type: "text"; text: string }[] } { 69 + try { 70 + const planInput = { 71 + context: args.context, 72 + goal: args.goal, 73 + approach: args.approach, 74 + successCriteria: args.success_criteria, 75 + ...(args.inputs !== undefined && { inputs: args.inputs }), 76 + ...(args.outputs !== undefined && { outputs: args.outputs }), 77 + }; 78 + 79 + const plan = store.addPlan(planInput, args.position ?? "back"); 80 + 81 + const planPath = store.getSessionPath() + `/plans/${plan.id}.txt`; 82 + const positionNote = args.position === "front" ? " (front)" : ""; 83 + 84 + const response = formatResponse( 85 + sessionName, 86 + `Plan added: ${plan.id} 87 + Path: ${planPath} 88 + Queue position: ${String(plan.queuePosition)}${positionNote} 89 + 90 + The plan file has been created. Use 9plan_queue_pull when ready to execute.`, 91 + ); 92 + 93 + return { 94 + content: [{ type: "text", text: response }], 95 + }; 96 + } catch (error) { 97 + if (error instanceof NinePlanError) { 98 + return { 99 + content: [{ type: "text", text: error.format(sessionName) }], 100 + }; 101 + } 102 + throw error; 103 + } 104 + } 105 + 106 + // Tool configuration for registration 107 + export const queueAddToolConfig = { 108 + title: "Add Plan to Queue", 109 + description: 110 + "Adds a new plan to the queue. Required fields: context, goal, approach, success_criteria. Optional: inputs, outputs, position (front/back, default: back). Front = blocking work, back = eventual work.", 111 + inputSchema: queueAddInputSchema, 112 + };
+94
src/tools/queue-pull.ts
··· 1 + /** 2 + * 9plan_queue_pull tool 3 + * 4 + * Removes the front plan from queue and marks it active. 5 + */ 6 + 7 + import type { SessionStoreInterface } from "../types.js"; 8 + import { formatResponse, NinePlanError } from "../types.js"; 9 + 10 + // Input schema (no required inputs) 11 + export const queuePullInputSchema = {}; 12 + 13 + // Input type 14 + export type QueuePullInput = Record<string, never>; 15 + 16 + /** 17 + * Handle queue pull tool call 18 + */ 19 + export function handleQueuePull( 20 + store: SessionStoreInterface, 21 + sessionName: string, 22 + _args: QueuePullInput, 23 + ): { content: { type: "text"; text: string }[] } { 24 + try { 25 + const plan = store.pullPlan(); 26 + const planPath = store.getSessionPath() + `/plans/${plan.id}.txt`; 27 + 28 + const response = formatResponse( 29 + sessionName, 30 + `Active plan: ${plan.id} 31 + Path: ${planPath} 32 + 33 + Read the plan file for full context. Review for any ambiguities before starting execution. 34 + 35 + If the plan has inputs from other plans, use 9plan_history_search to find their outputs. 36 + If the plan's Notes indicate it was previously decomposed, use 9plan_history_get to retrieve child outcomes.`, 37 + ); 38 + 39 + return { 40 + content: [{ type: "text", text: response }], 41 + }; 42 + } catch (error) { 43 + if (error instanceof NinePlanError) { 44 + // Special handling for queue empty (not really an error) 45 + if (error.category === "QUEUE_EMPTY") { 46 + const completedCount = store.getCompletedCount(); 47 + const response = formatResponse( 48 + sessionName, 49 + `Queue is empty. Task complete! 50 + 51 + Completed plans: ${String(completedCount)} 52 + 53 + Use 9plan_history_search to review what was accomplished.`, 54 + ); 55 + return { 56 + content: [{ type: "text", text: response }], 57 + }; 58 + } 59 + 60 + // Special handling for plan already active 61 + if (error.category === "PLAN_ALREADY_ACTIVE") { 62 + const active = store.getActivePlan(); 63 + if (active) { 64 + const activePath = store.getSessionPath() + `/plans/${active.id}.txt`; 65 + const response = formatResponse( 66 + sessionName, 67 + `Error: Cannot pull - a plan is already active 68 + 69 + Active plan: ${active.id} 70 + Path: ${activePath} 71 + 72 + Complete, defer, or discard the active plan before pulling another.`, 73 + ); 74 + return { 75 + content: [{ type: "text", text: response }], 76 + }; 77 + } 78 + } 79 + 80 + return { 81 + content: [{ type: "text", text: error.format(sessionName) }], 82 + }; 83 + } 84 + throw error; 85 + } 86 + } 87 + 88 + // Tool configuration for registration 89 + export const queuePullToolConfig = { 90 + title: "Pull Plan from Queue", 91 + description: 92 + "Removes the front plan from queue and marks it as active. Returns the plan ID and file path. Only one plan can be active at a time.", 93 + inputSchema: queuePullInputSchema, 94 + };
+55
src/tools/session-create.ts
··· 1 + /** 2 + * 9plan_session_create tool 3 + * 4 + * Creates a new session with a randomly-generated three-word identifier. 5 + */ 6 + 7 + import { z } from "zod"; 8 + import { createNewSession, getSessionPath } from "../container.js"; 9 + import { formatResponse } from "../types.js"; 10 + 11 + // Input schema 12 + export const sessionCreateInputSchema = { 13 + task_description: z 14 + .string() 15 + .optional() 16 + .describe("Optional description of the overall task this session is for"), 17 + }; 18 + 19 + // Input type derived from schema 20 + export interface SessionCreateInput { 21 + task_description?: string; 22 + } 23 + 24 + /** 25 + * Handle session create tool call 26 + */ 27 + export function handleSessionCreate( 28 + args: SessionCreateInput, 29 + ): { content: { type: "text"; text: string }[] } { 30 + const { sessionName, store } = createNewSession(args.task_description); 31 + const sessionPath = getSessionPath(sessionName); 32 + 33 + // Close the store since we're just creating the session 34 + store.close(); 35 + 36 + const response = formatResponse( 37 + sessionName, 38 + `Session created: ${sessionName} 39 + Directory: ${sessionPath} 40 + 41 + The session is ready. Use 9plan_queue_add to add plans, then 9plan_queue_pull to begin work.`, 42 + ); 43 + 44 + return { 45 + content: [{ type: "text", text: response }], 46 + }; 47 + } 48 + 49 + // Tool configuration for registration 50 + export const sessionCreateToolConfig = { 51 + title: "Create Session", 52 + description: 53 + "Creates a new session with a randomly-generated three-word identifier (e.g., amber-quiet-river). Optionally provide a task description to document the overall goal.", 54 + inputSchema: sessionCreateInputSchema, 55 + };
+109
src/tools/session-resume.ts
··· 1 + /** 2 + * 9plan_session_resume tool 3 + * 4 + * Loads an existing session by name and returns its current state. 5 + */ 6 + 7 + import { z } from "zod"; 8 + import { resumeSession } from "../container.js"; 9 + import { formatResponse, NinePlanError } from "../types.js"; 10 + 11 + // Input schema 12 + export const sessionResumeInputSchema = { 13 + session_name: z 14 + .string() 15 + .regex( 16 + /^[a-z]+-[a-z]+-[a-z]+$/, 17 + "Session name must be in format: word-word-word (e.g., amber-quiet-river)", 18 + ) 19 + .describe("The three-word session name to resume"), 20 + }; 21 + 22 + // Input type 23 + export interface SessionResumeInput { 24 + session_name: string; 25 + } 26 + 27 + /** 28 + * Handle session resume tool call 29 + */ 30 + export function handleSessionResume( 31 + args: SessionResumeInput, 32 + ): { content: { type: "text"; text: string }[] } { 33 + try { 34 + const store = resumeSession(args.session_name); 35 + const state = store.getState(); 36 + 37 + // Build response 38 + let response = `Session resumed: ${state.sessionName} 39 + Directory: ${state.sessionPath} 40 + 41 + Task: ${state.taskDescription ?? "(no description)"} 42 + 43 + `; 44 + 45 + // Active plan info 46 + if (state.activePlan) { 47 + response += `Active plan: ${state.activePlan.id} 48 + Path: ${state.activePlan.filePath} 49 + Goal: ${state.activePlan.goal} 50 + 51 + `; 52 + } else { 53 + response += `Active plan: (none) 54 + 55 + `; 56 + } 57 + 58 + // Queue info 59 + if (state.queue.length > 0) { 60 + response += `Queue (${String(state.queue.length)} plans):\n`; 61 + for (const plan of state.queue) { 62 + response += ` ${String(plan.queuePosition)}. ${plan.id} - ${plan.goal}\n`; 63 + } 64 + response += "\n"; 65 + } else { 66 + response += `Queue: (empty)\n\n`; 67 + } 68 + 69 + // Completed count 70 + response += `Completed: ${String(state.completedCount)} plans\n\n`; 71 + 72 + // Suggestion 73 + if (state.activePlan) { 74 + response += 75 + "An active plan exists. Read the plan file to continue, or use 9plan_plan_complete/defer/discard to close it out."; 76 + } else if (state.queue.length > 0) { 77 + response += "Use 9plan_queue_pull to get the next plan."; 78 + } else if (state.completedCount > 0) { 79 + response += 80 + "Task complete! Use 9plan_history_search to review completed work."; 81 + } else { 82 + response += "Session is empty. Use 9plan_queue_add to add plans."; 83 + } 84 + 85 + // Close store 86 + store.close(); 87 + 88 + return { 89 + content: [ 90 + { type: "text", text: formatResponse(state.sessionName, response) }, 91 + ], 92 + }; 93 + } catch (error) { 94 + if (error instanceof NinePlanError) { 95 + return { 96 + content: [{ type: "text", text: error.format() }], 97 + }; 98 + } 99 + throw error; 100 + } 101 + } 102 + 103 + // Tool configuration for registration 104 + export const sessionResumeToolConfig = { 105 + title: "Resume Session", 106 + description: 107 + "Loads an existing session by its three-word name and returns its current state including queue contents, active plan, and completed count.", 108 + inputSchema: sessionResumeInputSchema, 109 + };
+167
src/types.ts
··· 1 + /** 2 + * Core TypeScript interfaces for 9plan MCP server 3 + */ 4 + 5 + import type { Logger } from "pino"; 6 + 7 + // ============================================================================ 8 + // Plan Status 9 + // ============================================================================ 10 + 11 + export type PlanStatus = "queued" | "active" | "completed" | "discarded"; 12 + 13 + // ============================================================================ 14 + // Core Data Types 15 + // ============================================================================ 16 + 17 + export interface Session { 18 + name: string; 19 + taskDescription: string | null; 20 + createdAt: string; 21 + } 22 + 23 + export interface Plan { 24 + id: string; 25 + sessionName: string; 26 + status: PlanStatus; 27 + queuePosition: number | null; 28 + goal: string; 29 + context: string | null; 30 + inputs: string | null; 31 + outputs: string | null; 32 + approach: string | null; 33 + successCriteria: string | null; 34 + notes: string | null; 35 + outcome: string | null; 36 + createdAt: string; 37 + completedAt: string | null; 38 + } 39 + 40 + export interface QueuedPlan { 41 + id: string; 42 + goal: string; 43 + queuePosition: number; 44 + } 45 + 46 + export interface SessionState { 47 + sessionName: string; 48 + sessionPath: string; 49 + taskDescription: string | null; 50 + queue: QueuedPlan[]; 51 + activePlan: { id: string; filePath: string; goal: string } | null; 52 + completedCount: number; 53 + } 54 + 55 + export interface HistoryMatch { 56 + id: string; 57 + goal: string; 58 + outcome: string | null; 59 + relevanceScore: number; 60 + } 61 + 62 + // ============================================================================ 63 + // Plan Input/Output Types 64 + // ============================================================================ 65 + 66 + export interface PlanInput { 67 + context: string; 68 + goal: string; 69 + approach: string; 70 + successCriteria: string; 71 + inputs?: string; 72 + outputs?: string; 73 + } 74 + 75 + export interface PlanContent { 76 + context: string; 77 + goal: string; 78 + inputs: string; 79 + outputs: string; 80 + approach: string; 81 + successCriteria: string; 82 + notes: string; 83 + } 84 + 85 + // ============================================================================ 86 + // Dependency Injection Interfaces 87 + // ============================================================================ 88 + 89 + export interface PlanFileHandler { 90 + write(planId: string, content: PlanContent): void; 91 + read(planId: string): PlanContent; 92 + delete(planId: string): void; 93 + exists(planId: string): boolean; 94 + appendToNotes(planId: string, note: string): void; 95 + getFilePath(planId: string): string; 96 + } 97 + 98 + export interface SessionStoreDeps { 99 + logger: Logger; 100 + generatePlanId: () => string; 101 + planFiles: PlanFileHandler; 102 + } 103 + 104 + export interface PlanFileHandlerDeps { 105 + logger: Logger; 106 + } 107 + 108 + /** 109 + * Interface for SessionStore public methods - used by tool handlers 110 + * This allows mocking without coupling to the concrete class 111 + */ 112 + export interface SessionStoreInterface { 113 + addPlan(input: PlanInput, position?: "front" | "back"): Plan; 114 + getHistoryPlan(planId: string): Plan | null; 115 + searchHistory(query: string, maxResults?: number): HistoryMatch[]; 116 + getSessionPath(): string; 117 + close(): void; 118 + pullPlan(): Plan; 119 + getCompletedCount(): number; 120 + getActivePlan(): Plan | null; 121 + deferPlan(reason: string, position?: "front" | "back"): void; 122 + getQueue(): QueuedPlan[]; 123 + discardPlan(reason: string): void; 124 + completePlan(outcome: string): void; 125 + getState(): SessionState; 126 + } 127 + 128 + // ============================================================================ 129 + // Error Types 130 + // ============================================================================ 131 + 132 + export type ErrorCategory = 133 + | "SESSION_NOT_FOUND" 134 + | "PLAN_NOT_FOUND" 135 + | "NO_ACTIVE_PLAN" 136 + | "PLAN_ALREADY_ACTIVE" 137 + | "QUEUE_EMPTY" 138 + | "VALIDATION_ERROR" 139 + | "FILE_SYSTEM_ERROR" 140 + | "DATABASE_ERROR"; 141 + 142 + export class NinePlanError extends Error { 143 + constructor( 144 + public readonly category: ErrorCategory, 145 + message: string, 146 + public readonly suggestion?: string, 147 + ) { 148 + super(message); 149 + this.name = "NinePlanError"; 150 + } 151 + 152 + format(sessionName?: string): string { 153 + const prefix = sessionName ? `[Session: ${sessionName}] ` : ""; 154 + const suggestion = this.suggestion 155 + ? `\n\nSuggested action: ${this.suggestion}` 156 + : ""; 157 + return `${prefix}Error: ${this.category}\n\n${this.message}${suggestion}`; 158 + } 159 + } 160 + 161 + // ============================================================================ 162 + // Tool Response Helpers 163 + // ============================================================================ 164 + 165 + export function formatResponse(sessionName: string, content: string): string { 166 + return `[Session: ${sessionName}]\n\n${content}`; 167 + }
+89
tests/fixtures/plans.ts
··· 1 + /** 2 + * Test fixtures for plan data 3 + */ 4 + 5 + import type { PlanInput, PlanContent } from "../../src/types.js"; 6 + 7 + /** 8 + * Valid plan input for testing 9 + */ 10 + export const validPlanInput: PlanInput = { 11 + context: "Test context for unit testing the 9plan system", 12 + goal: "Verify the system works correctly", 13 + approach: 14 + "1. Set up test environment\n2. Run assertions\n3. Verify expected behavior", 15 + successCriteria: "All assertions pass without errors", 16 + inputs: "", 17 + outputs: "Test results and verification report", 18 + }; 19 + 20 + /** 21 + * Plan input with dependencies 22 + */ 23 + export const planWithDependencies: PlanInput = { 24 + context: "Building feature that depends on authentication module", 25 + goal: "Implement feature X with authentication", 26 + approach: "1. Import auth module\n2. Implement feature\n3. Test integration", 27 + successCriteria: "Feature works with authentication", 28 + inputs: "- auth_client module: from Authentication work", 29 + outputs: "- feature_x implementation: authenticated feature", 30 + }; 31 + 32 + /** 33 + * Minimal plan input (required fields only) 34 + */ 35 + export const minimalPlanInput: PlanInput = { 36 + context: "Minimal test context", 37 + goal: "Minimal test goal", 38 + approach: "Minimal approach", 39 + successCriteria: "Done", 40 + }; 41 + 42 + /** 43 + * Valid plan content for file operations 44 + */ 45 + export const validPlanContent: PlanContent = { 46 + context: "Test context for unit testing", 47 + goal: "Verify the system works", 48 + inputs: "- dependency: from other work", 49 + outputs: "- result: test output", 50 + approach: "Run the tests", 51 + successCriteria: "Tests pass", 52 + notes: "", 53 + }; 54 + 55 + /** 56 + * Plan content with notes 57 + */ 58 + export const planContentWithNotes: PlanContent = { 59 + context: "Context with progress notes", 60 + goal: "Complete task with checkpoints", 61 + inputs: "", 62 + outputs: "- deliverable: output", 63 + approach: "Step by step execution", 64 + successCriteria: "All steps complete", 65 + notes: 66 + "[2024-01-15 10:00] Started work\n[2024-01-15 11:00] Checkpoint 1 done", 67 + }; 68 + 69 + /** 70 + * Create a plan input with custom values 71 + */ 72 + export function createPlanInput(overrides: Partial<PlanInput> = {}): PlanInput { 73 + return { 74 + ...validPlanInput, 75 + ...overrides, 76 + }; 77 + } 78 + 79 + /** 80 + * Create plan content with custom values 81 + */ 82 + export function createPlanContent( 83 + overrides: Partial<PlanContent> = {}, 84 + ): PlanContent { 85 + return { 86 + ...validPlanContent, 87 + ...overrides, 88 + }; 89 + }
+187
tests/helpers/mocks.ts
··· 1 + /** 2 + * Mock factory for unit tests 3 + * 4 + * Creates fresh mocks for each test to avoid state leakage. 5 + */ 6 + 7 + import { vi, type MockedFunction } from "vitest"; 8 + import type { Logger } from "pino"; 9 + import type { 10 + PlanFileHandler, 11 + SessionStoreDeps, 12 + PlanContent, 13 + Plan, 14 + SessionState, 15 + PlanInput, 16 + HistoryMatch, 17 + QueuedPlan, 18 + SessionStoreInterface, 19 + } from "../../src/types.js"; 20 + 21 + /** 22 + * Mock type for SessionStore with only the methods needed by tool handlers 23 + * Extends SessionStoreInterface for compatibility with tool handlers 24 + */ 25 + export interface MockSessionStore extends SessionStoreInterface { 26 + addPlan: MockedFunction<(input: PlanInput, position?: "front" | "back") => Plan>; 27 + getHistoryPlan: MockedFunction<(planId: string) => Plan | null>; 28 + searchHistory: MockedFunction<(query: string, maxResults?: number) => HistoryMatch[]>; 29 + getSessionPath: MockedFunction<() => string>; 30 + close: MockedFunction<() => void>; 31 + pullPlan: MockedFunction<() => Plan>; 32 + getCompletedCount: MockedFunction<() => number>; 33 + getActivePlan: MockedFunction<() => Plan | null>; 34 + deferPlan: MockedFunction<(reason: string, position?: "front" | "back") => void>; 35 + getQueue: MockedFunction<() => QueuedPlan[]>; 36 + discardPlan: MockedFunction<(reason: string) => void>; 37 + completePlan: MockedFunction<(outcome: string) => void>; 38 + getState: MockedFunction<() => SessionState>; 39 + } 40 + 41 + /** 42 + * Create a mock logger 43 + */ 44 + export function createMockLogger(): Logger { 45 + const childLogger = { 46 + info: vi.fn(), 47 + error: vi.fn(), 48 + warn: vi.fn(), 49 + debug: vi.fn(), 50 + trace: vi.fn(), 51 + fatal: vi.fn(), 52 + child: vi.fn(), 53 + }; 54 + childLogger.child.mockReturnValue(childLogger); 55 + 56 + return { 57 + info: vi.fn(), 58 + error: vi.fn(), 59 + warn: vi.fn(), 60 + debug: vi.fn(), 61 + trace: vi.fn(), 62 + fatal: vi.fn(), 63 + child: vi.fn().mockReturnValue(childLogger), 64 + level: "info", 65 + } as unknown as Logger; 66 + } 67 + 68 + /** 69 + * Create a mock plan file handler 70 + */ 71 + export function createMockPlanFileHandler(): PlanFileHandler { 72 + const files = new Map<string, PlanContent>(); 73 + 74 + return { 75 + write: vi.fn((planId: string, content: PlanContent) => { 76 + files.set(planId, content); 77 + }), 78 + read: vi.fn((planId: string) => { 79 + const content = files.get(planId); 80 + if (!content) { 81 + throw new Error(`Plan file not found: ${planId}`); 82 + } 83 + return content; 84 + }), 85 + delete: vi.fn((planId: string) => { 86 + files.delete(planId); 87 + }), 88 + exists: vi.fn((planId: string) => { 89 + return files.has(planId); 90 + }), 91 + appendToNotes: vi.fn((planId: string, note: string) => { 92 + const content = files.get(planId); 93 + if (content) { 94 + content.notes = content.notes ? `${content.notes}\n${note}` : note; 95 + files.set(planId, content); 96 + } 97 + }), 98 + getFilePath: vi.fn((planId: string) => `/tmp/test/plans/${planId}.txt`), 99 + }; 100 + } 101 + 102 + /** 103 + * Create mock plan ID generator with predictable IDs 104 + */ 105 + export function createMockPlanIdGenerator(): () => string { 106 + let counter = 0; 107 + return vi.fn(() => { 108 + counter++; 109 + return `test${counter.toString().padStart(2, "0")}`; 110 + }); 111 + } 112 + 113 + /** 114 + * Create all mocks needed for SessionStore 115 + */ 116 + export function createSessionStoreMocks(): SessionStoreDeps { 117 + return { 118 + logger: createMockLogger(), 119 + generatePlanId: createMockPlanIdGenerator(), 120 + planFiles: createMockPlanFileHandler(), 121 + }; 122 + } 123 + 124 + /** 125 + * Create a mock Plan object with default values 126 + * Allows partial overrides for test convenience 127 + */ 128 + export function createMockPlan(overrides: Partial<Plan> = {}): Plan { 129 + return { 130 + id: "test01", 131 + sessionName: "test-session", 132 + status: "queued", 133 + queuePosition: 1, 134 + goal: "Test goal", 135 + context: null, 136 + inputs: null, 137 + outputs: null, 138 + approach: null, 139 + successCriteria: null, 140 + notes: null, 141 + outcome: null, 142 + createdAt: "2024-01-15T10:00:00Z", 143 + completedAt: null, 144 + ...overrides, 145 + }; 146 + } 147 + 148 + /** 149 + * Create a mock SessionStore for tool tests 150 + */ 151 + export function createMockSessionStore(): MockSessionStore { 152 + return { 153 + addPlan: vi.fn(), 154 + getHistoryPlan: vi.fn(), 155 + searchHistory: vi.fn(), 156 + getSessionPath: vi.fn(), 157 + close: vi.fn(), 158 + pullPlan: vi.fn(), 159 + getCompletedCount: vi.fn(), 160 + getActivePlan: vi.fn(), 161 + deferPlan: vi.fn(), 162 + getQueue: vi.fn(), 163 + discardPlan: vi.fn(), 164 + completePlan: vi.fn(), 165 + getState: vi.fn(), 166 + }; 167 + } 168 + 169 + /** 170 + * Reset all mocks in a SessionStoreDeps object 171 + */ 172 + export function resetMocks(mocks: SessionStoreDeps): void { 173 + 174 + vi.mocked(mocks.generatePlanId).mockClear(); 175 + // eslint-disable-next-line @typescript-eslint/unbound-method 176 + vi.mocked(mocks.planFiles.write).mockClear(); 177 + // eslint-disable-next-line @typescript-eslint/unbound-method 178 + vi.mocked(mocks.planFiles.read).mockClear(); 179 + // eslint-disable-next-line @typescript-eslint/unbound-method 180 + vi.mocked(mocks.planFiles.delete).mockClear(); 181 + // eslint-disable-next-line @typescript-eslint/unbound-method 182 + vi.mocked(mocks.planFiles.exists).mockClear(); 183 + // eslint-disable-next-line @typescript-eslint/unbound-method 184 + vi.mocked(mocks.planFiles.appendToNotes).mockClear(); 185 + // eslint-disable-next-line @typescript-eslint/unbound-method 186 + vi.mocked(mocks.planFiles.getFilePath).mockClear(); 187 + }
+73
tests/helpers/temp.ts
··· 1 + /** 2 + * Temporary directory helpers for integration tests 3 + */ 4 + 5 + import { mkdtempSync, rmSync, mkdirSync } from "node:fs"; 6 + import { tmpdir } from "node:os"; 7 + import { join } from "node:path"; 8 + 9 + /** 10 + * Create a temporary directory and run a function with it. 11 + * The directory is cleaned up automatically after the function completes. 12 + */ 13 + export async function withTempDir<T>( 14 + fn: (dir: string) => T | Promise<T>, 15 + ): Promise<T> { 16 + const dir = mkdtempSync(join(tmpdir(), "9plan-test-")); 17 + try { 18 + return await fn(dir); 19 + } finally { 20 + rmSync(dir, { recursive: true, force: true }); 21 + } 22 + } 23 + 24 + /** 25 + * Create a temporary directory and run a sync function with it. 26 + * The directory is cleaned up automatically after the function completes. 27 + */ 28 + export function withTempDirSync<T>(fn: (dir: string) => T): T { 29 + const dir = mkdtempSync(join(tmpdir(), "9plan-test-")); 30 + try { 31 + return fn(dir); 32 + } finally { 33 + rmSync(dir, { recursive: true, force: true }); 34 + } 35 + } 36 + 37 + /** 38 + * Create a temporary session directory structure 39 + */ 40 + export function createTempSessionDir(): { 41 + sessionPath: string; 42 + plansDir: string; 43 + cleanup: () => void; 44 + } { 45 + const sessionPath = mkdtempSync(join(tmpdir(), "9plan-session-")); 46 + const plansDir = join(sessionPath, "plans"); 47 + mkdirSync(plansDir, { recursive: true }); 48 + 49 + return { 50 + sessionPath, 51 + plansDir, 52 + cleanup: () => { 53 + rmSync(sessionPath, { recursive: true, force: true }); 54 + }, 55 + }; 56 + } 57 + 58 + /** 59 + * Create a test sessions base directory 60 + */ 61 + export function createTempSessionsDir(): { 62 + sessionsPath: string; 63 + cleanup: () => void; 64 + } { 65 + const sessionsPath = mkdtempSync(join(tmpdir(), "9plan-sessions-")); 66 + 67 + return { 68 + sessionsPath, 69 + cleanup: () => { 70 + rmSync(sessionsPath, { recursive: true, force: true }); 71 + }, 72 + }; 73 + }
+277
tests/integration/decomposition.test.ts
··· 1 + /** 2 + * Integration tests for decomposition workflow 3 + * 4 + * Tests the pattern: pull parent -> add children -> defer parent -> complete children -> pull parent 5 + * This is a core workflow for breaking down complex tasks. 6 + */ 7 + 8 + import { describe, it, expect, beforeEach, afterEach } from "vitest"; 9 + import { SessionStore } from "../../src/db/session-store.js"; 10 + import { PlanFileHandlerImpl } from "../../src/files/plan-files.js"; 11 + import { generatePlanId } from "../../src/generators/plan-id.js"; 12 + import { createMockLogger } from "../helpers/mocks.js"; 13 + import { createTempSessionDir } from "../helpers/temp.js"; 14 + 15 + describe("Decomposition workflow", () => { 16 + let store: SessionStore; 17 + let planFiles: PlanFileHandlerImpl; 18 + let cleanup: () => void; 19 + 20 + beforeEach(() => { 21 + const temp = createTempSessionDir(); 22 + cleanup = temp.cleanup; 23 + 24 + const logger = createMockLogger(); 25 + planFiles = new PlanFileHandlerImpl(temp.sessionPath, { logger }); 26 + 27 + store = new SessionStore("decomposition-test", temp.sessionPath, { 28 + logger, 29 + generatePlanId, 30 + planFiles, 31 + }); 32 + 33 + store.createSession("Decomposition test session"); 34 + }); 35 + 36 + afterEach(() => { 37 + store.close(); 38 + cleanup(); 39 + }); 40 + 41 + it("should handle decomposition: pull parent -> add children -> defer parent -> complete children -> pull parent", () => { 42 + // Step 1: Add parent plan 43 + const parent = store.addPlan({ 44 + context: "Building a complete authentication system", 45 + goal: "Implement full authentication flow", 46 + approach: "Break down into login, registration, and password reset", 47 + successCriteria: "All auth features working with tests", 48 + }); 49 + 50 + // Step 2: Pull parent 51 + store.pullPlan(); 52 + expect(store.getActivePlan()?.id).toBe(parent.id); 53 + 54 + // Step 3: Add child plans at front (in reverse order for correct execution) 55 + const child3 = store.addPlan( 56 + { 57 + context: "Part of auth system implementation", 58 + goal: "Implement password reset", 59 + approach: "Build email verification and reset form", 60 + successCriteria: "Password reset emails sent and processed", 61 + outputs: "password_reset_module", 62 + }, 63 + "front", 64 + ); 65 + 66 + const child2 = store.addPlan( 67 + { 68 + context: "Part of auth system implementation", 69 + goal: "Implement user registration", 70 + approach: "Build registration form with validation", 71 + successCriteria: "Users can register new accounts", 72 + outputs: "registration_module", 73 + }, 74 + "front", 75 + ); 76 + 77 + const child1 = store.addPlan( 78 + { 79 + context: "Part of auth system implementation", 80 + goal: "Implement login functionality", 81 + approach: "Build login form and session management", 82 + successCriteria: "Users can log in and out", 83 + outputs: "login_module", 84 + }, 85 + "front", 86 + ); 87 + 88 + // Step 4: Defer parent to back 89 + store.deferPlan( 90 + "Decomposed into subtasks: login, registration, password reset", 91 + "back", 92 + ); 93 + 94 + // Verify queue order: children at front, parent at back 95 + const queueAfterDefer = store.getQueue(); 96 + expect(queueAfterDefer).toHaveLength(4); 97 + expect(queueAfterDefer[0]?.id).toBe(child1.id); 98 + expect(queueAfterDefer[1]?.id).toBe(child2.id); 99 + expect(queueAfterDefer[2]?.id).toBe(child3.id); 100 + expect(queueAfterDefer[3]?.id).toBe(parent.id); 101 + 102 + // Step 5: Complete children 103 + store.pullPlan(); 104 + store.completePlan("Login module implemented with session cookies"); 105 + 106 + store.pullPlan(); 107 + store.completePlan("Registration module with email validation"); 108 + 109 + store.pullPlan(); 110 + store.completePlan("Password reset with email tokens"); 111 + 112 + // Step 6: Pull parent again 113 + const parentAgain = store.pullPlan(); 114 + expect(parentAgain.id).toBe(parent.id); 115 + 116 + // Parent can now aggregate child outcomes via history 117 + const loginResult = store.searchHistory("login_module"); 118 + const regResult = store.searchHistory("registration_module"); 119 + const resetResult = store.searchHistory("password_reset"); 120 + 121 + expect(loginResult.length).toBeGreaterThan(0); 122 + expect(regResult.length).toBeGreaterThan(0); 123 + expect(resetResult.length).toBeGreaterThan(0); 124 + 125 + // Complete parent 126 + store.completePlan("All auth modules integrated and tested"); 127 + 128 + expect(store.getCompletedCount()).toBe(4); 129 + expect(store.getQueue()).toHaveLength(0); 130 + }); 131 + 132 + it("should accumulate notes on multiple deferrals", () => { 133 + const plan = store.addPlan({ 134 + context: "Complex task requiring multiple attempts", 135 + goal: "Complete tricky implementation", 136 + approach: "Try different approaches", 137 + successCriteria: "Working implementation", 138 + }); 139 + 140 + // First attempt - blocked 141 + store.pullPlan(); 142 + store.deferPlan("Blocked by missing API docs", "back"); 143 + 144 + // Second attempt - still blocked 145 + store.pullPlan(); 146 + store.deferPlan("API docs arrived but need clarification", "back"); 147 + 148 + // Third attempt - almost there 149 + store.pullPlan(); 150 + store.deferPlan("Implementation 90% done, need review", "back"); 151 + 152 + // Read plan file to check accumulated notes 153 + const content = planFiles.read(plan.id); 154 + 155 + expect(content.notes).toContain("Blocked by missing API docs"); 156 + expect(content.notes).toContain("API docs arrived but need clarification"); 157 + expect(content.notes).toContain("Implementation 90% done, need review"); 158 + 159 + // All notes should have timestamps 160 + const timestampMatches = content.notes.match(/\[\d{4}-\d{2}-\d{2}/g); 161 + expect(timestampMatches).toHaveLength(3); 162 + }); 163 + 164 + it("should allow parent to aggregate child outcomes via history_get", () => { 165 + // Add and complete child plans first 166 + const child1 = store.addPlan({ 167 + context: "Module development", 168 + goal: "Build API client", 169 + approach: "HTTP wrapper with retry logic", 170 + successCriteria: "Client handles all endpoints", 171 + outputs: "api_client module", 172 + }); 173 + store.pullPlan(); 174 + store.completePlan("API client ready at src/api-client.ts"); 175 + 176 + const child2 = store.addPlan({ 177 + context: "Module development", 178 + goal: "Build data validator", 179 + approach: "Zod schemas for all models", 180 + successCriteria: "All data validated at runtime", 181 + outputs: "validator module", 182 + }); 183 + store.pullPlan(); 184 + store.completePlan("Validator at src/validator.ts with 20 schemas"); 185 + 186 + // Parent plan can retrieve completed child details 187 + const apiClientPlan = store.getHistoryPlan(child1.id); 188 + const validatorPlan = store.getHistoryPlan(child2.id); 189 + 190 + expect(apiClientPlan?.outcome).toBe( 191 + "API client ready at src/api-client.ts", 192 + ); 193 + expect(validatorPlan?.outcome).toBe( 194 + "Validator at src/validator.ts with 20 schemas", 195 + ); 196 + }); 197 + 198 + it("should preserve queue order during decomposition", () => { 199 + // Add some initial plans 200 + store.addPlan({ 201 + context: "ctx", 202 + goal: "Existing plan 1", 203 + approach: "app", 204 + successCriteria: "done", 205 + }); 206 + store.addPlan({ 207 + context: "ctx", 208 + goal: "Existing plan 2", 209 + approach: "app", 210 + successCriteria: "done", 211 + }); 212 + 213 + // Pull first and decompose 214 + store.pullPlan(); 215 + 216 + // Add children at front 217 + store.addPlan( 218 + { 219 + context: "ctx", 220 + goal: "Child A", 221 + approach: "app", 222 + successCriteria: "done", 223 + }, 224 + "front", 225 + ); 226 + store.addPlan( 227 + { 228 + context: "ctx", 229 + goal: "Child B", 230 + approach: "app", 231 + successCriteria: "done", 232 + }, 233 + "front", 234 + ); 235 + 236 + // Defer parent to back 237 + store.deferPlan("Decomposed", "back"); 238 + 239 + // Queue should be: Child B, Child A, Existing plan 2, Existing plan 1 (parent) 240 + const queue = store.getQueue(); 241 + expect(queue[0]?.goal).toBe("Child B"); 242 + expect(queue[1]?.goal).toBe("Child A"); 243 + expect(queue[2]?.goal).toBe("Existing plan 2"); 244 + expect(queue[3]?.goal).toBe("Existing plan 1"); 245 + }); 246 + 247 + it("should handle defer to front for blocking subtasks", () => { 248 + store.addPlan({ 249 + context: "Need to complete blocking work first", 250 + goal: "Main task", 251 + approach: "Identify blockers and resolve", 252 + successCriteria: "Task complete", 253 + }); 254 + 255 + store.pullPlan(); 256 + 257 + // Realize we need something done first - add blocker at front 258 + store.addPlan( 259 + { 260 + context: "Blocking work", 261 + goal: "Resolve blocker", 262 + approach: "Fix the dependency", 263 + successCriteria: "Blocker resolved", 264 + }, 265 + "front", 266 + ); 267 + 268 + // Defer parent to BACK so blocker runs first 269 + // (deferring to front would put parent BEFORE blocker, which is wrong) 270 + store.deferPlan("Need blocker resolved first", "back"); 271 + 272 + const queue = store.getQueue(); 273 + // Blocker should be first (position 1), parent at back (position 2) 274 + expect(queue[0]?.goal).toBe("Resolve blocker"); 275 + expect(queue[1]?.goal).toBe("Main task"); 276 + }); 277 + });
+234
tests/integration/history-search.test.ts
··· 1 + /** 2 + * Integration tests for FTS5 history search 3 + * 4 + * Tests the full-text search functionality for finding completed plans. 5 + * Uses real SQLite with FTS5 indexing. 6 + */ 7 + 8 + import { describe, it, expect, beforeEach, afterEach } from "vitest"; 9 + import { SessionStore } from "../../src/db/session-store.js"; 10 + import { PlanFileHandlerImpl } from "../../src/files/plan-files.js"; 11 + import { generatePlanId } from "../../src/generators/plan-id.js"; 12 + import { createMockLogger } from "../helpers/mocks.js"; 13 + import { createTempSessionDir } from "../helpers/temp.js"; 14 + 15 + describe("History search integration", () => { 16 + let store: SessionStore; 17 + let cleanup: () => void; 18 + 19 + beforeEach(() => { 20 + const temp = createTempSessionDir(); 21 + cleanup = temp.cleanup; 22 + 23 + const logger = createMockLogger(); 24 + const planFiles = new PlanFileHandlerImpl(temp.sessionPath, { logger }); 25 + 26 + store = new SessionStore("search-test", temp.sessionPath, { 27 + logger, 28 + generatePlanId, 29 + planFiles, 30 + }); 31 + 32 + store.createSession("History search test session"); 33 + 34 + // Seed with some completed plans for search tests 35 + seedCompletedPlans(store); 36 + }); 37 + 38 + afterEach(() => { 39 + store.close(); 40 + cleanup(); 41 + }); 42 + 43 + function seedCompletedPlans(store: SessionStore): void { 44 + const plans = [ 45 + { 46 + goal: "Implement user authentication module", 47 + context: "Building secure login system", 48 + approach: "Use JWT tokens with refresh mechanism", 49 + successCriteria: "Secure auth with token rotation", 50 + outputs: "auth_module", 51 + outcome: "Authentication module complete with JWT and refresh tokens", 52 + }, 53 + { 54 + goal: "Build REST API client", 55 + context: "Need HTTP client for external services", 56 + approach: "Axios wrapper with retry and caching", 57 + successCriteria: "Reliable API communication", 58 + outputs: "api_client", 59 + outcome: "API client with automatic retry and response caching", 60 + }, 61 + { 62 + goal: "Create database schema", 63 + context: "PostgreSQL database design", 64 + approach: "Normalized schema with proper indexes", 65 + successCriteria: "Efficient queries and data integrity", 66 + outputs: "database_schema", 67 + outcome: "Schema with 15 tables and optimized indexes", 68 + }, 69 + { 70 + goal: "Implement caching layer", 71 + context: "Redis integration for performance", 72 + approach: "Cache invalidation with TTL strategy", 73 + successCriteria: "Sub-10ms cache hits", 74 + outputs: "cache_module", 75 + outcome: "Redis caching with 5ms average response time", 76 + }, 77 + { 78 + goal: "Build user dashboard", 79 + context: "React frontend for user management", 80 + approach: "Component-based architecture with hooks", 81 + successCriteria: "Interactive dashboard with charts", 82 + outputs: "dashboard_ui", 83 + outcome: "Dashboard with real-time updates and D3 charts", 84 + }, 85 + ]; 86 + 87 + for (const plan of plans) { 88 + store.addPlan({ 89 + context: plan.context, 90 + goal: plan.goal, 91 + approach: plan.approach, 92 + successCriteria: plan.successCriteria, 93 + outputs: plan.outputs, 94 + }); 95 + store.pullPlan(); 96 + store.completePlan(plan.outcome); 97 + } 98 + } 99 + 100 + it("should find completed plans by goal keywords", () => { 101 + const results = store.searchHistory("authentication"); 102 + 103 + expect(results.length).toBeGreaterThan(0); 104 + expect( 105 + results.some((r) => r.goal.toLowerCase().includes("authentication")), 106 + ).toBe(true); 107 + }); 108 + 109 + it("should find completed plans by output keywords", () => { 110 + const results = store.searchHistory("api_client"); 111 + 112 + expect(results.length).toBeGreaterThan(0); 113 + expect(results[0]?.goal).toContain("API"); 114 + }); 115 + 116 + it("should find completed plans by outcome keywords", () => { 117 + const results = store.searchHistory("Redis"); 118 + 119 + expect(results.length).toBeGreaterThan(0); 120 + expect(results[0]?.outcome).toContain("Redis"); 121 + }); 122 + 123 + it("should rank more relevant matches higher", () => { 124 + // Search for "database" - should prioritize the database-specific plan 125 + const results = store.searchHistory("database"); 126 + 127 + expect(results.length).toBeGreaterThan(0); 128 + expect(results[0]?.goal.toLowerCase()).toContain("database"); 129 + }); 130 + 131 + it("should return empty results for non-matching queries", () => { 132 + const results = store.searchHistory("xyznonexistent123"); 133 + 134 + expect(results).toHaveLength(0); 135 + }); 136 + 137 + it("should support dependency resolution workflow", () => { 138 + // Simulate: Agent needs to find auth module from previous work 139 + // Search by semantic description 140 + const results = store.searchHistory("authentication JWT tokens"); 141 + 142 + expect(results.length).toBeGreaterThan(0); 143 + expect(results[0]?.outcome).toContain("JWT"); 144 + 145 + // Can then use getHistoryPlan for full details 146 + const fullPlan = store.getHistoryPlan(results[0]!.id); 147 + expect(fullPlan).not.toBeNull(); 148 + expect(fullPlan?.outputs).toBe("auth_module"); 149 + }); 150 + 151 + it("should respect max_results limit", () => { 152 + // Search with wildcard-like broad term 153 + const results = store.searchHistory( 154 + "module OR client OR schema OR cache", 155 + 2, 156 + ); 157 + 158 + expect(results.length).toBeLessThanOrEqual(2); 159 + }); 160 + 161 + it("should search across multiple fields", () => { 162 + // Add a plan with unique text in different fields 163 + store.addPlan({ 164 + context: "Implementing frobnicator system", 165 + goal: "Build widget generator", 166 + approach: "Use bazinator pattern", 167 + successCriteria: "Widgets generated correctly", 168 + outputs: "quuxinator_module", 169 + }); 170 + store.pullPlan(); 171 + store.completePlan("Successfully built gizmo processor"); 172 + 173 + // Should find by context 174 + const contextResults = store.searchHistory("frobnicator"); 175 + expect(contextResults.length).toBeGreaterThan(0); 176 + 177 + // Should find by outcome 178 + const outcomeResults = store.searchHistory("gizmo"); 179 + expect(outcomeResults.length).toBeGreaterThan(0); 180 + }); 181 + 182 + it("should handle special characters in search queries", () => { 183 + // FTS5 might have issues with certain characters 184 + const results = store.searchHistory("API"); 185 + 186 + // Should still work 187 + expect(results.length).toBeGreaterThan(0); 188 + }); 189 + 190 + it("should return relevance scores", () => { 191 + const results = store.searchHistory("authentication"); 192 + 193 + expect(results[0]?.relevanceScore).toBeDefined(); 194 + expect(typeof results[0]?.relevanceScore).toBe("number"); 195 + expect(results[0]?.relevanceScore).toBeGreaterThan(0); 196 + }); 197 + 198 + it("should not include discarded plans in search", () => { 199 + // Add and discard a plan 200 + store.addPlan({ 201 + context: "This will be discarded", 202 + goal: "Implement unicorn sparkle feature", 203 + approach: "Glitter everywhere", 204 + successCriteria: "Maximum sparkle", 205 + }); 206 + store.pullPlan(); 207 + store.discardPlan("Changed requirements"); 208 + 209 + // Should not find it 210 + const results = store.searchHistory("unicorn sparkle"); 211 + expect(results).toHaveLength(0); 212 + }); 213 + 214 + it("should not include queued or active plans in search", () => { 215 + // Add a plan but don't complete it 216 + store.addPlan({ 217 + context: "Still in queue", 218 + goal: "Implement dragon feature", 219 + approach: "Fire breathing logic", 220 + successCriteria: "Dragons work", 221 + }); 222 + 223 + // Should not find queued plan 224 + let results = store.searchHistory("dragon"); 225 + expect(results).toHaveLength(0); 226 + 227 + // Pull but don't complete 228 + store.pullPlan(); 229 + 230 + // Should not find active plan 231 + results = store.searchHistory("dragon"); 232 + expect(results).toHaveLength(0); 233 + }); 234 + });
+201
tests/integration/lifecycle.test.ts
··· 1 + /** 2 + * Integration tests for full plan lifecycle 3 + * 4 + * Tests the complete flow: create -> add -> pull -> complete -> search 5 + * Uses real SQLite and real filesystem with temp directories. 6 + */ 7 + 8 + import { describe, it, expect, beforeEach, afterEach } from "vitest"; 9 + import { SessionStore } from "../../src/db/session-store.js"; 10 + import { PlanFileHandlerImpl } from "../../src/files/plan-files.js"; 11 + import { generatePlanId } from "../../src/generators/plan-id.js"; 12 + import { createMockLogger } from "../helpers/mocks.js"; 13 + import { createTempSessionDir } from "../helpers/temp.js"; 14 + import { validPlanInput } from "../fixtures/plans.js"; 15 + 16 + describe("Full plan lifecycle", () => { 17 + let store: SessionStore; 18 + let planFiles: PlanFileHandlerImpl; 19 + let sessionPath: string; 20 + let cleanup: () => void; 21 + 22 + beforeEach(() => { 23 + const temp = createTempSessionDir(); 24 + cleanup = temp.cleanup; 25 + sessionPath = temp.sessionPath; 26 + 27 + const logger = createMockLogger(); 28 + 29 + planFiles = new PlanFileHandlerImpl(sessionPath, { logger }); 30 + 31 + store = new SessionStore("integration-test", sessionPath, { 32 + logger, 33 + generatePlanId, 34 + planFiles, 35 + }); 36 + 37 + store.createSession("Integration test session"); 38 + }); 39 + 40 + afterEach(() => { 41 + store.close(); 42 + cleanup(); 43 + }); 44 + 45 + it("should complete full lifecycle: create -> add -> pull -> complete -> search", () => { 46 + // Add a plan 47 + const plan = store.addPlan(validPlanInput); 48 + expect(plan.status).toBe("queued"); 49 + expect(planFiles.exists(plan.id)).toBe(true); 50 + 51 + // Pull the plan 52 + const pulled = store.pullPlan(); 53 + expect(pulled.id).toBe(plan.id); 54 + expect(pulled.status).toBe("active"); 55 + 56 + // Complete the plan 57 + store.completePlan("Successfully verified the system"); 58 + 59 + // File should be deleted after completion 60 + expect(planFiles.exists(plan.id)).toBe(false); 61 + 62 + // Should be searchable in history 63 + const results = store.searchHistory("verify"); 64 + expect(results.length).toBeGreaterThan(0); 65 + expect(results[0]?.id).toBe(plan.id); 66 + }); 67 + 68 + it("should maintain queue order across multiple adds", () => { 69 + // Add plans at back 70 + const plan1 = store.addPlan({ ...validPlanInput, goal: "First" }); 71 + const plan2 = store.addPlan({ ...validPlanInput, goal: "Second" }); 72 + const plan3 = store.addPlan({ ...validPlanInput, goal: "Third" }); 73 + 74 + const queue = store.getQueue(); 75 + 76 + expect(queue[0]?.id).toBe(plan1.id); 77 + expect(queue[1]?.id).toBe(plan2.id); 78 + expect(queue[2]?.id).toBe(plan3.id); 79 + }); 80 + 81 + it("should handle multiple plans completing in sequence", () => { 82 + // Add three plans 83 + store.addPlan({ ...validPlanInput, goal: "Task 1" }); 84 + store.addPlan({ ...validPlanInput, goal: "Task 2" }); 85 + store.addPlan({ ...validPlanInput, goal: "Task 3" }); 86 + 87 + // Complete them in sequence 88 + store.pullPlan(); 89 + store.completePlan("Task 1 done"); 90 + 91 + store.pullPlan(); 92 + store.completePlan("Task 2 done"); 93 + 94 + store.pullPlan(); 95 + store.completePlan("Task 3 done"); 96 + 97 + // All should be in history 98 + expect(store.getCompletedCount()).toBe(3); 99 + expect(store.getQueue()).toHaveLength(0); 100 + expect(store.getActivePlan()).toBeNull(); 101 + }); 102 + 103 + it("should delete file on completion and keep in history", () => { 104 + const plan = store.addPlan(validPlanInput); 105 + const planId = plan.id; 106 + 107 + // File exists while queued 108 + expect(planFiles.exists(planId)).toBe(true); 109 + 110 + store.pullPlan(); 111 + // File still exists while active 112 + expect(planFiles.exists(planId)).toBe(true); 113 + 114 + store.completePlan("Done!"); 115 + // File deleted after completion 116 + expect(planFiles.exists(planId)).toBe(false); 117 + 118 + // But plan is in history 119 + const historyPlan = store.getHistoryPlan(planId); 120 + expect(historyPlan).not.toBeNull(); 121 + expect(historyPlan?.outcome).toBe("Done!"); 122 + }); 123 + 124 + it("should have file for queued and active plans", () => { 125 + const plan1 = store.addPlan({ ...validPlanInput, goal: "Queued plan" }); 126 + const plan2 = store.addPlan({ ...validPlanInput, goal: "Will be active" }); 127 + 128 + // Both should have files 129 + expect(planFiles.exists(plan1.id)).toBe(true); 130 + expect(planFiles.exists(plan2.id)).toBe(true); 131 + 132 + // Pull one to make active 133 + store.pullPlan(); 134 + 135 + // Both should still have files 136 + expect(planFiles.exists(plan1.id)).toBe(true); 137 + expect(planFiles.exists(plan2.id)).toBe(true); 138 + }); 139 + 140 + it("should handle front and back positioning correctly", () => { 141 + // Add at back 142 + store.addPlan({ ...validPlanInput, goal: "Back 1" }); 143 + store.addPlan({ ...validPlanInput, goal: "Back 2" }); 144 + 145 + // Add at front 146 + store.addPlan({ ...validPlanInput, goal: "Front 1" }, "front"); 147 + store.addPlan({ ...validPlanInput, goal: "Front 2" }, "front"); 148 + 149 + const queue = store.getQueue(); 150 + 151 + // Front insertions go to position 1, so last front insertion is first 152 + expect(queue[0]?.goal).toBe("Front 2"); 153 + expect(queue[1]?.goal).toBe("Front 1"); 154 + expect(queue[2]?.goal).toBe("Back 1"); 155 + expect(queue[3]?.goal).toBe("Back 2"); 156 + }); 157 + 158 + it("should preserve queue positions after pull and complete", () => { 159 + store.addPlan({ ...validPlanInput, goal: "First" }); 160 + store.addPlan({ ...validPlanInput, goal: "Second" }); 161 + store.addPlan({ ...validPlanInput, goal: "Third" }); 162 + 163 + // Pull and complete first 164 + store.pullPlan(); 165 + store.completePlan("Done"); 166 + 167 + // Remaining queue should be renumbered 168 + const queue = store.getQueue(); 169 + expect(queue).toHaveLength(2); 170 + expect(queue[0]?.queuePosition).toBe(1); 171 + expect(queue[0]?.goal).toBe("Second"); 172 + expect(queue[1]?.queuePosition).toBe(2); 173 + expect(queue[1]?.goal).toBe("Third"); 174 + }); 175 + 176 + it("should find completed plan via search", () => { 177 + // Add and complete a plan with specific keywords 178 + store.addPlan({ 179 + ...validPlanInput, 180 + goal: "Implement unicorn rainbow sparkle feature", 181 + }); 182 + store.pullPlan(); 183 + store.completePlan("Unicorns are now sparkling beautifully"); 184 + 185 + // Should find by goal keywords 186 + const goalResults = store.searchHistory("unicorn"); 187 + expect(goalResults.length).toBeGreaterThan(0); 188 + 189 + // Should find by outcome keywords 190 + const outcomeResults = store.searchHistory("sparkling"); 191 + expect(outcomeResults.length).toBeGreaterThan(0); 192 + }); 193 + 194 + it("should handle empty queue gracefully", () => { 195 + const state = store.getState(); 196 + 197 + expect(state.queue).toHaveLength(0); 198 + expect(state.activePlan).toBeNull(); 199 + expect(state.completedCount).toBe(0); 200 + }); 201 + });
+573
tests/unit/db/session-store.test.ts
··· 1 + /** 2 + * Tests for SessionStore 3 + * 4 + * Uses real SQLite database (temp directory) but mocks PlanFileHandler 5 + * to isolate database logic from filesystem operations. 6 + */ 7 + 8 + import { describe, it, expect, beforeEach, afterEach } from "vitest"; 9 + import { SessionStore } from "../../../src/db/session-store.js"; 10 + import { NinePlanError } from "../../../src/types.js"; 11 + import { 12 + createMockLogger, 13 + createMockPlanIdGenerator, 14 + createMockPlanFileHandler, 15 + } from "../../helpers/mocks.js"; 16 + import { createTempSessionDir } from "../../helpers/temp.js"; 17 + import { validPlanInput, minimalPlanInput } from "../../fixtures/plans.js"; 18 + 19 + describe("SessionStore", () => { 20 + let store: SessionStore; 21 + let cleanup: () => void; 22 + let mockPlanFiles: ReturnType<typeof createMockPlanFileHandler>; 23 + let mockGeneratePlanId: ReturnType<typeof createMockPlanIdGenerator>; 24 + 25 + beforeEach(() => { 26 + const temp = createTempSessionDir(); 27 + cleanup = temp.cleanup; 28 + 29 + mockPlanFiles = createMockPlanFileHandler(); 30 + mockGeneratePlanId = createMockPlanIdGenerator(); 31 + 32 + store = new SessionStore("test-session", temp.sessionPath, { 33 + logger: createMockLogger(), 34 + generatePlanId: mockGeneratePlanId, 35 + planFiles: mockPlanFiles, 36 + }); 37 + 38 + store.createSession("Test task description"); 39 + }); 40 + 41 + afterEach(() => { 42 + store.close(); 43 + cleanup(); 44 + }); 45 + 46 + // =========================================================================== 47 + // addPlan Tests 48 + // =========================================================================== 49 + 50 + describe("addPlan", () => { 51 + it("should generate unique plan ID", () => { 52 + store.addPlan(validPlanInput); 53 + 54 + expect(mockGeneratePlanId).toHaveBeenCalledTimes(1); 55 + }); 56 + 57 + it("should write plan file", () => { 58 + store.addPlan(validPlanInput); 59 + 60 + // eslint-disable-next-line @typescript-eslint/unbound-method 61 + expect(mockPlanFiles.write).toHaveBeenCalledTimes(1); 62 + // eslint-disable-next-line @typescript-eslint/unbound-method 63 + expect(mockPlanFiles.write).toHaveBeenCalledWith( 64 + "test01", 65 + expect.objectContaining({ 66 + context: validPlanInput.context, 67 + goal: validPlanInput.goal, 68 + }), 69 + ); 70 + }); 71 + 72 + it("should add plan to back of queue by default", () => { 73 + store.addPlan(validPlanInput); 74 + store.addPlan({ ...validPlanInput, goal: "Second plan" }); 75 + 76 + const queue = store.getQueue(); 77 + 78 + expect(queue).toHaveLength(2); 79 + expect(queue[0]?.goal).toBe(validPlanInput.goal); 80 + expect(queue[0]?.queuePosition).toBe(1); 81 + expect(queue[1]?.goal).toBe("Second plan"); 82 + expect(queue[1]?.queuePosition).toBe(2); 83 + }); 84 + 85 + it("should add plan to front of queue when position is front", () => { 86 + store.addPlan(validPlanInput); 87 + store.addPlan({ ...validPlanInput, goal: "Front plan" }, "front"); 88 + 89 + const queue = store.getQueue(); 90 + 91 + expect(queue).toHaveLength(2); 92 + expect(queue[0]?.goal).toBe("Front plan"); 93 + expect(queue[0]?.queuePosition).toBe(1); 94 + expect(queue[1]?.goal).toBe(validPlanInput.goal); 95 + expect(queue[1]?.queuePosition).toBe(2); 96 + }); 97 + 98 + it("should return the created plan", () => { 99 + const plan = store.addPlan(validPlanInput); 100 + 101 + expect(plan.id).toBe("test01"); 102 + expect(plan.goal).toBe(validPlanInput.goal); 103 + expect(plan.status).toBe("queued"); 104 + expect(plan.queuePosition).toBe(1); 105 + }); 106 + 107 + it("should handle minimal input without optional fields", () => { 108 + const plan = store.addPlan(minimalPlanInput); 109 + 110 + expect(plan.id).toBe("test01"); 111 + expect(plan.inputs).toBeNull(); 112 + expect(plan.outputs).toBeNull(); 113 + }); 114 + 115 + it("should store inputs and outputs when provided", () => { 116 + const plan = store.addPlan(validPlanInput); 117 + 118 + expect(plan.inputs).toBe(validPlanInput.inputs); 119 + expect(plan.outputs).toBe(validPlanInput.outputs); 120 + }); 121 + }); 122 + 123 + // =========================================================================== 124 + // pullPlan Tests 125 + // =========================================================================== 126 + 127 + describe("pullPlan", () => { 128 + it("should return front plan from queue", () => { 129 + store.addPlan(validPlanInput); 130 + store.addPlan({ ...validPlanInput, goal: "Second plan" }); 131 + 132 + const plan = store.pullPlan(); 133 + 134 + expect(plan.goal).toBe(validPlanInput.goal); 135 + }); 136 + 137 + it("should mark plan as active", () => { 138 + store.addPlan(validPlanInput); 139 + 140 + const plan = store.pullPlan(); 141 + 142 + expect(plan.status).toBe("active"); 143 + expect(plan.queuePosition).toBeNull(); 144 + }); 145 + 146 + it("should renumber queue after pull", () => { 147 + store.addPlan(validPlanInput); 148 + store.addPlan({ ...validPlanInput, goal: "Second plan" }); 149 + 150 + store.pullPlan(); 151 + 152 + const queue = store.getQueue(); 153 + expect(queue).toHaveLength(1); 154 + expect(queue[0]?.queuePosition).toBe(1); 155 + }); 156 + 157 + it("should throw PLAN_ALREADY_ACTIVE when plan already active", () => { 158 + store.addPlan(validPlanInput); 159 + store.pullPlan(); 160 + 161 + store.addPlan({ ...validPlanInput, goal: "Another plan" }); 162 + 163 + expect(() => { store.pullPlan(); }).toThrow(NinePlanError); 164 + try { 165 + store.pullPlan(); 166 + } catch (error) { 167 + expect(error).toBeInstanceOf(NinePlanError); 168 + expect((error as NinePlanError).category).toBe("PLAN_ALREADY_ACTIVE"); 169 + } 170 + }); 171 + 172 + it("should throw QUEUE_EMPTY when queue is empty", () => { 173 + expect(() => { store.pullPlan(); }).toThrow(NinePlanError); 174 + try { 175 + store.pullPlan(); 176 + } catch (error) { 177 + expect(error).toBeInstanceOf(NinePlanError); 178 + expect((error as NinePlanError).category).toBe("QUEUE_EMPTY"); 179 + } 180 + }); 181 + }); 182 + 183 + // =========================================================================== 184 + // completePlan Tests 185 + // =========================================================================== 186 + 187 + describe("completePlan", () => { 188 + it("should store outcome in database", () => { 189 + store.addPlan(validPlanInput); 190 + store.pullPlan(); 191 + 192 + store.completePlan("Task completed successfully"); 193 + 194 + const completed = store.getHistoryPlan("test01"); 195 + expect(completed?.outcome).toBe("Task completed successfully"); 196 + }); 197 + 198 + it("should mark plan as completed", () => { 199 + store.addPlan(validPlanInput); 200 + store.pullPlan(); 201 + 202 + store.completePlan("Done"); 203 + 204 + const completed = store.getHistoryPlan("test01"); 205 + expect(completed?.status).toBe("completed"); 206 + }); 207 + 208 + it("should delete plan file", () => { 209 + store.addPlan(validPlanInput); 210 + store.pullPlan(); 211 + 212 + store.completePlan("Done"); 213 + 214 + // eslint-disable-next-line @typescript-eslint/unbound-method 215 + expect(mockPlanFiles.delete).toHaveBeenCalledWith("test01"); 216 + }); 217 + 218 + it("should clear active plan reference", () => { 219 + store.addPlan(validPlanInput); 220 + store.pullPlan(); 221 + 222 + store.completePlan("Done"); 223 + 224 + expect(store.getActivePlan()).toBeNull(); 225 + }); 226 + 227 + it("should throw NO_ACTIVE_PLAN when no active plan", () => { 228 + expect(() => { store.completePlan("Done"); }).toThrow(NinePlanError); 229 + try { 230 + store.completePlan("Done"); 231 + } catch (error) { 232 + expect(error).toBeInstanceOf(NinePlanError); 233 + expect((error as NinePlanError).category).toBe("NO_ACTIVE_PLAN"); 234 + } 235 + }); 236 + 237 + it("should set completed_at timestamp", () => { 238 + store.addPlan(validPlanInput); 239 + store.pullPlan(); 240 + 241 + store.completePlan("Done"); 242 + 243 + const completed = store.getHistoryPlan("test01"); 244 + expect(completed?.completedAt).not.toBeNull(); 245 + }); 246 + }); 247 + 248 + // =========================================================================== 249 + // deferPlan Tests 250 + // =========================================================================== 251 + 252 + describe("deferPlan", () => { 253 + it("should append reason to Notes with timestamp", () => { 254 + store.addPlan(validPlanInput); 255 + store.pullPlan(); 256 + 257 + store.deferPlan("Need more information"); 258 + 259 + // eslint-disable-next-line @typescript-eslint/unbound-method 260 + expect(mockPlanFiles.appendToNotes).toHaveBeenCalledWith( 261 + "test01", 262 + "Deferred: Need more information", 263 + ); 264 + }); 265 + 266 + it("should return plan to front of queue when position is front", () => { 267 + store.addPlan(validPlanInput); 268 + store.addPlan({ ...validPlanInput, goal: "Second plan" }); 269 + store.pullPlan(); 270 + 271 + store.deferPlan("Blocked", "front"); 272 + 273 + const queue = store.getQueue(); 274 + expect(queue[0]?.goal).toBe(validPlanInput.goal); 275 + }); 276 + 277 + it("should return plan to back of queue when position is back", () => { 278 + store.addPlan(validPlanInput); 279 + store.addPlan({ ...validPlanInput, goal: "Second plan" }); 280 + store.pullPlan(); 281 + 282 + store.deferPlan("Do later", "back"); 283 + 284 + const queue = store.getQueue(); 285 + expect(queue[0]?.goal).toBe("Second plan"); 286 + expect(queue[1]?.goal).toBe(validPlanInput.goal); 287 + }); 288 + 289 + it("should default position to back", () => { 290 + store.addPlan(validPlanInput); 291 + store.addPlan({ ...validPlanInput, goal: "Second plan" }); 292 + store.pullPlan(); 293 + 294 + store.deferPlan("Do later"); 295 + 296 + const queue = store.getQueue(); 297 + expect(queue[1]?.goal).toBe(validPlanInput.goal); 298 + }); 299 + 300 + it("should throw NO_ACTIVE_PLAN when no active plan", () => { 301 + expect(() => { store.deferPlan("reason"); }).toThrow(NinePlanError); 302 + try { 303 + store.deferPlan("reason"); 304 + } catch (error) { 305 + expect(error).toBeInstanceOf(NinePlanError); 306 + expect((error as NinePlanError).category).toBe("NO_ACTIVE_PLAN"); 307 + } 308 + }); 309 + 310 + it("should mark plan as queued after deferral", () => { 311 + store.addPlan(validPlanInput); 312 + store.pullPlan(); 313 + 314 + store.deferPlan("Need info"); 315 + 316 + const queue = store.getQueue(); 317 + expect(queue[0]?.id).toBe("test01"); 318 + }); 319 + }); 320 + 321 + // =========================================================================== 322 + // discardPlan Tests 323 + // =========================================================================== 324 + 325 + describe("discardPlan", () => { 326 + it("should mark plan as discarded", () => { 327 + store.addPlan(validPlanInput); 328 + store.pullPlan(); 329 + 330 + store.discardPlan("No longer needed"); 331 + 332 + // Discarded plans don't appear in queue or history 333 + expect(store.getQueue()).toHaveLength(0); 334 + expect(store.getHistoryPlan("test01")).toBeNull(); 335 + }); 336 + 337 + it("should delete plan file", () => { 338 + store.addPlan(validPlanInput); 339 + store.pullPlan(); 340 + 341 + store.discardPlan("Cancelled"); 342 + 343 + // eslint-disable-next-line @typescript-eslint/unbound-method 344 + expect(mockPlanFiles.delete).toHaveBeenCalledWith("test01"); 345 + }); 346 + 347 + it("should NOT add to searchable history", () => { 348 + store.addPlan(validPlanInput); 349 + store.pullPlan(); 350 + 351 + store.discardPlan("Cancelled"); 352 + 353 + // Should not appear in history search 354 + const results = store.searchHistory(validPlanInput.goal); 355 + expect(results).toHaveLength(0); 356 + }); 357 + 358 + it("should clear active plan reference", () => { 359 + store.addPlan(validPlanInput); 360 + store.pullPlan(); 361 + 362 + store.discardPlan("Cancelled"); 363 + 364 + expect(store.getActivePlan()).toBeNull(); 365 + }); 366 + 367 + it("should throw NO_ACTIVE_PLAN when no active plan", () => { 368 + expect(() => { store.discardPlan("reason"); }).toThrow(NinePlanError); 369 + try { 370 + store.discardPlan("reason"); 371 + } catch (error) { 372 + expect(error).toBeInstanceOf(NinePlanError); 373 + expect((error as NinePlanError).category).toBe("NO_ACTIVE_PLAN"); 374 + } 375 + }); 376 + }); 377 + 378 + // =========================================================================== 379 + // searchHistory Tests 380 + // =========================================================================== 381 + 382 + describe("searchHistory", () => { 383 + beforeEach(() => { 384 + // Add and complete a few plans for search tests 385 + store.addPlan(validPlanInput); 386 + store.pullPlan(); 387 + store.completePlan("Verification complete"); 388 + 389 + store.addPlan({ 390 + ...validPlanInput, 391 + goal: "Implement authentication module", 392 + }); 393 + store.pullPlan(); 394 + store.completePlan("Auth module ready"); 395 + 396 + store.addPlan({ 397 + ...validPlanInput, 398 + goal: "Build user dashboard", 399 + }); 400 + store.pullPlan(); 401 + store.completePlan("Dashboard built with charts"); 402 + }); 403 + 404 + it("should return matching completed plans", () => { 405 + const results = store.searchHistory("authentication"); 406 + 407 + expect(results.length).toBeGreaterThan(0); 408 + expect(results.some((r) => r.goal.includes("authentication"))).toBe(true); 409 + }); 410 + 411 + it("should return empty array when no matches", () => { 412 + const results = store.searchHistory("nonexistentxyzabc"); 413 + 414 + expect(results).toHaveLength(0); 415 + }); 416 + 417 + it("should respect max_results limit", () => { 418 + // Use a broad search term that matches multiple completed plans 419 + const results = store.searchHistory("module OR dashboard", 2); 420 + 421 + expect(results.length).toBeLessThanOrEqual(2); 422 + }); 423 + 424 + it("should include outcome in results", () => { 425 + const results = store.searchHistory("authentication"); 426 + 427 + expect(results.some((r) => r.outcome !== null)).toBe(true); 428 + }); 429 + 430 + it("should include relevance score", () => { 431 + const results = store.searchHistory("authentication"); 432 + 433 + expect(results[0]?.relevanceScore).toBeDefined(); 434 + expect(typeof results[0]?.relevanceScore).toBe("number"); 435 + }); 436 + }); 437 + 438 + // =========================================================================== 439 + // getHistoryPlan Tests 440 + // =========================================================================== 441 + 442 + describe("getHistoryPlan", () => { 443 + it("should return full plan content", () => { 444 + store.addPlan(validPlanInput); 445 + store.pullPlan(); 446 + store.completePlan("All done"); 447 + 448 + const plan = store.getHistoryPlan("test01"); 449 + 450 + expect(plan?.id).toBe("test01"); 451 + expect(plan?.goal).toBe(validPlanInput.goal); 452 + expect(plan?.outcome).toBe("All done"); 453 + expect(plan?.status).toBe("completed"); 454 + }); 455 + 456 + it("should return null when plan ID not found", () => { 457 + const plan = store.getHistoryPlan("nonexistent"); 458 + 459 + expect(plan).toBeNull(); 460 + }); 461 + 462 + it("should return null for queued plans", () => { 463 + store.addPlan(validPlanInput); 464 + 465 + const plan = store.getHistoryPlan("test01"); 466 + 467 + expect(plan).toBeNull(); 468 + }); 469 + 470 + it("should return null for active plans", () => { 471 + store.addPlan(validPlanInput); 472 + store.pullPlan(); 473 + 474 + const plan = store.getHistoryPlan("test01"); 475 + 476 + expect(plan).toBeNull(); 477 + }); 478 + 479 + it("should return null for discarded plans", () => { 480 + store.addPlan(validPlanInput); 481 + store.pullPlan(); 482 + store.discardPlan("Cancelled"); 483 + 484 + const plan = store.getHistoryPlan("test01"); 485 + 486 + expect(plan).toBeNull(); 487 + }); 488 + }); 489 + 490 + // =========================================================================== 491 + // State Queries Tests 492 + // =========================================================================== 493 + 494 + describe("getState", () => { 495 + it("should return full session state", () => { 496 + store.addPlan(validPlanInput); 497 + 498 + const state = store.getState(); 499 + 500 + expect(state.sessionName).toBe("test-session"); 501 + expect(state.taskDescription).toBe("Test task description"); 502 + expect(state.queue).toHaveLength(1); 503 + expect(state.activePlan).toBeNull(); 504 + expect(state.completedCount).toBe(0); 505 + }); 506 + 507 + it("should include active plan when present", () => { 508 + store.addPlan(validPlanInput); 509 + store.pullPlan(); 510 + 511 + const state = store.getState(); 512 + 513 + expect(state.activePlan).not.toBeNull(); 514 + expect(state.activePlan?.id).toBe("test01"); 515 + expect(state.activePlan?.goal).toBe(validPlanInput.goal); 516 + }); 517 + 518 + it("should track completed count", () => { 519 + store.addPlan(validPlanInput); 520 + store.pullPlan(); 521 + store.completePlan("Done"); 522 + 523 + store.addPlan({ ...validPlanInput, goal: "Second task" }); 524 + store.pullPlan(); 525 + store.completePlan("Also done"); 526 + 527 + const state = store.getState(); 528 + 529 + expect(state.completedCount).toBe(2); 530 + }); 531 + }); 532 + 533 + // =========================================================================== 534 + // Invariant Tests 535 + // =========================================================================== 536 + 537 + describe("invariants", () => { 538 + it("should only allow one active plan at a time", () => { 539 + store.addPlan(validPlanInput); 540 + store.addPlan({ ...validPlanInput, goal: "Second" }); 541 + 542 + store.pullPlan(); 543 + 544 + expect(() => { store.pullPlan(); }).toThrow(NinePlanError); 545 + }); 546 + 547 + it("should preserve queue order across operations", () => { 548 + store.addPlan({ ...validPlanInput, goal: "First" }); 549 + store.addPlan({ ...validPlanInput, goal: "Second" }); 550 + store.addPlan({ ...validPlanInput, goal: "Third" }); 551 + 552 + // Pull and complete first 553 + store.pullPlan(); 554 + store.completePlan("Done"); 555 + 556 + // Add new one at front 557 + store.addPlan({ ...validPlanInput, goal: "Urgent" }, "front"); 558 + 559 + const queue = store.getQueue(); 560 + expect(queue[0]?.goal).toBe("Urgent"); 561 + expect(queue[1]?.goal).toBe("Second"); 562 + expect(queue[2]?.goal).toBe("Third"); 563 + }); 564 + 565 + it("should generate unique plan IDs", () => { 566 + store.addPlan(validPlanInput); 567 + store.addPlan({ ...validPlanInput, goal: "Second" }); 568 + store.addPlan({ ...validPlanInput, goal: "Third" }); 569 + 570 + expect(mockGeneratePlanId).toHaveBeenCalledTimes(3); 571 + }); 572 + }); 573 + });
+287
tests/unit/files/plan-files.test.ts
··· 1 + /** 2 + * Tests for PlanFileHandler 3 + * 4 + * Uses real filesystem with temp directories to test actual file operations. 5 + */ 6 + 7 + import { describe, it, expect, beforeEach, afterEach } from "vitest"; 8 + import { existsSync, readFileSync } from "node:fs"; 9 + import { join } from "node:path"; 10 + import { PlanFileHandlerImpl } from "../../../src/files/plan-files.js"; 11 + import { createMockLogger } from "../../helpers/mocks.js"; 12 + import { createTempSessionDir } from "../../helpers/temp.js"; 13 + import { validPlanContent, createPlanContent } from "../../fixtures/plans.js"; 14 + 15 + describe("PlanFileHandler", () => { 16 + let handler: PlanFileHandlerImpl; 17 + let plansDir: string; 18 + let cleanup: () => void; 19 + 20 + beforeEach(() => { 21 + const temp = createTempSessionDir(); 22 + cleanup = temp.cleanup; 23 + plansDir = temp.plansDir; 24 + 25 + handler = new PlanFileHandlerImpl(temp.sessionPath, { 26 + logger: createMockLogger(), 27 + }); 28 + }); 29 + 30 + afterEach(() => { 31 + cleanup(); 32 + }); 33 + 34 + // =========================================================================== 35 + // write Tests 36 + // =========================================================================== 37 + 38 + describe("write", () => { 39 + it("should write plan file with correct format", () => { 40 + handler.write("test01", validPlanContent); 41 + 42 + const filePath = join(plansDir, "test01.txt"); 43 + const content = readFileSync(filePath, "utf-8"); 44 + 45 + expect(content).toContain("# Context"); 46 + expect(content).toContain(validPlanContent.context); 47 + expect(content).toContain("# Goal"); 48 + expect(content).toContain(validPlanContent.goal); 49 + expect(content).toContain("# Inputs"); 50 + expect(content).toContain("# Outputs"); 51 + expect(content).toContain("# Approach"); 52 + expect(content).toContain("# Success Criteria"); 53 + expect(content).toContain("# Notes"); 54 + }); 55 + 56 + it("should create file in plans directory", () => { 57 + handler.write("test01", validPlanContent); 58 + 59 + const filePath = join(plansDir, "test01.txt"); 60 + expect(existsSync(filePath)).toBe(true); 61 + }); 62 + 63 + it("should overwrite existing file", () => { 64 + handler.write("test01", validPlanContent); 65 + handler.write("test01", createPlanContent({ goal: "Updated goal" })); 66 + 67 + const content = handler.read("test01"); 68 + expect(content.goal).toBe("Updated goal"); 69 + }); 70 + 71 + it("should handle empty optional fields", () => { 72 + const contentWithEmpty = createPlanContent({ 73 + inputs: "", 74 + outputs: "", 75 + notes: "", 76 + }); 77 + 78 + handler.write("test01", contentWithEmpty); 79 + 80 + const filePath = join(plansDir, "test01.txt"); 81 + const fileContent = readFileSync(filePath, "utf-8"); 82 + 83 + expect(fileContent).toContain("# Inputs\n(none)"); 84 + expect(fileContent).toContain("# Outputs\n(none)"); 85 + expect(fileContent).toContain("# Notes\n(none)"); 86 + }); 87 + }); 88 + 89 + // =========================================================================== 90 + // read Tests 91 + // =========================================================================== 92 + 93 + describe("read", () => { 94 + it("should read plan file and parse sections", () => { 95 + handler.write("test01", validPlanContent); 96 + 97 + const content = handler.read("test01"); 98 + 99 + expect(content.context).toBe(validPlanContent.context); 100 + expect(content.goal).toBe(validPlanContent.goal); 101 + expect(content.inputs).toBe(validPlanContent.inputs); 102 + expect(content.outputs).toBe(validPlanContent.outputs); 103 + expect(content.approach).toBe(validPlanContent.approach); 104 + expect(content.successCriteria).toBe(validPlanContent.successCriteria); 105 + }); 106 + 107 + it("should parse empty optional fields as empty strings", () => { 108 + const contentWithEmpty = createPlanContent({ 109 + inputs: "", 110 + outputs: "", 111 + notes: "", 112 + }); 113 + handler.write("test01", contentWithEmpty); 114 + 115 + const content = handler.read("test01"); 116 + 117 + expect(content.inputs).toBe(""); 118 + expect(content.outputs).toBe(""); 119 + expect(content.notes).toBe(""); 120 + }); 121 + 122 + it("should throw when file does not exist", () => { 123 + expect(() => handler.read("nonexistent")).toThrow(); 124 + }); 125 + }); 126 + 127 + // =========================================================================== 128 + // delete Tests 129 + // =========================================================================== 130 + 131 + describe("delete", () => { 132 + it("should delete plan file", () => { 133 + handler.write("test01", validPlanContent); 134 + 135 + handler.delete("test01"); 136 + 137 + const filePath = join(plansDir, "test01.txt"); 138 + expect(existsSync(filePath)).toBe(false); 139 + }); 140 + 141 + it("should not throw when file does not exist", () => { 142 + expect(() => { handler.delete("nonexistent"); }).not.toThrow(); 143 + }); 144 + }); 145 + 146 + // =========================================================================== 147 + // exists Tests 148 + // =========================================================================== 149 + 150 + describe("exists", () => { 151 + it("should return true when file exists", () => { 152 + handler.write("test01", validPlanContent); 153 + 154 + expect(handler.exists("test01")).toBe(true); 155 + }); 156 + 157 + it("should return false when file does not exist", () => { 158 + expect(handler.exists("nonexistent")).toBe(false); 159 + }); 160 + }); 161 + 162 + // =========================================================================== 163 + // appendToNotes Tests 164 + // =========================================================================== 165 + 166 + describe("appendToNotes", () => { 167 + it("should append to Notes section", () => { 168 + handler.write("test01", createPlanContent({ notes: "" })); 169 + 170 + handler.appendToNotes("test01", "First note"); 171 + 172 + const content = handler.read("test01"); 173 + expect(content.notes).toContain("First note"); 174 + }); 175 + 176 + it("should include timestamp", () => { 177 + handler.write("test01", createPlanContent({ notes: "" })); 178 + 179 + handler.appendToNotes("test01", "Timestamped note"); 180 + 181 + const content = handler.read("test01"); 182 + // Should have ISO-ish timestamp format [YYYY-MM-DD HH:MM:SS] 183 + expect(content.notes).toMatch(/\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]/); 184 + }); 185 + 186 + it("should accumulate multiple notes", () => { 187 + handler.write("test01", createPlanContent({ notes: "" })); 188 + 189 + handler.appendToNotes("test01", "First note"); 190 + handler.appendToNotes("test01", "Second note"); 191 + handler.appendToNotes("test01", "Third note"); 192 + 193 + const content = handler.read("test01"); 194 + expect(content.notes).toContain("First note"); 195 + expect(content.notes).toContain("Second note"); 196 + expect(content.notes).toContain("Third note"); 197 + }); 198 + 199 + it("should preserve existing notes", () => { 200 + handler.write("test01", createPlanContent({ notes: "Initial notes" })); 201 + 202 + handler.appendToNotes("test01", "New note"); 203 + 204 + const content = handler.read("test01"); 205 + expect(content.notes).toContain("Initial notes"); 206 + expect(content.notes).toContain("New note"); 207 + }); 208 + }); 209 + 210 + // =========================================================================== 211 + // getFilePath Tests 212 + // =========================================================================== 213 + 214 + describe("getFilePath", () => { 215 + it("should return correct file path", () => { 216 + const filePath = handler.getFilePath("test01"); 217 + 218 + expect(filePath).toBe(join(plansDir, "test01.txt")); 219 + }); 220 + }); 221 + 222 + // =========================================================================== 223 + // Edge Cases 224 + // =========================================================================== 225 + 226 + describe("edge cases", () => { 227 + it("should handle special characters in content", () => { 228 + const contentWithSpecial = createPlanContent({ 229 + context: "Context with \"quotes\" and 'apostrophes'", 230 + goal: "Goal with <angle> brackets & ampersand", 231 + notes: "Notes with newlines\nand\ttabs", 232 + }); 233 + 234 + handler.write("test01", contentWithSpecial); 235 + const content = handler.read("test01"); 236 + 237 + expect(content.context).toBe(contentWithSpecial.context); 238 + expect(content.goal).toBe(contentWithSpecial.goal); 239 + }); 240 + 241 + it("should handle unicode content", () => { 242 + const unicodeContent = createPlanContent({ 243 + context: "Context with emoji: \u{1F680}\u{1F31F}\u{2728}", 244 + goal: "\u65E5\u672C\u8A9E\u306E\u30C6\u30AD\u30B9\u30C8", 245 + notes: "\u4E2D\u6587\u5185\u5BB9", 246 + }); 247 + 248 + handler.write("unicode01", unicodeContent); 249 + const content = handler.read("unicode01"); 250 + 251 + expect(content.context).toBe(unicodeContent.context); 252 + expect(content.goal).toBe(unicodeContent.goal); 253 + expect(content.notes).toBe(unicodeContent.notes); 254 + }); 255 + 256 + it("should handle very long content", () => { 257 + // Note: Parser trims whitespace, so don't use trailing spaces 258 + const longContent = createPlanContent({ 259 + context: "A".repeat(10000), 260 + approach: "Step by step process. ".repeat(500).trim(), 261 + }); 262 + 263 + handler.write("long01", longContent); 264 + const content = handler.read("long01"); 265 + 266 + expect(content.context).toBe(longContent.context); 267 + expect(content.approach).toBe(longContent.approach); 268 + }); 269 + 270 + it("should handle multiline content in all fields", () => { 271 + const multilineContent = createPlanContent({ 272 + context: "Line 1\nLine 2\nLine 3", 273 + goal: "Goal line 1\nGoal line 2", 274 + approach: "1. First step\n2. Second step\n3. Third step", 275 + successCriteria: "Criterion 1\nCriterion 2", 276 + }); 277 + 278 + handler.write("multi01", multilineContent); 279 + const content = handler.read("multi01"); 280 + 281 + expect(content.context).toBe(multilineContent.context); 282 + expect(content.goal).toBe(multilineContent.goal); 283 + expect(content.approach).toBe(multilineContent.approach); 284 + expect(content.successCriteria).toBe(multilineContent.successCriteria); 285 + }); 286 + }); 287 + });
+43
tests/unit/generators/plan-id.test.ts
··· 1 + /** 2 + * Tests for plan ID generator 3 + */ 4 + 5 + import { describe, it, expect } from "vitest"; 6 + import { generatePlanId } from "../../../src/generators/plan-id.js"; 7 + 8 + describe("generatePlanId", () => { 9 + it("should return 5-character alphanumeric ID", () => { 10 + const id = generatePlanId(); 11 + 12 + expect(id).toHaveLength(5); 13 + expect(id).toMatch(/^[0-9a-z]+$/); 14 + }); 15 + 16 + it("should generate unique IDs", () => { 17 + const ids = new Set<string>(); 18 + const count = 500; // Reduced count to minimize collision chance 19 + 20 + for (let i = 0; i < count; i++) { 21 + ids.add(generatePlanId()); 22 + } 23 + 24 + // With 500 IDs from a 36^5 (~60M) space, collisions are extremely unlikely 25 + // Allow up to 2 collisions to account for rare statistical events 26 + expect(ids.size).toBeGreaterThanOrEqual(count - 2); 27 + }); 28 + 29 + it("should not contain uppercase letters", () => { 30 + // Generate several IDs to increase confidence 31 + for (let i = 0; i < 100; i++) { 32 + const id = generatePlanId(); 33 + expect(id).not.toMatch(/[A-Z]/); 34 + } 35 + }); 36 + 37 + it("should only contain alphanumeric characters", () => { 38 + for (let i = 0; i < 100; i++) { 39 + const id = generatePlanId(); 40 + expect(id).not.toMatch(/[^0-9a-z]/); 41 + } 42 + }); 43 + });
+42
tests/unit/generators/session-name.test.ts
··· 1 + /** 2 + * Tests for session name generator 3 + */ 4 + 5 + import { describe, it, expect } from "vitest"; 6 + import { generateSessionName } from "../../../src/generators/session-name.js"; 7 + 8 + describe("generateSessionName", () => { 9 + it("should return three-word hyphenated name", () => { 10 + const name = generateSessionName(); 11 + const parts = name.split("-"); 12 + 13 + expect(parts).toHaveLength(3); 14 + expect(parts.every((part) => part.length > 0)).toBe(true); 15 + }); 16 + 17 + it("should use lowercase letters only", () => { 18 + const name = generateSessionName(); 19 + 20 + expect(name).toMatch(/^[a-z]+-[a-z]+-[a-z]+$/); 21 + }); 22 + 23 + it("should generate unique names", () => { 24 + const names = new Set<string>(); 25 + const count = 100; 26 + 27 + for (let i = 0; i < count; i++) { 28 + names.add(generateSessionName()); 29 + } 30 + 31 + // With 100 names from large dictionaries, we should have no collisions 32 + expect(names.size).toBe(count); 33 + }); 34 + 35 + it("should generate different names on each call", () => { 36 + const name1 = generateSessionName(); 37 + const name2 = generateSessionName(); 38 + 39 + // While theoretically possible to get the same name, it's extremely unlikely 40 + expect(name1).not.toBe(name2); 41 + }); 42 + });
+126
tests/unit/tools/history-get.test.ts
··· 1 + /** 2 + * Tests for 9plan_history_get tool 3 + */ 4 + 5 + import { describe, it, expect, vi, afterEach } from "vitest"; 6 + import { handleHistoryGet } from "../../../src/tools/history-get.js"; 7 + import { createMockSessionStore, createMockPlan } from "../../helpers/mocks.js"; 8 + 9 + describe("handleHistoryGet", () => { 10 + const mockStore = createMockSessionStore(); 11 + 12 + afterEach(() => { 13 + vi.clearAllMocks(); 14 + }); 15 + 16 + it("should get plan and return formatted response", () => { 17 + mockStore.getHistoryPlan.mockReturnValue( 18 + createMockPlan({ 19 + id: "abc12", 20 + goal: "Build authentication", 21 + context: "Part of user system", 22 + approach: "Use JWT tokens", 23 + successCriteria: "Users can log in", 24 + inputs: "- db_schema: from database work", 25 + outputs: "- auth_module: JWT implementation", 26 + outcome: "Successfully implemented auth with refresh tokens", 27 + completedAt: "2024-01-15T10:30:00Z", 28 + }), 29 + ); 30 + 31 + const result = handleHistoryGet(mockStore, "test-session", { 32 + plan_id: "abc12", 33 + }); 34 + 35 + expect(mockStore.getHistoryPlan).toHaveBeenCalledWith("abc12"); 36 + expect(result.content).toHaveLength(1); 37 + expect(result.content[0]!.type).toBe("text"); 38 + expect(result.content[0]!.text).toContain("[Session: test-session]"); 39 + }); 40 + 41 + it("should include all plan fields in response", () => { 42 + mockStore.getHistoryPlan.mockReturnValue( 43 + createMockPlan({ 44 + id: "abc12", 45 + goal: "Test goal", 46 + context: "Test context", 47 + approach: "Test approach", 48 + successCriteria: "Test criteria", 49 + inputs: "Test inputs", 50 + outputs: "Test outputs", 51 + outcome: "Test outcome", 52 + completedAt: "2024-01-15T10:30:00Z", 53 + }), 54 + ); 55 + 56 + const result = handleHistoryGet(mockStore, "test-session", { 57 + plan_id: "abc12", 58 + }); 59 + 60 + const text = result.content[0]!.text; 61 + expect(text).toContain("Test goal"); 62 + expect(text).toContain("Test context"); 63 + expect(text).toContain("Test outcome"); 64 + }); 65 + 66 + it("should include outcome in response", () => { 67 + mockStore.getHistoryPlan.mockReturnValue( 68 + createMockPlan({ 69 + id: "abc12", 70 + goal: "Build feature", 71 + outcome: "Feature built successfully with all tests passing", 72 + completedAt: "2024-01-15T10:30:00Z", 73 + }), 74 + ); 75 + 76 + const result = handleHistoryGet(mockStore, "test-session", { 77 + plan_id: "abc12", 78 + }); 79 + 80 + expect(result.content[0]!.text).toContain("Feature built successfully"); 81 + }); 82 + 83 + it("should handle plan not found", () => { 84 + mockStore.getHistoryPlan.mockReturnValue(null); 85 + 86 + const result = handleHistoryGet(mockStore, "test-session", { 87 + plan_id: "nonexistent", 88 + }); 89 + 90 + expect(result.content[0]!.text).toMatch(/not found|doesn't exist/i); 91 + }); 92 + 93 + it("should show plan ID in response", () => { 94 + mockStore.getHistoryPlan.mockReturnValue( 95 + createMockPlan({ 96 + id: "xyz99", 97 + goal: "Some task", 98 + outcome: "Done", 99 + completedAt: "2024-01-15T10:30:00Z", 100 + }), 101 + ); 102 + 103 + const result = handleHistoryGet(mockStore, "test-session", { 104 + plan_id: "xyz99", 105 + }); 106 + 107 + expect(result.content[0]!.text).toContain("xyz99"); 108 + }); 109 + 110 + it("should indicate plan is completed in response", () => { 111 + mockStore.getHistoryPlan.mockReturnValue( 112 + createMockPlan({ 113 + id: "abc12", 114 + goal: "Task", 115 + outcome: "Done", 116 + completedAt: "2024-01-15T10:30:00Z", 117 + }), 118 + ); 119 + 120 + const result = handleHistoryGet(mockStore, "test-session", { 121 + plan_id: "abc12", 122 + }); 123 + 124 + expect(result.content[0]!.text).toContain("completed"); 125 + }); 126 + });
+121
tests/unit/tools/history-search.test.ts
··· 1 + /** 2 + * Tests for 9plan_history_search tool 3 + */ 4 + 5 + import { describe, it, expect, vi, afterEach } from "vitest"; 6 + import { handleHistorySearch } from "../../../src/tools/history-search.js"; 7 + import { createMockSessionStore } from "../../helpers/mocks.js"; 8 + 9 + describe("handleHistorySearch", () => { 10 + const mockStore = createMockSessionStore(); 11 + 12 + afterEach(() => { 13 + vi.clearAllMocks(); 14 + }); 15 + 16 + it("should search history and return formatted response", () => { 17 + mockStore.searchHistory.mockReturnValue([ 18 + { 19 + id: "abc12", 20 + goal: "Auth module", 21 + outcome: "Implemented JWT auth", 22 + relevanceScore: 5.2, 23 + }, 24 + { 25 + id: "def34", 26 + goal: "API client", 27 + outcome: "Built REST client", 28 + relevanceScore: 3.1, 29 + }, 30 + ]); 31 + 32 + const result = handleHistorySearch(mockStore, "test-session", { 33 + query: "authentication", 34 + }); 35 + 36 + expect(mockStore.searchHistory).toHaveBeenCalledWith("authentication", 10); 37 + expect(result.content).toHaveLength(1); 38 + expect(result.content[0]!.type).toBe("text"); 39 + expect(result.content[0]!.text).toContain("[Session: test-session]"); 40 + }); 41 + 42 + it("should include matching plans in response", () => { 43 + mockStore.searchHistory.mockReturnValue([ 44 + { 45 + id: "abc12", 46 + goal: "Auth module", 47 + outcome: "Implemented JWT", 48 + relevanceScore: 5.0, 49 + }, 50 + ]); 51 + 52 + const result = handleHistorySearch(mockStore, "test-session", { 53 + query: "auth", 54 + }); 55 + 56 + expect(result.content[0]!.text).toContain("abc12"); 57 + expect(result.content[0]!.text).toContain("Auth module"); 58 + }); 59 + 60 + it("should respect max_results parameter", () => { 61 + mockStore.searchHistory.mockReturnValue([]); 62 + 63 + handleHistorySearch(mockStore, "test-session", { 64 + query: "test", 65 + max_results: 5, 66 + }); 67 + 68 + expect(mockStore.searchHistory).toHaveBeenCalledWith("test", 5); 69 + }); 70 + 71 + it("should default max_results to 10", () => { 72 + mockStore.searchHistory.mockReturnValue([]); 73 + 74 + handleHistorySearch(mockStore, "test-session", { 75 + query: "test", 76 + }); 77 + 78 + expect(mockStore.searchHistory).toHaveBeenCalledWith("test", 10); 79 + }); 80 + 81 + it("should handle no results gracefully", () => { 82 + mockStore.searchHistory.mockReturnValue([]); 83 + 84 + const result = handleHistorySearch(mockStore, "test-session", { 85 + query: "nonexistent", 86 + }); 87 + 88 + expect(result.content[0]!.text).toMatch(/no.*match|0.*found|empty/i); 89 + }); 90 + 91 + it("should show result count", () => { 92 + mockStore.searchHistory.mockReturnValue([ 93 + { id: "abc12", goal: "Task 1", outcome: "Done 1", relevanceScore: 5.0 }, 94 + { id: "def34", goal: "Task 2", outcome: "Done 2", relevanceScore: 4.0 }, 95 + { id: "ghi56", goal: "Task 3", outcome: "Done 3", relevanceScore: 3.0 }, 96 + ]); 97 + 98 + const result = handleHistorySearch(mockStore, "test-session", { 99 + query: "task", 100 + }); 101 + 102 + expect(result.content[0]!.text).toContain("3"); 103 + }); 104 + 105 + it("should include outcome snippets in results", () => { 106 + mockStore.searchHistory.mockReturnValue([ 107 + { 108 + id: "abc12", 109 + goal: "Build auth", 110 + outcome: "Implemented JWT with refresh tokens", 111 + relevanceScore: 5.0, 112 + }, 113 + ]); 114 + 115 + const result = handleHistorySearch(mockStore, "test-session", { 116 + query: "auth", 117 + }); 118 + 119 + expect(result.content[0]!.text).toContain("JWT"); 120 + }); 121 + });
+97
tests/unit/tools/plan-complete.test.ts
··· 1 + /** 2 + * Tests for 9plan_plan_complete tool 3 + */ 4 + 5 + import { describe, it, expect, vi, afterEach } from "vitest"; 6 + import { handlePlanComplete } from "../../../src/tools/plan-complete.js"; 7 + import { createMockSessionStore, createMockPlan } from "../../helpers/mocks.js"; 8 + import { NinePlanError } from "../../../src/types.js"; 9 + 10 + describe("handlePlanComplete", () => { 11 + const mockStore = createMockSessionStore(); 12 + 13 + afterEach(() => { 14 + vi.clearAllMocks(); 15 + }); 16 + 17 + it("should complete plan and return formatted response", () => { 18 + mockStore.getActivePlan.mockReturnValue( 19 + createMockPlan({ id: "abc12", goal: "Test goal" }), 20 + ); 21 + mockStore.getQueue.mockReturnValue([]); 22 + mockStore.getCompletedCount.mockReturnValue(5); 23 + 24 + const result = handlePlanComplete(mockStore, "test-session", { 25 + outcome: "Successfully implemented the feature", 26 + }); 27 + 28 + expect(mockStore.completePlan).toHaveBeenCalledWith( 29 + "Successfully implemented the feature", 30 + ); 31 + expect(result.content).toHaveLength(1); 32 + expect(result.content[0]!.type).toBe("text"); 33 + expect(result.content[0]!.text).toContain("[Session: test-session]"); 34 + expect(result.content[0]!.text).toContain("completed"); 35 + }); 36 + 37 + it("should include plan ID in response", () => { 38 + mockStore.getActivePlan.mockReturnValue( 39 + createMockPlan({ id: "abc12", goal: "Test goal" }), 40 + ); 41 + mockStore.getQueue.mockReturnValue([]); 42 + mockStore.getCompletedCount.mockReturnValue(1); 43 + 44 + const result = handlePlanComplete(mockStore, "test-session", { 45 + outcome: "Done", 46 + }); 47 + 48 + expect(result.content[0]!.text).toContain("abc12"); 49 + }); 50 + 51 + it("should show remaining queue count", () => { 52 + mockStore.getActivePlan.mockReturnValue( 53 + createMockPlan({ id: "abc12", goal: "Test goal" }), 54 + ); 55 + mockStore.getQueue.mockReturnValue([ 56 + { id: "def34", goal: "Next task", queuePosition: 1 }, 57 + { id: "ghi56", goal: "Another task", queuePosition: 2 }, 58 + ]); 59 + mockStore.getCompletedCount.mockReturnValue(3); 60 + 61 + const result = handlePlanComplete(mockStore, "test-session", { 62 + outcome: "Finished", 63 + }); 64 + 65 + expect(result.content[0]!.text).toContain("2"); // Remaining in queue 66 + }); 67 + 68 + it("should indicate when queue is empty", () => { 69 + mockStore.getActivePlan.mockReturnValue( 70 + createMockPlan({ id: "abc12", goal: "Test goal" }), 71 + ); 72 + mockStore.getQueue.mockReturnValue([]); 73 + mockStore.getCompletedCount.mockReturnValue(1); 74 + 75 + const result = handlePlanComplete(mockStore, "test-session", { 76 + outcome: "All done", 77 + }); 78 + 79 + expect(result.content[0]!.text).toMatch(/queue.*empty|0.*remaining/i); 80 + }); 81 + 82 + it("should handle NO_ACTIVE_PLAN error", () => { 83 + mockStore.completePlan.mockImplementation(() => { 84 + throw new NinePlanError( 85 + "NO_ACTIVE_PLAN", 86 + "No plan is currently active", 87 + "Pull a plan first", 88 + ); 89 + }); 90 + 91 + const result = handlePlanComplete(mockStore, "test-session", { 92 + outcome: "Done", 93 + }); 94 + 95 + expect(result.content[0]!.text).toContain("NO_ACTIVE_PLAN"); 96 + }); 97 + });
+97
tests/unit/tools/plan-defer.test.ts
··· 1 + /** 2 + * Tests for 9plan_plan_defer tool 3 + */ 4 + 5 + import { describe, it, expect, vi, afterEach } from "vitest"; 6 + import { handlePlanDefer } from "../../../src/tools/plan-defer.js"; 7 + import { createMockSessionStore, createMockPlan } from "../../helpers/mocks.js"; 8 + import { NinePlanError } from "../../../src/types.js"; 9 + 10 + describe("handlePlanDefer", () => { 11 + const mockStore = createMockSessionStore(); 12 + 13 + afterEach(() => { 14 + vi.clearAllMocks(); 15 + }); 16 + 17 + it("should defer plan and return formatted response", () => { 18 + mockStore.getActivePlan.mockReturnValue( 19 + createMockPlan({ id: "abc12", goal: "Test goal" }), 20 + ); 21 + mockStore.getQueue.mockReturnValue([ 22 + { id: "abc12", goal: "Test goal", queuePosition: 1 }, 23 + ]); 24 + 25 + const result = handlePlanDefer(mockStore, "test-session", { 26 + reason: "Need more information", 27 + }); 28 + 29 + expect(mockStore.deferPlan).toHaveBeenCalledWith( 30 + "Need more information", 31 + "back", 32 + ); 33 + expect(result.content).toHaveLength(1); 34 + expect(result.content[0]!.type).toBe("text"); 35 + expect(result.content[0]!.text).toContain("[Session: test-session]"); 36 + expect(result.content[0]!.text).toContain("deferred"); 37 + }); 38 + 39 + it("should respect front position", () => { 40 + mockStore.getActivePlan.mockReturnValue( 41 + createMockPlan({ id: "abc12", goal: "Test goal" }), 42 + ); 43 + mockStore.getQueue.mockReturnValue([]); 44 + 45 + handlePlanDefer(mockStore, "test-session", { 46 + reason: "Blocking issue", 47 + position: "front", 48 + }); 49 + 50 + expect(mockStore.deferPlan).toHaveBeenCalledWith("Blocking issue", "front"); 51 + }); 52 + 53 + it("should default position to back", () => { 54 + mockStore.getActivePlan.mockReturnValue( 55 + createMockPlan({ id: "abc12", goal: "Test goal" }), 56 + ); 57 + mockStore.getQueue.mockReturnValue([]); 58 + 59 + handlePlanDefer(mockStore, "test-session", { 60 + reason: "Later", 61 + }); 62 + 63 + expect(mockStore.deferPlan).toHaveBeenCalledWith("Later", "back"); 64 + }); 65 + 66 + it("should show queue status after deferral", () => { 67 + mockStore.getActivePlan.mockReturnValue( 68 + createMockPlan({ id: "abc12", goal: "Test goal" }), 69 + ); 70 + mockStore.getQueue.mockReturnValue([ 71 + { id: "def34", goal: "Other task", queuePosition: 1 }, 72 + { id: "abc12", goal: "Test goal", queuePosition: 2 }, 73 + ]); 74 + 75 + const result = handlePlanDefer(mockStore, "test-session", { 76 + reason: "Blocked", 77 + }); 78 + 79 + expect(result.content[0]!.text).toContain("2"); // Queue length 80 + }); 81 + 82 + it("should handle NO_ACTIVE_PLAN error", () => { 83 + mockStore.deferPlan.mockImplementation(() => { 84 + throw new NinePlanError( 85 + "NO_ACTIVE_PLAN", 86 + "No plan is currently active", 87 + "Pull a plan first", 88 + ); 89 + }); 90 + 91 + const result = handlePlanDefer(mockStore, "test-session", { 92 + reason: "Blocked", 93 + }); 94 + 95 + expect(result.content[0]!.text).toContain("NO_ACTIVE_PLAN"); 96 + }); 97 + });
+88
tests/unit/tools/plan-discard.test.ts
··· 1 + /** 2 + * Tests for 9plan_plan_discard tool 3 + */ 4 + 5 + import { describe, it, expect, vi, afterEach } from "vitest"; 6 + import { handlePlanDiscard } from "../../../src/tools/plan-discard.js"; 7 + import { createMockSessionStore, createMockPlan } from "../../helpers/mocks.js"; 8 + import { NinePlanError } from "../../../src/types.js"; 9 + 10 + describe("handlePlanDiscard", () => { 11 + const mockStore = createMockSessionStore(); 12 + 13 + afterEach(() => { 14 + vi.clearAllMocks(); 15 + }); 16 + 17 + it("should discard plan and return formatted response", () => { 18 + mockStore.getActivePlan.mockReturnValue( 19 + createMockPlan({ id: "abc12", goal: "Test goal" }), 20 + ); 21 + mockStore.getQueue.mockReturnValue([]); 22 + 23 + const result = handlePlanDiscard(mockStore, "test-session", { 24 + reason: "Requirements changed", 25 + }); 26 + 27 + expect(mockStore.discardPlan).toHaveBeenCalledWith("Requirements changed"); 28 + expect(result.content).toHaveLength(1); 29 + expect(result.content[0]!.type).toBe("text"); 30 + expect(result.content[0]!.text).toContain("[Session: test-session]"); 31 + expect(result.content[0]!.text).toContain("discarded"); 32 + }); 33 + 34 + it("should include plan ID in response", () => { 35 + mockStore.getActivePlan.mockReturnValue( 36 + createMockPlan({ id: "xyz99", goal: "Test goal" }), 37 + ); 38 + mockStore.getQueue.mockReturnValue([]); 39 + 40 + const result = handlePlanDiscard(mockStore, "test-session", { 41 + reason: "Not needed", 42 + }); 43 + 44 + expect(result.content[0]!.text).toContain("xyz99"); 45 + }); 46 + 47 + it("should include reason in response", () => { 48 + mockStore.getActivePlan.mockReturnValue( 49 + createMockPlan({ id: "abc12", goal: "Test goal" }), 50 + ); 51 + mockStore.getQueue.mockReturnValue([]); 52 + 53 + const result = handlePlanDiscard(mockStore, "test-session", { 54 + reason: "Scope reduced - feature not needed", 55 + }); 56 + 57 + expect(result.content[0]!.text).toContain("Scope reduced"); 58 + }); 59 + 60 + it("should indicate plan not added to history", () => { 61 + mockStore.getActivePlan.mockReturnValue( 62 + createMockPlan({ id: "abc12", goal: "Test goal" }), 63 + ); 64 + mockStore.getQueue.mockReturnValue([]); 65 + 66 + const result = handlePlanDiscard(mockStore, "test-session", { 67 + reason: "Cancelled", 68 + }); 69 + 70 + expect(result.content[0]!.text).toMatch(/not.*history|won't.*search/i); 71 + }); 72 + 73 + it("should handle NO_ACTIVE_PLAN error", () => { 74 + mockStore.discardPlan.mockImplementation(() => { 75 + throw new NinePlanError( 76 + "NO_ACTIVE_PLAN", 77 + "No plan is currently active", 78 + "Pull a plan first", 79 + ); 80 + }); 81 + 82 + const result = handlePlanDiscard(mockStore, "test-session", { 83 + reason: "Cancel it", 84 + }); 85 + 86 + expect(result.content[0]!.text).toContain("NO_ACTIVE_PLAN"); 87 + }); 88 + });
+118
tests/unit/tools/queue-add.test.ts
··· 1 + /** 2 + * Tests for 9plan_queue_add tool 3 + */ 4 + 5 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 6 + import { handleQueueAdd } from "../../../src/tools/queue-add.js"; 7 + import { NinePlanError } from "../../../src/types.js"; 8 + import { createMockSessionStore, createMockPlan } from "../../helpers/mocks.js"; 9 + 10 + describe("handleQueueAdd", () => { 11 + const mockStore = createMockSessionStore(); 12 + 13 + beforeEach(() => { 14 + mockStore.getSessionPath.mockReturnValue("/tmp/9plan/sessions/test-session"); 15 + }); 16 + 17 + const validArgs = { 18 + context: "Test context", 19 + goal: "Test goal", 20 + approach: "Test approach", 21 + success_criteria: "Test criteria", 22 + }; 23 + 24 + beforeEach(() => { 25 + mockStore.addPlan.mockReturnValue( 26 + createMockPlan({ 27 + id: "abc12", 28 + goal: "Test goal", 29 + queuePosition: 1, 30 + sessionName: "test-session", 31 + status: "queued", 32 + context: "Test context", 33 + }), 34 + ); 35 + }); 36 + 37 + afterEach(() => { 38 + vi.clearAllMocks(); 39 + }); 40 + 41 + it("should add plan and return formatted response", () => { 42 + const result = handleQueueAdd(mockStore, "test-session", validArgs); 43 + 44 + expect(mockStore.addPlan).toHaveBeenCalled(); 45 + expect(result.content).toHaveLength(1); 46 + expect(result.content[0]!.type).toBe("text"); 47 + expect(result.content[0]!.text).toContain("[Session: test-session]"); 48 + expect(result.content[0]!.text).toContain("Plan added: abc12"); 49 + }); 50 + 51 + it("should pass correct input to addPlan", () => { 52 + handleQueueAdd(mockStore, "test-session", validArgs); 53 + 54 + expect(mockStore.addPlan).toHaveBeenCalledWith( 55 + expect.objectContaining({ 56 + context: "Test context", 57 + goal: "Test goal", 58 + approach: "Test approach", 59 + successCriteria: "Test criteria", 60 + }), 61 + "back", 62 + ); 63 + }); 64 + 65 + it("should include optional inputs when provided", () => { 66 + handleQueueAdd(mockStore, "test-session", { 67 + ...validArgs, 68 + inputs: "- auth_module: from auth work", 69 + outputs: "- feature_x: implementation", 70 + }); 71 + 72 + expect(mockStore.addPlan).toHaveBeenCalledWith( 73 + expect.objectContaining({ 74 + inputs: "- auth_module: from auth work", 75 + outputs: "- feature_x: implementation", 76 + }), 77 + "back", 78 + ); 79 + }); 80 + 81 + it("should respect front position", () => { 82 + handleQueueAdd(mockStore, "test-session", { 83 + ...validArgs, 84 + position: "front", 85 + }); 86 + 87 + expect(mockStore.addPlan).toHaveBeenCalledWith(expect.anything(), "front"); 88 + }); 89 + 90 + it("should include plan path in response", () => { 91 + const result = handleQueueAdd(mockStore, "test-session", validArgs); 92 + 93 + expect(result.content[0]!.text).toContain( 94 + "/tmp/9plan/sessions/test-session/plans/abc12.txt", 95 + ); 96 + }); 97 + 98 + it("should include queue position in response", () => { 99 + const result = handleQueueAdd(mockStore, "test-session", validArgs); 100 + 101 + expect(result.content[0]!.text).toContain("Queue position: 1"); 102 + }); 103 + 104 + it("should handle NinePlanError gracefully", () => { 105 + mockStore.addPlan.mockImplementation(() => { 106 + throw new NinePlanError( 107 + "VALIDATION_ERROR", 108 + "Invalid input", 109 + "Check your input", 110 + ); 111 + }); 112 + 113 + const result = handleQueueAdd(mockStore, "test-session", validArgs); 114 + 115 + expect(result.content[0]!.text).toContain("VALIDATION_ERROR"); 116 + expect(result.content[0]!.text).toContain("Invalid input"); 117 + }); 118 + });
+103
tests/unit/tools/queue-pull.test.ts
··· 1 + /** 2 + * Tests for 9plan_queue_pull tool 3 + */ 4 + 5 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 6 + import { handleQueuePull } from "../../../src/tools/queue-pull.js"; 7 + import { createMockSessionStore, createMockPlan } from "../../helpers/mocks.js"; 8 + import { NinePlanError } from "../../../src/types.js"; 9 + 10 + describe("handleQueuePull", () => { 11 + const mockStore = createMockSessionStore(); 12 + 13 + beforeEach(() => { 14 + mockStore.getSessionPath.mockReturnValue("/tmp/9plan/sessions/test-session"); 15 + }); 16 + 17 + afterEach(() => { 18 + vi.clearAllMocks(); 19 + }); 20 + 21 + it("should pull plan and return formatted response", () => { 22 + mockStore.pullPlan.mockReturnValue( 23 + createMockPlan({ 24 + id: "abc12", 25 + goal: "Test goal", 26 + }), 27 + ); 28 + 29 + const result = handleQueuePull(mockStore, "test-session", {}); 30 + 31 + expect(mockStore.pullPlan).toHaveBeenCalled(); 32 + expect(result.content).toHaveLength(1); 33 + expect(result.content[0]!.type).toBe("text"); 34 + expect(result.content[0]!.text).toContain("[Session: test-session]"); 35 + expect(result.content[0]!.text).toContain("Active plan: abc12"); 36 + }); 37 + 38 + it("should include plan path in response", () => { 39 + mockStore.pullPlan.mockReturnValue( 40 + createMockPlan({ 41 + id: "abc12", 42 + goal: "Test goal", 43 + }), 44 + ); 45 + 46 + const result = handleQueuePull(mockStore, "test-session", {}); 47 + 48 + expect(result.content[0]!.text).toContain( 49 + "/tmp/9plan/sessions/test-session/plans/abc12.txt", 50 + ); 51 + }); 52 + 53 + it("should include instructions for reviewing plan", () => { 54 + mockStore.pullPlan.mockReturnValue( 55 + createMockPlan({ 56 + id: "abc12", 57 + goal: "Build the authentication module", 58 + }), 59 + ); 60 + 61 + const result = handleQueuePull(mockStore, "test-session", {}); 62 + 63 + // The response tells users to read the plan file 64 + expect(result.content[0]!.text).toContain("Read the plan file"); 65 + }); 66 + 67 + it("should handle QUEUE_EMPTY with completed count", () => { 68 + mockStore.pullPlan.mockImplementation(() => { 69 + throw new NinePlanError( 70 + "QUEUE_EMPTY", 71 + "The queue is empty", 72 + "Use history search", 73 + ); 74 + }); 75 + mockStore.getCompletedCount.mockReturnValue(10); 76 + 77 + const result = handleQueuePull(mockStore, "test-session", {}); 78 + 79 + expect(result.content[0]!.text).toContain("Queue is empty"); 80 + expect(result.content[0]!.text).toContain("10"); 81 + }); 82 + 83 + it("should handle PLAN_ALREADY_ACTIVE with active plan info", () => { 84 + mockStore.pullPlan.mockImplementation(() => { 85 + throw new NinePlanError( 86 + "PLAN_ALREADY_ACTIVE", 87 + "Plan xyz99 is already active", 88 + "Complete or defer the active plan", 89 + ); 90 + }); 91 + mockStore.getActivePlan.mockReturnValue( 92 + createMockPlan({ 93 + id: "xyz99", 94 + goal: "Active task", 95 + }), 96 + ); 97 + 98 + const result = handleQueuePull(mockStore, "test-session", {}); 99 + 100 + expect(result.content[0]!.text).toContain("already active"); 101 + expect(result.content[0]!.text).toContain("xyz99"); 102 + }); 103 + });
+72
tests/unit/tools/session-create.test.ts
··· 1 + /** 2 + * Tests for 9plan_session_create tool 3 + */ 4 + 5 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 6 + import { handleSessionCreate } from "../../../src/tools/session-create.js"; 7 + import { createMockSessionStore } from "../../helpers/mocks.js"; 8 + 9 + // Mock the container module 10 + vi.mock("../../../src/container.js", () => ({ 11 + createNewSession: vi.fn(), 12 + getSessionPath: vi.fn(), 13 + })); 14 + 15 + import { createNewSession, getSessionPath } from "../../../src/container.js"; 16 + 17 + describe("handleSessionCreate", () => { 18 + const mockStore = createMockSessionStore(); 19 + 20 + beforeEach(() => { 21 + vi.mocked(createNewSession).mockReturnValue({ 22 + sessionName: "test-session-name", 23 + store: mockStore as never, 24 + }); 25 + vi.mocked(getSessionPath).mockReturnValue( 26 + "/tmp/9plan/sessions/test-session-name", 27 + ); 28 + }); 29 + 30 + afterEach(() => { 31 + vi.clearAllMocks(); 32 + }); 33 + 34 + it("should create session and return formatted response", () => { 35 + const result = handleSessionCreate({}); 36 + 37 + expect(createNewSession).toHaveBeenCalledWith(undefined); 38 + expect(result.content).toHaveLength(1); 39 + expect(result.content[0]!.type).toBe("text"); 40 + expect(result.content[0]!.text).toContain("[Session: test-session-name]"); 41 + expect(result.content[0]!.text).toContain( 42 + "Session created: test-session-name", 43 + ); 44 + }); 45 + 46 + it("should pass task description to createNewSession", () => { 47 + handleSessionCreate({ task_description: "Build a cool app" }); 48 + 49 + expect(createNewSession).toHaveBeenCalledWith("Build a cool app"); 50 + }); 51 + 52 + it("should include session path in response", () => { 53 + const result = handleSessionCreate({}); 54 + 55 + expect(result.content[0]!.text).toContain( 56 + "/tmp/9plan/sessions/test-session-name", 57 + ); 58 + }); 59 + 60 + it("should close the store after creation", () => { 61 + handleSessionCreate({}); 62 + 63 + expect(mockStore.close).toHaveBeenCalled(); 64 + }); 65 + 66 + it("should include usage instructions in response", () => { 67 + const result = handleSessionCreate({}); 68 + 69 + expect(result.content[0]!.text).toContain("9plan_queue_add"); 70 + expect(result.content[0]!.text).toContain("9plan_queue_pull"); 71 + }); 72 + });
+117
tests/unit/tools/session-resume.test.ts
··· 1 + /** 2 + * Tests for 9plan_session_resume tool 3 + */ 4 + 5 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 6 + import { handleSessionResume } from "../../../src/tools/session-resume.js"; 7 + import type { SessionState } from "../../../src/types.js"; 8 + import { createMockSessionStore } from "../../helpers/mocks.js"; 9 + 10 + // Mock the container module 11 + vi.mock("../../../src/container.js", () => ({ 12 + resumeSession: vi.fn(), 13 + })); 14 + 15 + import { resumeSession } from "../../../src/container.js"; 16 + 17 + describe("handleSessionResume", () => { 18 + const mockState: SessionState = { 19 + sessionName: "test-session", 20 + sessionPath: "/tmp/9plan/sessions/test-session", 21 + taskDescription: "Test task", 22 + queue: [ 23 + { id: "abc12", goal: "First task", queuePosition: 1 }, 24 + { id: "def34", goal: "Second task", queuePosition: 2 }, 25 + ], 26 + activePlan: null, 27 + completedCount: 5, 28 + }; 29 + 30 + const mockStore = createMockSessionStore(); 31 + 32 + beforeEach(() => { 33 + mockStore.getState.mockReturnValue(mockState); 34 + }); 35 + 36 + beforeEach(() => { 37 + vi.mocked(resumeSession).mockReturnValue(mockStore as never); 38 + }); 39 + 40 + afterEach(() => { 41 + vi.clearAllMocks(); 42 + }); 43 + 44 + it("should resume session and return state", () => { 45 + const result = handleSessionResume({ session_name: "test-session" }); 46 + 47 + expect(resumeSession).toHaveBeenCalledWith("test-session"); 48 + expect(result.content).toHaveLength(1); 49 + expect(result.content[0]!.type).toBe("text"); 50 + expect(result.content[0]!.text).toContain("[Session: test-session]"); 51 + }); 52 + 53 + it("should include task description in response", () => { 54 + const result = handleSessionResume({ session_name: "test-session" }); 55 + 56 + expect(result.content[0]!.text).toContain("Test task"); 57 + }); 58 + 59 + it("should list queue contents", () => { 60 + const result = handleSessionResume({ session_name: "test-session" }); 61 + 62 + expect(result.content[0]!.text).toContain("abc12"); 63 + expect(result.content[0]!.text).toContain("First task"); 64 + expect(result.content[0]!.text).toContain("def34"); 65 + expect(result.content[0]!.text).toContain("Second task"); 66 + }); 67 + 68 + it("should show completed count", () => { 69 + const result = handleSessionResume({ session_name: "test-session" }); 70 + 71 + expect(result.content[0]!.text).toContain("5"); 72 + }); 73 + 74 + it("should show active plan when present", () => { 75 + mockStore.getState.mockReturnValue({ 76 + ...mockState, 77 + activePlan: { 78 + id: "xyz99", 79 + goal: "Active work", 80 + filePath: "/tmp/xyz99.txt", 81 + }, 82 + }); 83 + 84 + const result = handleSessionResume({ session_name: "test-session" }); 85 + 86 + expect(result.content[0]!.text).toContain("xyz99"); 87 + expect(result.content[0]!.text).toContain("Active work"); 88 + }); 89 + 90 + it("should handle NinePlanError gracefully", async () => { 91 + const { NinePlanError } = await import("../../../src/types.js"); 92 + vi.mocked(resumeSession).mockImplementation(() => { 93 + throw new NinePlanError( 94 + "SESSION_NOT_FOUND", 95 + "Session not found: nonexistent", 96 + "Create a new session", 97 + ); 98 + }); 99 + 100 + const result = handleSessionResume({ 101 + session_name: "test-test-test", 102 + }); 103 + 104 + expect(result.content[0]!.text).toContain("SESSION_NOT_FOUND"); 105 + }); 106 + 107 + it("should propagate non-NinePlanError errors", () => { 108 + const genericError = new Error("Unexpected error"); 109 + vi.mocked(resumeSession).mockImplementation(() => { 110 + throw genericError; 111 + }); 112 + 113 + expect(() => { 114 + handleSessionResume({ session_name: "test-test-test" }); 115 + }).toThrow("Unexpected error"); 116 + }); 117 + });
+9
tsconfig.build.json
··· 1 + { 2 + "extends": "./tsconfig.json", 3 + "compilerOptions": { 4 + "rootDir": "./src", 5 + "outDir": "./dist" 6 + }, 7 + "include": ["src/**/*"], 8 + "exclude": ["node_modules", "dist", "tests", "scripts"] 9 + }
+40
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + // File Layout 4 + "rootDir": "./src", 5 + "outDir": "./dist", 6 + 7 + // Environment Settings 8 + "module": "NodeNext", 9 + "moduleResolution": "NodeNext", 10 + "target": "ES2022", 11 + "lib": ["ES2022"], 12 + "types": ["node"], 13 + 14 + // Output Settings (tsgo compatible) 15 + "sourceMap": true, 16 + "declaration": false, 17 + "declarationMap": false, 18 + 19 + // Stricter Typechecking 20 + "strict": true, 21 + "noUncheckedIndexedAccess": true, 22 + "exactOptionalPropertyTypes": true, 23 + "noImplicitReturns": true, 24 + "noFallthroughCasesInSwitch": true, 25 + "noUnusedLocals": true, 26 + "noUnusedParameters": true, 27 + 28 + // Module Compatibility (tsgo required) 29 + "verbatimModuleSyntax": true, 30 + "isolatedModules": true, 31 + "moduleDetection": "force", 32 + "esModuleInterop": true, 33 + "forceConsistentCasingInFileNames": true, 34 + "skipLibCheck": true, 35 + "resolveJsonModule": true, 36 + "noEmitOnError": true 37 + }, 38 + "include": ["src/**/*", "scripts/**/*", "tests/**/*"], 39 + "exclude": ["node_modules", "dist"] 40 + }
+18
vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config'; 2 + 3 + export default defineConfig({ 4 + test: { 5 + globals: true, 6 + environment: 'node', 7 + include: ['tests/**/*.test.ts'], 8 + exclude: ['node_modules', 'dist'], 9 + coverage: { 10 + provider: 'v8', 11 + reporter: ['text', 'json', 'html'], 12 + include: ['src/**/*.ts'], 13 + exclude: ['src/index.ts'], 14 + }, 15 + testTimeout: 10000, 16 + hookTimeout: 10000, 17 + }, 18 + });