diff --git a/README.md b/README.md index c56df8e..d669076 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,9 @@ Type a message and press Enter. Responses stream to the terminal as they're gene Useful built-in commands: -- `/status` shows current context usage +- `/status` shows current model, context usage, and session stats +- `/model` shows the current model +- `/model ` switches to a different model mid-session (takes effect on the next API call) - `/remember ` stores a durable project memory - `/recall [query]` lists stored memories or searches them - `/forget ` removes a stored memory diff --git a/ROADMAP.md b/ROADMAP.md index 976c2c4..6aa4395 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -148,6 +148,6 @@ This roadmap translates `REQUIREMENTS.md` into phased, atomic tasks for building - [ ] Add Model Context Protocol (MCP) client support for external tool servers. - [ ] Add reusable skills with named prompts and tool bundles. - [ ] Add lifecycle hooks (before tool, after edit, on startup). -- [ ] Add model switching inside an active session. +- [x] Add model switching inside an active session. - [ ] Add non-interactive headless mode for scripting and automation. - [ ] Add CI job coverage for headless execution paths. diff --git a/docs/SOCRATIC_JOURNAL.md b/docs/SOCRATIC_JOURNAL.md index 04c92ba..fe58c29 100644 --- a/docs/SOCRATIC_JOURNAL.md +++ b/docs/SOCRATIC_JOURNAL.md @@ -276,3 +276,21 @@ That is easier to reason about, easier to test, and safer than letting a live RE - `src/cli.ts:23-45` — CLI entrypoint accepts `--resume` and routes startup through the resume-aware bootstrap - `src/repl/bootstrap.ts:69-113` — fresh and resumed boot flows are modeled as separate bootstrap modes - `src/repl.ts:81-115` — REPL loads the selected bootstrap mode before entering the interactive loop + +--- + +## Going Further - Model Switching + +### Q1: Why use a getter function instead of a static value for the model in the subagent tool? + +**Why it matters:** The subagent tool is registered once at REPL startup. If it captures the model as a string at registration time, switching models mid-session wouldn't propagate to subagent calls — they'd keep using the original model. + +**What we learned:** This is a classic closure vs. late-binding decision. When state can change after an object is constructed, consumers need to read the current value at call time rather than capturing it at construction time. We solved this by changing `SubagentToolConfig.model: string` to `SubagentToolConfig.getModel: () => string`. The subagent tool calls `config.getModel()` when executing, not when being created. The same principle applies to the REPL's `handleSlashCommand` options — `getModel`/`setModel` callbacks let the command handler read and mutate the REPL's mutable `model` variable without holding a stale copy. + +**Demonstrated in:** +- `src/subagent/tool.ts:5` — `getModel: () => string` replaces `model: string` in `SubagentToolConfig` +- `src/subagent/tool.ts:36` — `config.getModel()` called at execution time, not construction +- `src/repl.ts:34-39` — subagent tool registered with `getModel: () => model` closure +- `src/repl.ts:108` — `let model` (mutable) replaces `const model` +- `src/repl/commands.ts:16-17` — `getModel`/`setModel` callbacks in `HandleSlashCommandOptions` +- `src/repl/commands.ts:53-62` — `/model` command shows current model or switches to a new one diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..7e9763b --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,7 @@ +import llmCore from "eslint-plugin-llm-core"; +import tseslint from "typescript-eslint"; + +export default [ + ...tseslint.configs.recommended, + ...llmCore.configs.recommended, +]; diff --git a/package-lock.json b/package-lock.json index eb26e5f..52f3494 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,12 @@ }, "devDependencies": { "@types/node": "^25.5.0", + "@typescript-eslint/utils": "^8.58.0", + "eslint": "^9.0.0", + "eslint-plugin-llm-core": "^0.3.2", "tsx": "^4.21.0", "typescript": "^5.9.3", + "typescript-eslint": "^8.0.0", "vitest": "^4.1.0" }, "engines": { @@ -499,6 +503,202 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -856,6 +1056,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", @@ -866,6 +1073,288 @@ "undici-types": "~7.18.0" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.58.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.58.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitest/expect": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", @@ -979,19 +1468,110 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=12" + "node": ">=0.4.0" } }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", @@ -1011,6 +1591,26 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/commander": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", @@ -1020,6 +1620,13 @@ "node": ">=20" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1027,6 +1634,46 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1098,70 +1745,501 @@ "@esbuild/win32-x64": "0.27.4" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-llm-core": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-llm-core/-/eslint-plugin-llm-core-0.3.2.tgz", + "integrity": "sha512-aqLiwMFqvb+sFSieHjwj4de9NMIxkcTjhNCxqbX/dqfSJpdwSiooc4t9LGiSodoqry7HhM2rcETNXdF7ynPIzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^8.0.0", + "eslint": ">=8" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">=0.8.19" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "dependencies": { + "json-buffer": "3.0.1" } }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "engines": { + "node": ">= 0.8.0" } }, "node_modules/lightningcss": { @@ -1437,6 +2515,29 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1447,6 +2548,26 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1466,6 +2587,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -1477,6 +2605,89 @@ ], "license": "MIT" }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1533,6 +2744,36 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1577,6 +2818,42 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -1608,6 +2885,32 @@ "dev": true, "license": "MIT" }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -1652,6 +2955,19 @@ "node": ">=14.0.0" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1680,6 +2996,19 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1694,6 +3023,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", + "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", @@ -1701,6 +3054,16 @@ "dev": true, "license": "MIT" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/vite": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", @@ -1861,6 +3224,22 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -1877,6 +3256,29 @@ "engines": { "node": ">=8" } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index d2c5bee..4acf796 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "dev": "tsx src/cli.ts", "build": "tsc", "typecheck": "tsc --noEmit", + "lint": "eslint src/", + "lint:fix": "eslint src/ --fix", "test": "vitest run" }, "repository": { @@ -30,8 +32,12 @@ }, "devDependencies": { "@types/node": "^25.5.0", + "@typescript-eslint/utils": "^8.58.0", + "eslint": "^9.0.0", + "eslint-plugin-llm-core": "^0.3.2", "tsx": "^4.21.0", "typescript": "^5.9.3", + "typescript-eslint": "^8.0.0", "vitest": "^4.1.0" } } diff --git a/src/__tests__/config/config.test.ts b/src/__tests__/config/config.test.ts index f335da2..e57fad8 100644 --- a/src/__tests__/config/config.test.ts +++ b/src/__tests__/config/config.test.ts @@ -96,7 +96,8 @@ describe("loadConfig", () => { }); expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining("always"), + "Warning: invalid permission value for tool", + expect.objectContaining({ tool: "bash", permission: "always" }), ); expect(result.permissions?.bash).toBeUndefined(); expect(result.permissions?.write_file).toBe("prompt"); diff --git a/src/__tests__/config/context.test.ts b/src/__tests__/config/context.test.ts index a97353b..9499cf0 100644 --- a/src/__tests__/config/context.test.ts +++ b/src/__tests__/config/context.test.ts @@ -48,7 +48,8 @@ describe("loadProjectInstructions", () => { expect(result).toBeNull(); expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining("AGENTS.md"), + "Warning: could not read AGENTS.md", + expect.objectContaining({ error: expect.stringContaining("EACCES") }), ); // Restore permissions for cleanup diff --git a/src/__tests__/config/loader.test.ts b/src/__tests__/config/loader.test.ts index de911db..8c2cc0c 100644 --- a/src/__tests__/config/loader.test.ts +++ b/src/__tests__/config/loader.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { loadConfigFile } from "../../config/loader.js"; +import { loadConfigFile } from "../../config/loadConfigFile.js"; describe("loadConfigFile", () => { let tempDir: string; diff --git a/src/__tests__/context/compression.test.ts b/src/__tests__/context/compression.test.ts index 13ec4ad..fd279b2 100644 --- a/src/__tests__/context/compression.test.ts +++ b/src/__tests__/context/compression.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { createMessageStream } from "../../api/anthropic.js"; import type { Message } from "../../api/anthropic.js"; -import { compressConversation } from "../../context/compression.js"; +import { compressConversation } from "../../context/compressConversation.js"; vi.mock("../../api/anthropic.js", async () => { const actual = await vi.importActual( diff --git a/src/__tests__/edit-file.test.ts b/src/__tests__/edit-file.test.ts index 032fdee..dccab71 100644 --- a/src/__tests__/edit-file.test.ts +++ b/src/__tests__/edit-file.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { editFileTool } from "../tools/edit-file.js"; +import { editFileTool } from "../tools/editFileTool.js"; describe("edit_file tool", () => { let tempDir: string | undefined; diff --git a/src/__tests__/model-switching.test.ts b/src/__tests__/model-switching.test.ts new file mode 100644 index 0000000..03095a7 --- /dev/null +++ b/src/__tests__/model-switching.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it, vi } from "vitest"; + +import { handleSlashCommand } from "../repl/commands.js"; +import { TokenTracker } from "../context/tracker.js"; + +function createCommandOptions(overrides: { + getModel?: () => string; + setModel?: (modelId: string) => void; +} = {}) { + return { + projectRoot: "/tmp/project", + tracker: new TokenTracker(), + writeLine: vi.fn(), + remember: vi.fn(), + recall: vi.fn(), + forget: vi.fn(), + getModel: overrides.getModel ?? (() => "claude-sonnet-4-20250514"), + setModel: overrides.setModel ?? vi.fn(), + }; +} + +describe("model switching", () => { + describe("/model command", () => { + it("shows current model when called without arguments", async () => { + const options = createCommandOptions({ + getModel: () => "claude-sonnet-4-20250514", + }); + + const handled = await handleSlashCommand("/model", options); + + expect(handled).toBe(true); + expect(options.writeLine).toHaveBeenCalledWith( + "Current model: claude-sonnet-4-20250514", + ); + }); + + it("switches model when called with an argument", async () => { + const setModel = vi.fn(); + const options = createCommandOptions({ + getModel: () => "claude-sonnet-4-20250514", + setModel, + }); + + const handled = await handleSlashCommand("/model claude-opus-4-20250514", options); + + expect(handled).toBe(true); + expect(setModel).toHaveBeenCalledWith("claude-opus-4-20250514"); + expect(options.writeLine).toHaveBeenCalledWith( + "Model switched: claude-sonnet-4-20250514 → claude-opus-4-20250514", + ); + }); + + it("is case-insensitive for the command prefix", async () => { + const options = createCommandOptions(); + + const handled = await handleSlashCommand("/MODEL", options); + + expect(handled).toBe(true); + expect(options.writeLine).toHaveBeenCalledWith( + expect.stringContaining("Current model:"), + ); + }); + + it("preserves model ID casing in the argument", async () => { + const setModel = vi.fn(); + const options = createCommandOptions({ + getModel: () => "old-model", + setModel, + }); + + await handleSlashCommand("/model Claude-Sonnet-4", options); + + expect(setModel).toHaveBeenCalledWith("Claude-Sonnet-4"); + }); + }); + + describe("/status includes model", () => { + it("shows current model in status output", async () => { + const options = createCommandOptions({ + getModel: () => "claude-haiku-3-20250307", + }); + + await handleSlashCommand("/status", options); + + expect(options.writeLine).toHaveBeenCalledWith( + expect.stringContaining("Model: claude-haiku-3-20250307"), + ); + }); + }); +}); diff --git a/src/__tests__/step7-spec-anchors.test.ts b/src/__tests__/step7-spec-anchors.test.ts index 54c716c..27d9adc 100644 --- a/src/__tests__/step7-spec-anchors.test.ts +++ b/src/__tests__/step7-spec-anchors.test.ts @@ -97,7 +97,7 @@ async function loadSessionsModule() { } async function loadCliAppModule() { - const moduleUrl = new URL("../cli/app.js", import.meta.url).href; + const moduleUrl = new URL("../cli/runCli.js", import.meta.url).href; return import(moduleUrl); } @@ -572,6 +572,8 @@ describe("Step 7 spec anchors", () => { remember: vi.fn(), recall: vi.fn(), forget: vi.fn(), + getModel: () => "claude-sonnet-4-20250514", + setModel: vi.fn(), }); expect(handled).toBe(true); @@ -591,6 +593,8 @@ describe("Step 7 spec anchors", () => { remember, recall: vi.fn(), forget: vi.fn(), + getModel: () => "claude-sonnet-4-20250514", + setModel: vi.fn(), }); expect(handled).toBe(true); @@ -612,6 +616,8 @@ describe("Step 7 spec anchors", () => { remember: vi.fn(), recall, forget: vi.fn(), + getModel: () => "claude-sonnet-4-20250514", + setModel: vi.fn(), }); expect(handled).toBe(true); @@ -631,6 +637,8 @@ describe("Step 7 spec anchors", () => { remember: vi.fn(), recall: vi.fn(), forget, + getModel: () => "claude-sonnet-4-20250514", + setModel: vi.fn(), }); expect(handled).toBe(true); diff --git a/src/__tests__/tools.test.ts b/src/__tests__/tools.test.ts index 3f05ff8..ed07fdc 100644 --- a/src/__tests__/tools.test.ts +++ b/src/__tests__/tools.test.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { createToolRegistry } from "../tools/index.js"; -import { readFileTool } from "../tools/read-file.js"; +import { readFileTool } from "../tools/readFileTool.js"; describe("tool registry", () => { it("registers all six built-in tools on creation", () => { diff --git a/src/__tests__/write-file.test.ts b/src/__tests__/write-file.test.ts index 77e2bf5..c54045f 100644 --- a/src/__tests__/write-file.test.ts +++ b/src/__tests__/write-file.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { writeFileTool } from "../tools/write-file.js"; +import { writeFileTool } from "../tools/writeFileTool.js"; describe("write_file tool", () => { let tempDir: string | undefined; diff --git a/src/agent.ts b/src/agent.ts index ec96d6c..b3beaf8 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -5,7 +5,7 @@ import type { StreamEvent, ToolUseBlock, } from "./api/anthropic.js"; -import { compressConversation } from "./context/compression.js"; +import { compressConversation } from "./context/compressConversation.js"; import type { TokenTracker } from "./context/tracker.js"; import type { ToolRegistry, ToolResult } from "./tools/index.js"; diff --git a/src/api/anthropic.ts b/src/api/anthropic.ts index efb1186..0340037 100644 --- a/src/api/anthropic.ts +++ b/src/api/anthropic.ts @@ -1,3 +1,5 @@ +import { parseSSEStream } from "./sse-parser.js"; + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -206,85 +208,7 @@ export async function createMessageStream( return response; } -type SSEEvent = { - event: string | null; - data: string; -}; - -export async function* parseSSEStream( - body: ReadableStream, -): AsyncGenerator { - const reader = body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - let currentEvent: string | null = null; - let dataLines: string[] = []; - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() ?? ""; - - for (const rawLine of lines) { - const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine; - - if (line === "") { - if (currentEvent !== null || dataLines.length > 0) { - yield { - event: currentEvent, - data: dataLines.join("\n"), - }; - currentEvent = null; - dataLines = []; - } - continue; - } - - if (line.startsWith(":")) continue; - - const colonIndex = line.indexOf(":"); - if (colonIndex === -1) continue; - - const field = line.slice(0, colonIndex); - let value = line.slice(colonIndex + 1); - if (value.startsWith(" ")) value = value.slice(1); - - if (field === "event") { - currentEvent = value; - } else if (field === "data") { - dataLines.push(value); - } - } - } - - if (buffer.length > 0) { - const line = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer; - if (line !== "" && !line.startsWith(":")) { - const colonIndex = line.indexOf(":"); - if (colonIndex !== -1) { - const field = line.slice(0, colonIndex); - let value = line.slice(colonIndex + 1); - if (value.startsWith(" ")) value = value.slice(1); - if (field === "event") currentEvent = value; - else if (field === "data") dataLines.push(value); - } - } - } - - if (currentEvent !== null || dataLines.length > 0) { - yield { - event: currentEvent, - data: dataLines.join("\n"), - }; - } - } finally { - reader.releaseLock(); - } -} +export { parseSSEStream, type SSEEvent } from "./sse-parser.js"; type StreamMessageOptions = { messages: Message[]; diff --git a/src/api/sse-parser.ts b/src/api/sse-parser.ts new file mode 100644 index 0000000..485b00f --- /dev/null +++ b/src/api/sse-parser.ts @@ -0,0 +1,96 @@ +// --------------------------------------------------------------------------- +// SSE Stream Parser - Extracted from anthropic.ts for modularity +// --------------------------------------------------------------------------- + +export type SSEEvent = { + event: string | null; + data: string; +}; + +function processLine( + line: string, + state: { currentEvent: string | null; dataLines: string[] }, +): void { + if (line === "") { + return; + } + + if (line.startsWith(":")) { + return; + } + + const colonIndex = line.indexOf(":"); + if (colonIndex === -1) { + return; + } + + const field = line.slice(0, colonIndex); + let value = line.slice(colonIndex + 1); + if (value.startsWith(" ")) { + value = value.slice(1); + } + + if (field === "event") { + state.currentEvent = value; + } else if (field === "data") { + state.dataLines.push(value); + } +} + +function finalizeEvent(state: { currentEvent: string | null; dataLines: string[] }): SSEEvent | null { + if (state.currentEvent !== null || state.dataLines.length > 0) { + const event: SSEEvent = { + event: state.currentEvent, + data: state.dataLines.join("\n"), + }; + state.currentEvent = null; + state.dataLines = []; + return event; + } + return null; +} + +export async function* parseSSEStream( + body: ReadableStream, +): AsyncGenerator { + const reader = body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + const state = { currentEvent: null as string | null, dataLines: [] as string[] }; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + + for (const rawLine of lines) { + const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine; + + if (line === "") { + const event = finalizeEvent(state); + if (event) yield event; + continue; + } + + processLine(line, state); + } + } + + // Handle remaining buffer + if (buffer.length > 0) { + const line = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer; + if (line !== "" && !line.startsWith(":")) { + processLine(line, state); + } + } + + const finalEvent = finalizeEvent(state); + if (finalEvent) yield finalEvent; + } finally { + reader.releaseLock(); + } +} diff --git a/src/cli.ts b/src/cli.ts index 8fb9785..840fff9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -26,7 +26,7 @@ program.action(async (_options, command) => { const parsed = command.opts() as { resume?: string }; const args = parsed.resume ? ["--resume", parsed.resume] : []; - const { runCli } = await import("./cli/app.js"); + const { runCli } = await import("./cli/runCli.js"); const { loadConfig, loadProjectInstructions } = await import("./config/index.js"); const { loadSessionForResume } = await import("./persistence/sessions.js"); const { startRepl } = await import("./repl.js"); diff --git a/src/cli/app.ts b/src/cli/runCli.ts similarity index 100% rename from src/cli/app.ts rename to src/cli/runCli.ts diff --git a/src/config/context.ts b/src/config/context.ts index a6cd489..4de3c40 100644 --- a/src/config/context.ts +++ b/src/config/context.ts @@ -8,7 +8,7 @@ export function loadProjectInstructions(cwd: string): string | null { } catch (error: unknown) { const isNotFound = error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT"; if (!isNotFound) { - console.warn(`Warning: could not read AGENTS.md: ${error instanceof Error ? error.message : String(error)}`); + console.warn("Warning: could not read AGENTS.md", { error: error instanceof Error ? error.message : String(error) }); } return null; } diff --git a/src/config/index.ts b/src/config/index.ts index cd7f42d..43abcec 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -2,12 +2,12 @@ import { homedir } from "node:os"; import { join } from "node:path"; import type { ToolPermission } from "../tools/index.js"; -import { loadConfigFile } from "./loader.js"; +import { loadConfigFile } from "./loadConfigFile.js"; import { mergeConfigs } from "./merge.js"; import type { Config } from "./types.js"; export type { Config, ResolvedConfig } from "./types.js"; -export { loadConfigFile } from "./loader.js"; +export { loadConfigFile } from "./loadConfigFile.js"; export { mergeConfigs } from "./merge.js"; export { loadProjectInstructions, assembleSystemPrompt } from "./context.js"; @@ -26,7 +26,7 @@ function validatePermissions(config: Config): Config { if (VALID_PERMISSIONS.has(perm as ToolPermission)) { validated[tool] = perm as ToolPermission; } else { - console.warn(`Warning: invalid permission value "${perm}" for tool "${tool}", skipping`); + console.warn("Warning: invalid permission value for tool", { tool, permission: perm }); } } diff --git a/src/config/loader.ts b/src/config/loadConfigFile.ts similarity index 100% rename from src/config/loader.ts rename to src/config/loadConfigFile.ts diff --git a/src/context/compression.ts b/src/context/compressConversation.ts similarity index 97% rename from src/context/compression.ts rename to src/context/compressConversation.ts index 6ca662f..fd3d1e3 100644 --- a/src/context/compression.ts +++ b/src/context/compressConversation.ts @@ -1,5 +1,6 @@ import type { ContentBlock, Message } from "../api/anthropic.js"; -import { createMessageStream, parseSSEStream } from "../api/anthropic.js"; +import { createMessageStream } from "../api/anthropic.js"; +import { parseSSEStream } from "../api/sse-parser.js"; import type { CompressionOptions, CompressionResult } from "./types.js"; const DEFAULT_PRESERVE_TURNS = 6; diff --git a/src/context/index.ts b/src/context/index.ts index 6e12f7a..700b710 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -7,4 +7,4 @@ export type { } from "./types.js"; export { TokenTracker } from "./tracker.js"; -export { compressConversation } from "./compression.js"; +export { compressConversation } from "./compressConversation.js"; diff --git a/src/repl.ts b/src/repl.ts index 0d3ee65..30d1c90 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -75,7 +75,7 @@ export async function startRepl(apiKey: string, config: ResolvedConfig = {}): Pr toolRegistry.register( createSubagentTool({ toolRegistry, - model: config.model ?? DEFAULT_MODEL, + getModel: () => model, apiKey, systemPrompt: assembleSystemPrompt( BASE_SYSTEM_PROMPT, @@ -86,7 +86,7 @@ export async function startRepl(apiKey: string, config: ResolvedConfig = {}): Pr ); const promptForApproval = createPromptForApproval(rl); const tokenTracker = new TokenTracker(); - const model = config.model ?? DEFAULT_MODEL; + let model = config.model ?? DEFAULT_MODEL; const projectRoot = config.projectRoot; const systemPrompt = assembleSystemPrompt( BASE_SYSTEM_PROMPT, @@ -164,6 +164,8 @@ export async function startRepl(apiKey: string, config: ResolvedConfig = {}): Pr remember, recall, forget, + getModel: () => model, + setModel: (newModel: string) => { model = newModel; }, })) { continue; } diff --git a/src/repl/commands.ts b/src/repl/commands.ts index 9a7e243..b43ef94 100644 --- a/src/repl/commands.ts +++ b/src/repl/commands.ts @@ -10,9 +10,11 @@ type HandleSlashCommandOptions = { remember: (projectRoot: string, text: string) => Promise<{ id: string; text: string }>; recall: (projectRoot: string, query?: string) => Promise; forget: (projectRoot: string, memoryId: string) => Promise<{ removed: boolean }>; + getModel: () => string; + setModel: (modelId: string) => void; }; -function formatStatus(tracker: TokenTracker): string { +function formatStatus(tracker: TokenTracker, model: string): string { const stats = tracker.getStats(); const percentage = stats.usagePercentage.toFixed(1); const warning = stats.usagePercentage >= 75 @@ -20,6 +22,7 @@ function formatStatus(tracker: TokenTracker): string { : chalk.green("Status: OK"); return [ + `Model: ${model}`, `Context: ${stats.currentContextCombinedTokens.toLocaleString()} / ${stats.contextWindowLimit.toLocaleString()} tokens (${percentage}%)`, `Session total: ${stats.sessionCombinedTokens.toLocaleString()} tokens`, `Messages: ${stats.messageCount} turns`, @@ -40,10 +43,22 @@ export async function handleSlashCommand( return false; } - const { projectRoot, tracker, writeLine, remember, recall, forget } = options; + const { projectRoot, tracker, writeLine, remember, recall, forget, getModel, setModel } = options; if (trimmed.toLowerCase() === "/status") { - writeLine(formatStatus(tracker)); + writeLine(formatStatus(tracker, getModel())); + return true; + } + + if (trimmed.toLowerCase().startsWith("/model")) { + const arg = trimmed.slice("/model".length).trim(); + if (arg.length === 0) { + writeLine(`Current model: ${getModel()}`); + } else { + const previousModel = getModel(); + setModel(arg); + writeLine(`Model switched: ${previousModel} → ${arg}`); + } return true; } diff --git a/src/subagent/tool.ts b/src/subagent/tool.ts index 010a57c..8a35a60 100644 --- a/src/subagent/tool.ts +++ b/src/subagent/tool.ts @@ -3,7 +3,7 @@ import { spawnSubagent, formatSubagentResult } from "./index.js"; export type SubagentToolConfig = { toolRegistry: Parameters[0]["toolRegistry"]; - model: string; + getModel: () => string; apiKey: string; systemPrompt?: string; }; @@ -43,7 +43,7 @@ export function createSubagentTool(config: SubagentToolConfig): ToolRegistration const result = await spawnSubagent({ task, toolRegistry: config.toolRegistry, - model: config.model, + model: config.getModel(), apiKey: config.apiKey, systemPrompt: config.systemPrompt, }); diff --git a/src/tools/edit-file.ts b/src/tools/editFileTool.ts similarity index 62% rename from src/tools/edit-file.ts rename to src/tools/editFileTool.ts index eda5894..9c043b8 100644 --- a/src/tools/edit-file.ts +++ b/src/tools/editFileTool.ts @@ -22,39 +22,54 @@ function countOccurrences( return { count, firstIndex }; } +type FileEditError = { + kind: "not_found" | "permission" | "is_dir" | "other"; + message: string; +}; + +function classifyEditError(error: unknown, filePath: string): FileEditError { + if (error instanceof Error) { + const nodeError = error as NodeJS.ErrnoException; + + if (nodeError.code === "ENOENT") { + return { kind: "not_found", message: `Error: File not found: ${filePath}` }; + } + + if (nodeError.code === "EACCES") { + return { kind: "permission", message: `Error: Permission denied: ${filePath}` }; + } + + if (nodeError.code === "EISDIR") { + return { kind: "is_dir", message: `Error: Path is a directory, not a file: ${filePath}` }; + } + + return { kind: "other", message: `Error editing file ${filePath}: ${error.message}` }; + } + + return { kind: "other", message: `Error editing file ${filePath}: ${String(error)}` }; +} + async function execute(input: Record): Promise { const filePath = input.filePath; if (typeof filePath !== "string" || filePath.length === 0) { - return { - content: "Error: filePath is required and must be a string.", - isError: true, - }; + return { content: "Error: filePath is required and must be a string.", isError: true }; } const findText = input.findText; if (typeof findText !== "string") { - return { - content: "Error: findText is required and must be a string.", - isError: true, - }; + return { content: "Error: findText is required and must be a string.", isError: true }; } if (findText.length === 0) { - return { - content: "Error: findText must not be empty.", - isError: true, - }; + return { content: "Error: findText must not be empty.", isError: true }; } const replaceText = input.replaceText; if (typeof replaceText !== "string") { - return { - content: "Error: replaceText is required and must be a string.", - isError: true, - }; + return { content: "Error: replaceText is required and must be a string.", isError: true }; } try { @@ -62,10 +77,7 @@ async function execute(input: Record): Promise { const { count, firstIndex } = countOccurrences(content, findText); if (count === 0) { - return { - content: `Error: The specified text was not found in ${filePath}.`, - isError: true, - }; + return { content: `Error: The specified text was not found in ${filePath}.`, isError: true }; } if (count > 1) { @@ -82,44 +94,10 @@ async function execute(input: Record): Promise { await writeFile(filePath, updated, "utf-8"); - return { - content: `Successfully edited ${filePath}. Replaced 1 occurrence.`, - }; + return { content: `Successfully edited ${filePath}. Replaced 1 occurrence.` }; } catch (error: unknown) { - if (error instanceof Error) { - const nodeError = error as NodeJS.ErrnoException; - - if (nodeError.code === "ENOENT") { - return { - content: `Error: File not found: ${filePath}`, - isError: true, - }; - } - - if (nodeError.code === "EACCES") { - return { - content: `Error: Permission denied: ${filePath}`, - isError: true, - }; - } - - if (nodeError.code === "EISDIR") { - return { - content: `Error: Path is a directory, not a file: ${filePath}`, - isError: true, - }; - } - - return { - content: `Error editing file ${filePath}: ${error.message}`, - isError: true, - }; - } - - return { - content: `Error editing file ${filePath}: ${String(error)}`, - isError: true, - }; + const classified = classifyEditError(error, filePath); + return { content: classified.message, isError: true }; } } diff --git a/src/tools/grep.ts b/src/tools/grep.ts index b40e15a..7e83c4d 100644 --- a/src/tools/grep.ts +++ b/src/tools/grep.ts @@ -35,6 +35,60 @@ async function searchFile( } } +async function searchDirectory( + targetPath: string, + matcher: (line: string) => boolean, + globPattern: string, +): Promise { + const files: string[] = []; + + for await (const entry of fsGlob(globPattern, { cwd: targetPath })) { + files.push(entry); + } + + const allMatches: string[] = []; + for (const file of files) { + const fullPath = join(targetPath, file); + const matches = await searchFileWithStat(fullPath, matcher); + allMatches.push(...matches); + } + + return allMatches; +} + +async function searchFileWithStat( + fullPath: string, + matcher: (line: string) => boolean, +): Promise { + try { + const fileStat = await stat(fullPath); + if (fileStat.isFile()) { + return searchFile(fullPath, matcher); + } + } catch { + // Skip files we can't stat + } + return []; +} + +function buildPathErrorResponse(targetPath: string, error: unknown): ToolResult { + if (error instanceof Error) { + const nodeError = error as NodeJS.ErrnoException; + + if (nodeError.code === "ENOENT") { + return { content: `Error: Path not found: ${targetPath}`, isError: true }; + } + + if (nodeError.code === "EACCES") { + return { content: `Error: Permission denied: ${targetPath}`, isError: true }; + } + + return { content: `Error running grep: ${error.message}`, isError: true }; + } + + return { content: `Error running grep: ${String(error)}`, isError: true }; +} + async function execute(input: Record): Promise { const pattern = input.pattern; @@ -48,19 +102,13 @@ async function execute(input: Record): Promise { const searchPath = input.path; if (searchPath !== undefined && typeof searchPath !== "string") { - return { - content: "Error: path must be a string.", - isError: true, - }; + return { content: "Error: path must be a string.", isError: true }; } const include = input.include; if (include !== undefined && typeof include !== "string") { - return { - content: "Error: include must be a string.", - isError: true, - }; + return { content: "Error: include must be a string.", isError: true }; } const matcher = buildMatcher(pattern); @@ -69,74 +117,29 @@ async function execute(input: Record): Promise { try { const pathStat = await stat(targetPath); - const allMatches: string[] = []; if (pathStat.isFile()) { const matches = await searchFile(targetPath, matcher); - allMatches.push(...matches); - } else if (pathStat.isDirectory()) { - const globPattern = - typeof include === "string" ? include : "**/*"; - - const files: string[] = []; - - for await (const entry of fsGlob(globPattern, { cwd: targetPath })) { - files.push(entry); - } + return matches.length > 0 + ? { content: matches.join("\n") } + : { content: "No matches found." }; + } - for (const file of files) { - const fullPath = join(targetPath, file); - - try { - const fileStat = await stat(fullPath); - if (fileStat.isFile()) { - const matches = await searchFile(fullPath, matcher); - allMatches.push(...matches); - } - } catch { - // Skip files we can't stat - } - } - } else { + if (!pathStat.isDirectory()) { return { content: `Error: Path is neither a file nor a directory: ${targetPath}`, isError: true, }; } - if (allMatches.length === 0) { - return { content: "No matches found." }; - } + const globPattern = typeof include === "string" ? include : "**/*"; + const allMatches = await searchDirectory(targetPath, matcher, globPattern); - return { content: allMatches.join("\n") }; + return allMatches.length > 0 + ? { content: allMatches.join("\n") } + : { content: "No matches found." }; } catch (error: unknown) { - if (error instanceof Error) { - const nodeError = error as NodeJS.ErrnoException; - - if (nodeError.code === "ENOENT") { - return { - content: `Error: Path not found: ${targetPath}`, - isError: true, - }; - } - - if (nodeError.code === "EACCES") { - return { - content: `Error: Permission denied: ${targetPath}`, - isError: true, - }; - } - - return { - content: `Error running grep: ${error.message}`, - isError: true, - }; - } - - return { - content: `Error running grep: ${String(error)}`, - isError: true, - }; + return buildPathErrorResponse(targetPath, error); } } diff --git a/src/tools/index.ts b/src/tools/index.ts index a9c06e9..c04b9b3 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,10 +1,10 @@ import type { ToolDefinition } from "../api/anthropic.js"; import { bashTool } from "./bash.js"; -import { editFileTool } from "./edit-file.js"; +import { editFileTool } from "./editFileTool.js"; import { globTool } from "./glob.js"; import { grepTool } from "./grep.js"; -import { readFileTool } from "./read-file.js"; -import { writeFileTool } from "./write-file.js"; +import { readFileTool } from "./readFileTool.js"; +import { writeFileTool } from "./writeFileTool.js"; export type { ToolDefinition }; diff --git a/src/tools/read-file.ts b/src/tools/readFileTool.ts similarity index 51% rename from src/tools/read-file.ts rename to src/tools/readFileTool.ts index 4e02bf5..e4cf3ae 100644 --- a/src/tools/read-file.ts +++ b/src/tools/readFileTool.ts @@ -2,48 +2,40 @@ import { readFile } from "node:fs/promises"; import type { ToolRegistration, ToolResult } from "./index.js"; +function classifyReadError(error: unknown, filePath: string): ToolResult { + if (error instanceof Error) { + const nodeError = error as NodeJS.ErrnoException; + + if (nodeError.code === "ENOENT") { + return { content: `Error: File not found: ${filePath}`, isError: true }; + } + + if (nodeError.code === "EACCES") { + return { content: `Error: Permission denied: ${filePath}`, isError: true }; + } + + if (nodeError.code === "EISDIR") { + return { content: `Error: Path is a directory, not a file: ${filePath}`, isError: true }; + } + + return { content: `Error reading file ${filePath}: ${error.message}`, isError: true }; + } + + return { content: `Error reading file ${filePath}: ${String(error)}`, isError: true }; +} + async function execute(input: Record): Promise { const filePath = input.filePath; if (typeof filePath !== "string" || filePath.length === 0) { - return { - content: "Error: filePath is required and must be a string.", - isError: true, - }; + return { content: "Error: filePath is required and must be a string.", isError: true }; } try { const content = await readFile(filePath, "utf-8"); return { content }; } catch (error: unknown) { - if (error instanceof Error) { - const nodeError = error as NodeJS.ErrnoException; - - if (nodeError.code === "ENOENT") { - return { content: `Error: File not found: ${filePath}`, isError: true }; - } - - if (nodeError.code === "EACCES") { - return { content: `Error: Permission denied: ${filePath}`, isError: true }; - } - - if (nodeError.code === "EISDIR") { - return { - content: `Error: Path is a directory, not a file: ${filePath}`, - isError: true, - }; - } - - return { - content: `Error reading file ${filePath}: ${error.message}`, - isError: true, - }; - } - - return { - content: `Error reading file ${filePath}: ${String(error)}`, - isError: true, - }; + return classifyReadError(error, filePath); } } diff --git a/src/tools/write-file.ts b/src/tools/writeFileTool.ts similarity index 60% rename from src/tools/write-file.ts rename to src/tools/writeFileTool.ts index f9a7ef7..0ef735b 100644 --- a/src/tools/write-file.ts +++ b/src/tools/writeFileTool.ts @@ -3,23 +3,35 @@ import { dirname } from "node:path"; import type { ToolRegistration, ToolResult } from "./index.js"; +function classifyWriteError(error: unknown, filePath: string): ToolResult { + if (error instanceof Error) { + const nodeError = error as NodeJS.ErrnoException; + + if (nodeError.code === "EACCES") { + return { content: `Error: Permission denied: ${filePath}`, isError: true }; + } + + if (nodeError.code === "EISDIR") { + return { content: `Error: Path is a directory, not a file: ${filePath}`, isError: true }; + } + + return { content: `Error writing file ${filePath}: ${error.message}`, isError: true }; + } + + return { content: `Error writing file ${filePath}: ${String(error)}`, isError: true }; +} + async function execute(input: Record): Promise { const filePath = input.filePath; if (typeof filePath !== "string" || filePath.length === 0) { - return { - content: "Error: filePath is required and must be a string.", - isError: true, - }; + return { content: "Error: filePath is required and must be a string.", isError: true }; } const fileContent = input.content; if (typeof fileContent !== "string") { - return { - content: "Error: content is required and must be a string.", - isError: true, - }; + return { content: "Error: content is required and must be a string.", isError: true }; } try { @@ -27,33 +39,7 @@ async function execute(input: Record): Promise { await writeFile(filePath, fileContent, "utf-8"); return { content: `Successfully wrote to ${filePath}` }; } catch (error: unknown) { - if (error instanceof Error) { - const nodeError = error as NodeJS.ErrnoException; - - if (nodeError.code === "EACCES") { - return { - content: `Error: Permission denied: ${filePath}`, - isError: true, - }; - } - - if (nodeError.code === "EISDIR") { - return { - content: `Error: Path is a directory, not a file: ${filePath}`, - isError: true, - }; - } - - return { - content: `Error writing file ${filePath}: ${error.message}`, - isError: true, - }; - } - - return { - content: `Error writing file ${filePath}: ${String(error)}`, - isError: true, - }; + return classifyWriteError(error, filePath); } }