diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 9fa4104..ba642ce 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -72,8 +72,8 @@ Only update the `Status` field — do not modify any other frontmatter or prompt ## Current State -Branch: `feature/mcp-exec-bump-preview4` -In-progress: PR #106 open, auto-merge enabled. `1.0.0-alpha.69` pending release after merge. +Branch: `feature/exec-permissions` +In-progress: `execPermissions` structured permission config committed (9973750). PR not yet created. SC testing locally before PR. diff --git a/.claude/sessions/2026-03-24.md b/.claude/sessions/2026-03-24.md new file mode 100644 index 0000000..cff865e --- /dev/null +++ b/.claude/sessions/2026-03-24.md @@ -0,0 +1,7 @@ +### 21:00 - feature/exec-permissions + +- Did: Implemented `execPermissions` structured permission config replacing glob-only `execAutoApprove`. Added `matchRules` (program matching by basename/path/glob/~ expansion, AND-logic args filtering), `isExecPermitted` orchestration (resolves paths, collects rules from presets + approve, checks all commands), "defaults" preset (`~/.claude/skills/*/scripts/*.sh`). Wired into `ClaudeCli.ts` with `execPermissions` taking precedence over `execAutoApprove`. 23 tests covering happy paths and documented edge cases. +- Files: `src/mcp/shellicar/autoApprove.ts`, `src/cli-config/schema.ts`, `src/ClaudeCli.ts`, `schema/cli-config.schema.json`, `test/execPermissions.spec.ts`, `.gitignore`, `vitest.config.ts`, `package.json`, `pnpm-lock.yaml` +- Decisions: Separated `matchRules` (pure, returns matched rules array) from `isExecPermitted` (orchestration, returns bool). Program matching branches on presence of `/` in `rule.program` to distinguish basename vs path intent. Args matching is subset membership (AND logic, positional-unaware). "defaults" preset maps to skill scripts glob. `execPermissions` takes precedence when present; falls back to `execAutoApprove` if absent. TDD: RED-GREEN-REFACTOR one test at a time. Two known limitations documented as tests with comments: (1) approve `args: ['push']` also matches `git push --force` (subset semantics); (2) positional-unaware matching means `git -c push ...` falsely matches an `args: ['push']` approve rule. Long-term mitigation for (2) is tool-aware normalization (strip known flag-value pairs like `-c ` before matching), not deny rules. +- Next: SC to test locally with `cli-config.json` and actual approve rules. Deny rules not implemented (v0). Tool-aware normalization not implemented. PR still needs to be created. +- Violations: None diff --git a/.gitignore b/.gitignore index 060b5ca..e03b9ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ dist/ +coverage/ # Claude harness — Stage 2 !.claude/*/ !.claude/**/*.md diff --git a/biome.json b/biome.json index 382af2f..c319c10 100644 --- a/biome.json +++ b/biome.json @@ -52,6 +52,7 @@ }, "style": { "noInferrableTypes": "error", + "useImportType": "error", "noNonNullAssertion": "error", "noYodaExpression": "error", "useBlockStatements": "error", diff --git a/package.json b/package.json index af6a668..2b385ac 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.77", "@js-joda/core": "^5.7.0", - "@shellicar/mcp-exec": "1.0.0-preview.4", + "@shellicar/mcp-exec": "1.0.0-preview.5", "sharp": "^0.34.5", "zod": "^4.3.6" }, @@ -55,11 +55,12 @@ "@shellicar/build-version": "^1.3.4", "@tsconfig/node24": "^24.0.4", "@types/node": "^25.4.0", + "@vitest/coverage-v8": "^4.1.1", "esbuild": "^0.27.3", "knip": "^5.86.0", "lefthook": "^2.1.3", "tsx": "^4.21.0", "typescript": "^5.9.3", - "vitest": "^4.0.18" + "vitest": "^4.1.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 639ebca..9bbad94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^5.7.0 version: 5.7.0 '@shellicar/mcp-exec': - specifier: 1.0.0-preview.4 - version: 1.0.0-preview.4 + specifier: 1.0.0-preview.5 + version: 1.0.0-preview.5 sharp: specifier: ^0.34.5 version: 0.34.5 @@ -48,6 +48,9 @@ importers: '@types/node': specifier: ^25.4.0 version: 25.4.0 + '@vitest/coverage-v8': + specifier: ^4.1.1 + version: 4.1.1(vitest@4.1.1(@types/node@25.4.0)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))) esbuild: specifier: ^0.27.3 version: 0.27.3 @@ -64,8 +67,8 @@ importers: specifier: ^5.9.3 version: 5.9.3 vitest: - specifier: ^4.0.18 - version: 4.0.18(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + specifier: ^4.1.1 + version: 4.1.1(@types/node@25.4.0)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) packages: @@ -84,10 +87,31 @@ packages: zod: optional: true + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.28.6': resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@biomejs/biome@2.4.7': resolution: {integrity: sha512-vXrgcmNGZ4lpdwZSpMf1hWw1aWS6B+SyeSYKTLrNsiUsAdSRN0J4d/7mF3ogJFbIwFFSOL3wT92Zzxia/d5/ng==} engines: {node: '>=14.21.3'} @@ -160,156 +184,312 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.27.3': resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.27.3': resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.27.3': resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.27.3': resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.27.3': resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.27.3': resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.27.3': resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.27.3': resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.27.3': resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.27.3': resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.27.3': resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.27.3': resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.27.3': resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.27.3': resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.27.3': resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.27.3': resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.27.3': resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.27.3': resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.27.3': resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.27.3': resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.27.3': resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.27.3': resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@hono/node-server@1.19.11': resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} engines: {node: '>=18.14.1'} @@ -621,141 +801,141 @@ packages: cpu: [x64] os: [win32] - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + '@rollup/rollup-android-arm-eabi@4.60.0': + resolution: {integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + '@rollup/rollup-android-arm64@4.60.0': + resolution: {integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + '@rollup/rollup-darwin-arm64@4.60.0': + resolution: {integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + '@rollup/rollup-darwin-x64@4.60.0': + resolution: {integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + '@rollup/rollup-freebsd-arm64@4.60.0': + resolution: {integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + '@rollup/rollup-freebsd-x64@4.60.0': + resolution: {integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + '@rollup/rollup-linux-arm-musleabihf@4.60.0': + resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + '@rollup/rollup-linux-arm64-gnu@4.60.0': + resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + '@rollup/rollup-linux-arm64-musl@4.60.0': + resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + '@rollup/rollup-linux-loong64-gnu@4.60.0': + resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + '@rollup/rollup-linux-loong64-musl@4.60.0': + resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + '@rollup/rollup-linux-ppc64-gnu@4.60.0': + resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + '@rollup/rollup-linux-ppc64-musl@4.60.0': + resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + '@rollup/rollup-linux-riscv64-gnu@4.60.0': + resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + '@rollup/rollup-linux-riscv64-musl@4.60.0': + resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + '@rollup/rollup-linux-s390x-gnu@4.60.0': + resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + '@rollup/rollup-linux-x64-gnu@4.60.0': + resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + '@rollup/rollup-linux-x64-musl@4.60.0': + resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + '@rollup/rollup-openbsd-x64@4.60.0': + resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + '@rollup/rollup-openharmony-arm64@4.60.0': + resolution: {integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + '@rollup/rollup-win32-arm64-msvc@4.60.0': + resolution: {integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + '@rollup/rollup-win32-ia32-msvc@4.60.0': + resolution: {integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + '@rollup/rollup-win32-x64-gnu@4.60.0': + resolution: {integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + '@rollup/rollup-win32-x64-msvc@4.60.0': + resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==} cpu: [x64] os: [win32] @@ -811,8 +991,8 @@ packages: webpack: optional: true - '@shellicar/mcp-exec@1.0.0-preview.4': - resolution: {integrity: sha512-bvEFaoM/3KJTxIy9ES0Z8MKkYfVDWwoBuu+455DOEokiDdmp8LBFFtyGaz7YwKFr5XaPRNPGB/YiqvyIDAl9kw==} + '@shellicar/mcp-exec@1.0.0-preview.5': + resolution: {integrity: sha512-5TPbK1gYRbqHt8nuN07yei0XRaipZYNYtZ1kUL+v/Nh6Aiv1LPfCxCq2T3cNI9t8W+SKGewwRRYf9M+IkiY87g==} hasBin: true '@standard-schema/spec@1.1.0': @@ -836,34 +1016,43 @@ packages: '@types/node@25.4.0': resolution: {integrity: sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==} - '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/coverage-v8@4.1.1': + resolution: {integrity: sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw==} + peerDependencies: + '@vitest/browser': 4.1.1 + vitest: 4.1.1 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.1.1': + resolution: {integrity: sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==} - '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + '@vitest/mocker@4.1.1': + resolution: {integrity: sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.0.18': - resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.1': + resolution: {integrity: sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==} - '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.1': + resolution: {integrity: sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==} - '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.1': + resolution: {integrity: sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==} - '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.1': + resolution: {integrity: sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==} - '@vitest/utils@4.0.18': - resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.1': + resolution: {integrity: sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==} accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} @@ -889,6 +1078,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-v8-to-istanbul@1.0.0: + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -921,6 +1113,9 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -973,8 +1168,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} @@ -985,6 +1180,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -1090,6 +1290,10 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1102,6 +1306,9 @@ packages: resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} engines: {node: '>=16.9.0'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -1139,6 +1346,18 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -1146,6 +1365,9 @@ packages: jose@6.2.1: resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + json-schema-to-ts@3.1.1: resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} engines: {node: '>=16'} @@ -1221,6 +1443,13 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1310,12 +1539,16 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} proxy-addr@2.0.7: @@ -1348,8 +1581,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + rollup@4.60.0: + resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1425,18 +1658,22 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} strip-json-comments@5.0.3: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} tinyglobby@0.2.15: @@ -1447,6 +1684,10 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1534,20 +1775,21 @@ packages: yaml: optional: true - vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + vitest@4.1.1: + resolution: {integrity: sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.18 - '@vitest/browser-preview': 4.0.18 - '@vitest/browser-webdriverio': 4.0.18 - '@vitest/ui': 4.0.18 + '@vitest/browser-playwright': 4.1.1 + '@vitest/browser-preview': 4.1.1 + '@vitest/browser-webdriverio': 4.1.1 + '@vitest/ui': 4.1.1 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -1623,8 +1865,23 @@ snapshots: optionalDependencies: zod: 4.3.6 + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + '@babel/runtime@7.28.6': {} + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@1.0.2': {} + '@biomejs/biome@2.4.7': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.4.7 @@ -1679,81 +1936,159 @@ snapshots: '@esbuild/aix-ppc64@0.27.3': optional: true + '@esbuild/aix-ppc64@0.27.4': + optional: true + '@esbuild/android-arm64@0.27.3': optional: true + '@esbuild/android-arm64@0.27.4': + optional: true + '@esbuild/android-arm@0.27.3': optional: true + '@esbuild/android-arm@0.27.4': + optional: true + '@esbuild/android-x64@0.27.3': optional: true + '@esbuild/android-x64@0.27.4': + optional: true + '@esbuild/darwin-arm64@0.27.3': optional: true + '@esbuild/darwin-arm64@0.27.4': + optional: true + '@esbuild/darwin-x64@0.27.3': optional: true + '@esbuild/darwin-x64@0.27.4': + optional: true + '@esbuild/freebsd-arm64@0.27.3': optional: true + '@esbuild/freebsd-arm64@0.27.4': + optional: true + '@esbuild/freebsd-x64@0.27.3': optional: true + '@esbuild/freebsd-x64@0.27.4': + optional: true + '@esbuild/linux-arm64@0.27.3': optional: true + '@esbuild/linux-arm64@0.27.4': + optional: true + '@esbuild/linux-arm@0.27.3': optional: true + '@esbuild/linux-arm@0.27.4': + optional: true + '@esbuild/linux-ia32@0.27.3': optional: true + '@esbuild/linux-ia32@0.27.4': + optional: true + '@esbuild/linux-loong64@0.27.3': optional: true + '@esbuild/linux-loong64@0.27.4': + optional: true + '@esbuild/linux-mips64el@0.27.3': optional: true + '@esbuild/linux-mips64el@0.27.4': + optional: true + '@esbuild/linux-ppc64@0.27.3': optional: true + '@esbuild/linux-ppc64@0.27.4': + optional: true + '@esbuild/linux-riscv64@0.27.3': optional: true + '@esbuild/linux-riscv64@0.27.4': + optional: true + '@esbuild/linux-s390x@0.27.3': optional: true + '@esbuild/linux-s390x@0.27.4': + optional: true + '@esbuild/linux-x64@0.27.3': optional: true + '@esbuild/linux-x64@0.27.4': + optional: true + '@esbuild/netbsd-arm64@0.27.3': optional: true + '@esbuild/netbsd-arm64@0.27.4': + optional: true + '@esbuild/netbsd-x64@0.27.3': optional: true + '@esbuild/netbsd-x64@0.27.4': + optional: true + '@esbuild/openbsd-arm64@0.27.3': optional: true + '@esbuild/openbsd-arm64@0.27.4': + optional: true + '@esbuild/openbsd-x64@0.27.3': optional: true + '@esbuild/openbsd-x64@0.27.4': + optional: true + '@esbuild/openharmony-arm64@0.27.3': optional: true + '@esbuild/openharmony-arm64@0.27.4': + optional: true + '@esbuild/sunos-x64@0.27.3': optional: true + '@esbuild/sunos-x64@0.27.4': + optional: true + '@esbuild/win32-arm64@0.27.3': optional: true + '@esbuild/win32-arm64@0.27.4': + optional: true + '@esbuild/win32-ia32@0.27.3': optional: true + '@esbuild/win32-ia32@0.27.4': + optional: true + '@esbuild/win32-x64@0.27.3': optional: true + '@esbuild/win32-x64@0.27.4': + optional: true + '@hono/node-server@1.19.11(hono@4.12.8)': dependencies: hono: 4.12.8 @@ -1978,79 +2313,79 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.19.1': optional: true - '@rollup/rollup-android-arm-eabi@4.59.0': + '@rollup/rollup-android-arm-eabi@4.60.0': optional: true - '@rollup/rollup-android-arm64@4.59.0': + '@rollup/rollup-android-arm64@4.60.0': optional: true - '@rollup/rollup-darwin-arm64@4.59.0': + '@rollup/rollup-darwin-arm64@4.60.0': optional: true - '@rollup/rollup-darwin-x64@4.59.0': + '@rollup/rollup-darwin-x64@4.60.0': optional: true - '@rollup/rollup-freebsd-arm64@4.59.0': + '@rollup/rollup-freebsd-arm64@4.60.0': optional: true - '@rollup/rollup-freebsd-x64@4.59.0': + '@rollup/rollup-freebsd-x64@4.60.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.59.0': + '@rollup/rollup-linux-arm-musleabihf@4.60.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.59.0': + '@rollup/rollup-linux-arm64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.59.0': + '@rollup/rollup-linux-arm64-musl@4.60.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.59.0': + '@rollup/rollup-linux-loong64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-loong64-musl@4.59.0': + '@rollup/rollup-linux-loong64-musl@4.60.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.59.0': + '@rollup/rollup-linux-ppc64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-ppc64-musl@4.59.0': + '@rollup/rollup-linux-ppc64-musl@4.60.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.59.0': + '@rollup/rollup-linux-riscv64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.59.0': + '@rollup/rollup-linux-riscv64-musl@4.60.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.59.0': + '@rollup/rollup-linux-s390x-gnu@4.60.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.59.0': + '@rollup/rollup-linux-x64-gnu@4.60.0': optional: true - '@rollup/rollup-linux-x64-musl@4.59.0': + '@rollup/rollup-linux-x64-musl@4.60.0': optional: true - '@rollup/rollup-openbsd-x64@4.59.0': + '@rollup/rollup-openbsd-x64@4.60.0': optional: true - '@rollup/rollup-openharmony-arm64@4.59.0': + '@rollup/rollup-openharmony-arm64@4.60.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.59.0': + '@rollup/rollup-win32-arm64-msvc@4.60.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.59.0': + '@rollup/rollup-win32-ia32-msvc@4.60.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.59.0': + '@rollup/rollup-win32-x64-gnu@4.60.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.59.0': + '@rollup/rollup-win32-x64-msvc@4.60.0': optional: true '@shellicar/build-clean@1.2.4(esbuild@0.27.3)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': @@ -2067,7 +2402,7 @@ snapshots: esbuild: 0.27.3 vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) - '@shellicar/mcp-exec@1.0.0-preview.4': + '@shellicar/mcp-exec@1.0.0-preview.5': dependencies: '@modelcontextprotocol/sdk': 1.27.1(zod@4.3.6) zod: 4.3.6 @@ -2097,44 +2432,60 @@ snapshots: dependencies: undici-types: 7.18.2 - '@vitest/expect@4.0.18': + '@vitest/coverage-v8@4.1.1(vitest@4.1.1(@types/node@25.4.0)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)))': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.1 + ast-v8-to-istanbul: 1.0.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.2 + obug: 2.1.1 + std-env: 4.0.0 + tinyrainbow: 3.0.3 + vitest: 4.1.1(@types/node@25.4.0)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + + '@vitest/expect@4.1.1': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 + '@vitest/spy': 4.1.1 + '@vitest/utils': 4.1.1 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.1.1(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@vitest/spy': 4.0.18 + '@vitest/spy': 4.1.1 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/pretty-format@4.0.18': + '@vitest/pretty-format@4.1.1': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/runner@4.0.18': + '@vitest/runner@4.1.1': dependencies: - '@vitest/utils': 4.0.18 + '@vitest/utils': 4.1.1 pathe: 2.0.3 - '@vitest/snapshot@4.0.18': + '@vitest/snapshot@4.1.1': dependencies: - '@vitest/pretty-format': 4.0.18 + '@vitest/pretty-format': 4.1.1 + '@vitest/utils': 4.1.1 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.1': {} - '@vitest/utils@4.0.18': + '@vitest/utils@4.1.1': dependencies: - '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.0.3 + '@vitest/pretty-format': 4.1.1 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 accepts@2.0.0: dependencies: @@ -2156,6 +2507,12 @@ snapshots: assertion-error@2.0.1: {} + ast-v8-to-istanbul@1.0.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -2192,6 +2549,8 @@ snapshots: content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -2229,7 +2588,7 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: dependencies: @@ -2264,6 +2623,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.3 '@esbuild/win32-x64': 0.27.3 + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + escape-html@1.0.3: {} estree-walker@3.0.3: @@ -2338,9 +2726,9 @@ snapshots: dependencies: walk-up-path: 4.0.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 fill-range@7.1.1: dependencies: @@ -2398,6 +2786,8 @@ snapshots: gopd@1.2.0: {} + has-flag@4.0.0: {} + has-symbols@1.1.0: {} hasown@2.0.2: @@ -2406,6 +2796,8 @@ snapshots: hono@4.12.8: {} + html-escaper@2.0.2: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -2436,10 +2828,25 @@ snapshots: isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jiti@2.6.1: {} jose@6.2.1: {} + js-tokens@10.0.0: {} + json-schema-to-ts@3.1.1: dependencies: '@babel/runtime': 7.28.6 @@ -2514,6 +2921,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.2: + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -2592,9 +3009,11 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + pkce-challenge@5.0.1: {} - postcss@8.5.6: + postcss@8.5.8: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -2626,35 +3045,35 @@ snapshots: reusify@1.1.0: {} - rollup@4.59.0: + rollup@4.60.0: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 + '@rollup/rollup-android-arm-eabi': 4.60.0 + '@rollup/rollup-android-arm64': 4.60.0 + '@rollup/rollup-darwin-arm64': 4.60.0 + '@rollup/rollup-darwin-x64': 4.60.0 + '@rollup/rollup-freebsd-arm64': 4.60.0 + '@rollup/rollup-freebsd-x64': 4.60.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.0 + '@rollup/rollup-linux-arm-musleabihf': 4.60.0 + '@rollup/rollup-linux-arm64-gnu': 4.60.0 + '@rollup/rollup-linux-arm64-musl': 4.60.0 + '@rollup/rollup-linux-loong64-gnu': 4.60.0 + '@rollup/rollup-linux-loong64-musl': 4.60.0 + '@rollup/rollup-linux-ppc64-gnu': 4.60.0 + '@rollup/rollup-linux-ppc64-musl': 4.60.0 + '@rollup/rollup-linux-riscv64-gnu': 4.60.0 + '@rollup/rollup-linux-riscv64-musl': 4.60.0 + '@rollup/rollup-linux-s390x-gnu': 4.60.0 + '@rollup/rollup-linux-x64-gnu': 4.60.0 + '@rollup/rollup-linux-x64-musl': 4.60.0 + '@rollup/rollup-openbsd-x64': 4.60.0 + '@rollup/rollup-openharmony-arm64': 4.60.0 + '@rollup/rollup-win32-arm64-msvc': 4.60.0 + '@rollup/rollup-win32-ia32-msvc': 4.60.0 + '@rollup/rollup-win32-x64-gnu': 4.60.0 + '@rollup/rollup-win32-x64-msvc': 4.60.0 fsevents: 2.3.3 router@2.2.0: @@ -2777,21 +3196,27 @@ snapshots: statuses@2.0.2: {} - std-env@3.10.0: {} + std-env@4.0.0: {} strip-json-comments@5.0.3: {} + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + tinybench@2.9.0: {} - tinyexec@1.0.2: {} + tinyexec@1.0.4: {} tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -2835,11 +3260,11 @@ snapshots: vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.59.0 + esbuild: 0.27.4 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.8 + rollup: 4.60.0 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 25.4.0 @@ -2848,42 +3273,32 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest@4.0.18(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.1.1(@types/node@25.4.0)(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: - '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) - '@vitest/pretty-format': 4.0.18 - '@vitest/runner': 4.0.18 - '@vitest/snapshot': 4.0.18 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - es-module-lexer: 1.7.0 + '@vitest/expect': 4.1.1 + '@vitest/mocker': 4.1.1(vite@7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.1 + '@vitest/runner': 4.1.1 + '@vitest/snapshot': 4.1.1 + '@vitest/spy': 4.1.1 + '@vitest/utils': 4.1.1 + es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 + picomatch: 4.0.4 + std-env: 4.0.0 tinybench: 2.9.0 - tinyexec: 1.0.2 + tinyexec: 1.0.4 tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 vite: 7.3.1(@types/node@25.4.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.4.0 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml walk-up-path@4.0.0: {} diff --git a/schema/cli-config.schema.json b/schema/cli-config.schema.json index 03465df..adbe2d5 100644 --- a/schema/cli-config.schema.json +++ b/schema/cli-config.schema.json @@ -197,12 +197,64 @@ }, "execAutoApprove": { "default": [], - "description": "Glob patterns for auto-approving Exec commands. Programs are resolved to absolute paths before matching. Supports $HOME expansion. Example: [\"$HOME/.claude/skills/*/scripts/*.sh\"]", + "description": "@deprecated Use execPermissions instead. Glob patterns for auto-approving Exec commands. Programs are resolved to absolute paths before matching. Supports $HOME expansion.", "type": "array", "items": { "type": "string" } }, + "execPermissions": { + "description": "Structured exec permission config. Takes precedence over execAutoApprove when present.", + "type": "object", + "properties": { + "presets": { + "default": [ + "defaults" + ], + "description": "Named permission sets. \"defaults\" includes built-in patterns for skill scripts.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "defaults" + ] + } + }, + "approve": { + "default": [], + "description": "Commands to auto-approve. Each rule specifies a program and args to match.", + "type": "array", + "items": { + "type": "object", + "properties": { + "program": { + "type": "string", + "minLength": 1, + "description": "Program name to match by basename (e.g. \"git\")." + }, + "args": { + "description": "Arguments that must ALL be present in the command (AND logic). Each entry is checked individually.", + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": [ + "program" + ], + "additionalProperties": false + } + } + }, + "required": [ + "presets", + "approve" + ], + "additionalProperties": false + }, "providers": { "default": { "git": { diff --git a/src/ClaudeCli.ts b/src/ClaudeCli.ts index 9d0a822..cebf266 100644 --- a/src/ClaudeCli.ts +++ b/src/ClaudeCli.ts @@ -21,7 +21,8 @@ import { backspace, clear, createEditor, deleteChar, deleteWord, deleteWordBackw import { discoverSkills, initFiles } from './files.js'; import { printHelp, printVersionInfo } from './help.js'; import { type KeyAction, setupKeypressHandler } from './input.js'; -import { isExecAutoApproved } from './mcp/shellicar/autoApprove.js'; +import { isExecAutoApproved } from './mcp/shellicar/isExecAutoApproved'; +import { isExecPermitted } from './mcp/shellicar/isExecPermitted'; import { PermissionManager } from './PermissionManager.js'; import { type AskQuestion, PromptManager } from './PromptManager.js'; import { detectPlatform, type Platform } from './platform.js'; @@ -836,6 +837,7 @@ export class ClaudeCli { this.session.canUseTool = (toolName, input, options) => { try { + this.term.log(`canUseTool: ${toolName}`, input); // Guard: if the query is no longer active, deny immediately. // This can happen when the SDK calls canUseTool from a task_notification // after the original query stream has ended. @@ -872,13 +874,21 @@ export class ClaudeCli { } } - // Auto-approve Exec commands matching configured patterns - if (toolName === 'mcp__shellicar__exec' && this.cliConfig.execAutoApprove.length > 0) { + // Auto-approve Exec commands: execPermissions takes precedence over execAutoApprove + if (toolName === 'mcp__shellicar__exec') { const execInput = ExecInputSchema.parse(input); - if (isExecAutoApproved(execInput, this.cliConfig.execAutoApprove, cwd)) { - const desc = (input as { description?: string }).description ?? toolName; - this.term.log(`auto-approved: ${toolName} (${desc})`); - return allow(input); + const desc = (input as { description?: string }).description ?? toolName; + + if (this.cliConfig.execPermissions) { + if (isExecPermitted(execInput, this.cliConfig.execPermissions, cwd)) { + this.term.log(`auto-approved: ${toolName} (${desc})`); + return allow(input); + } + } else if (this.cliConfig.execAutoApprove.length > 0) { + if (isExecAutoApproved(execInput, this.cliConfig.execAutoApprove, cwd)) { + this.term.log(`auto-approved: ${toolName} (${desc})`); + return allow(input); + } } } diff --git a/src/cli-config/loadCliConfig.ts b/src/cli-config/loadCliConfig.ts index c372905..15bc809 100644 --- a/src/cli-config/loadCliConfig.ts +++ b/src/cli-config/loadCliConfig.ts @@ -19,6 +19,8 @@ export function mergeRawConfigs(home: Record, local: Record globMatch(resolved, p))) { - return false; - } - } - } - - return true; -} diff --git a/src/mcp/shellicar/collectRules.ts b/src/mcp/shellicar/collectRules.ts new file mode 100644 index 0000000..0ebc637 --- /dev/null +++ b/src/mcp/shellicar/collectRules.ts @@ -0,0 +1,18 @@ +import { PRESET_RULES } from './consts'; +import type { ApproveRule, ExecPermissions } from './types'; + +export function collectRules(permissions: ExecPermissions): ApproveRule[] { + const rules: ApproveRule[] = []; + if (permissions.presets) { + for (const preset of permissions.presets) { + const presetRules = PRESET_RULES[preset]; + if (presetRules) { + rules.push(...presetRules); + } + } + } + if (permissions.approve) { + rules.push(...permissions.approve); + } + return rules; +} diff --git a/src/mcp/shellicar/consts.ts b/src/mcp/shellicar/consts.ts new file mode 100644 index 0000000..1c301ff --- /dev/null +++ b/src/mcp/shellicar/consts.ts @@ -0,0 +1,5 @@ +import type { ApproveRule } from './types'; + +export const PRESET_RULES: Record = { + defaults: [{ program: '~/.claude/skills/*/scripts/*.sh' }], +}; diff --git a/src/mcp/shellicar/escapeRegex.ts b/src/mcp/shellicar/escapeRegex.ts new file mode 100644 index 0000000..b7c27ac --- /dev/null +++ b/src/mcp/shellicar/escapeRegex.ts @@ -0,0 +1,3 @@ +export function escapeRegex(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/src/mcp/shellicar/globMatch.ts b/src/mcp/shellicar/globMatch.ts new file mode 100644 index 0000000..cede374 --- /dev/null +++ b/src/mcp/shellicar/globMatch.ts @@ -0,0 +1,12 @@ +import { match } from './match'; + +/** + * Simple glob matcher supporting * (single segment) and ** (any depth). + * Handles the subset of globs needed for path matching, with no dependencies. + */ +export function globMatch(path: string, pattern: string): boolean { + const pathParts = path.split('/').filter(Boolean); + const patParts = pattern.split('/').filter(Boolean); + + return match(pathParts, 0, patParts, 0); +} diff --git a/src/mcp/shellicar/isExecAutoApproved.ts b/src/mcp/shellicar/isExecAutoApproved.ts new file mode 100644 index 0000000..fd56fec --- /dev/null +++ b/src/mcp/shellicar/isExecAutoApproved.ts @@ -0,0 +1,30 @@ +import { resolve } from 'node:path'; +import { type ExecInput, expandPath } from '@shellicar/mcp-exec'; +import { globMatch } from './globMatch'; + +/** + * Check if all programs in an Exec tool input match at least one auto-approve pattern. + * + * Programs are resolved to absolute paths using path.resolve(cwd, program). + * Patterns support $HOME and ~ expansion, plus * and ** glob syntax. + * + * Returns true only if EVERY program in every step matches at least one pattern. + */ +export function isExecAutoApproved(input: ExecInput, patterns: string[], defaultCwd: string): boolean { + if (!patterns.length || !input.steps?.length) { + return false; + } + + const expandedPatterns = patterns.map((p) => expandPath(p)); + + for (const step of input.steps) { + for (const cmd of step.commands) { + const resolved = resolve(cmd.cwd ?? defaultCwd, expandPath(cmd.program)); + if (!expandedPatterns.some((p) => globMatch(resolved, p))) { + return false; + } + } + } + + return true; +} diff --git a/src/mcp/shellicar/isExecPermitted.ts b/src/mcp/shellicar/isExecPermitted.ts new file mode 100644 index 0000000..0780765 --- /dev/null +++ b/src/mcp/shellicar/isExecPermitted.ts @@ -0,0 +1,21 @@ +import { homedir } from 'node:os'; +import type { ExecInput } from '@shellicar/mcp-exec'; +import { collectRules } from './collectRules'; +import { matchRules } from './matchRules'; +import type { ExecPermissions } from './types'; + +/** + * Check if an Exec tool input is permitted by the structured execPermissions config. + * + * Resolves programs, collects rules from presets + approve, delegates to matchRules. + * Returns true only if EVERY command in every step is permitted by at least one rule. + */ +export function isExecPermitted(input: ExecInput, permissions: ExecPermissions, defaultCwd: string, home?: string): boolean { + const h = home ?? homedir(); + const rules = collectRules(permissions); + if (!rules.length || !input.steps?.length) { + return false; + } + + return input.steps.flatMap((s) => s.commands).every((cmd) => matchRules(cmd.program, cmd.args ?? [], rules, cmd.cwd ?? defaultCwd, h).length > 0); +} diff --git a/src/mcp/shellicar/match.ts b/src/mcp/shellicar/match.ts new file mode 100644 index 0000000..d9e46b3 --- /dev/null +++ b/src/mcp/shellicar/match.ts @@ -0,0 +1,37 @@ +import { segmentMatch } from './segmentMatch'; + +export function match(path: string[], pi: number, pat: string[], gi: number): boolean { + // Both exhausted — match + if (pi === path.length && gi === pat.length) { + return true; + } + // Pattern exhausted but path remains — no match + if (gi === pat.length) { + return false; + } + + const segment = pat[gi]; + + // ** matches zero or more path segments + if (segment === '**') { + // Try matching ** against 0, 1, 2, ... path segments + for (let skip = 0; skip <= path.length - pi; skip++) { + if (match(path, pi + skip, pat, gi + 1)) { + return true; + } + } + return false; + } + + // Path exhausted but non-** pattern remains — no match + if (pi === path.length) { + return false; + } + + // * matches any single segment, literal must match exactly + if (segment === '*' || segmentMatch(path[pi], segment)) { + return match(path, pi + 1, pat, gi + 1); + } + + return false; +} diff --git a/src/mcp/shellicar/matchRules.ts b/src/mcp/shellicar/matchRules.ts new file mode 100644 index 0000000..5f8fb2f --- /dev/null +++ b/src/mcp/shellicar/matchRules.ts @@ -0,0 +1,10 @@ +import { resolve } from 'node:path'; +import { expandPath } from '@shellicar/mcp-exec'; +import { ruleMatchesArgs } from './ruleMatchesArgs'; +import { ruleMatchesProgram } from './ruleMatchesProgram'; +import type { ApproveRule } from './types'; + +export function matchRules(program: string, commandArgs: string[], rules: ApproveRule[], cwd: string, home: string): ApproveRule[] { + const resolvedPath = resolve(cwd, expandPath(program, { home })); + return rules.filter((rule) => ruleMatchesProgram(resolvedPath, rule, home) && ruleMatchesArgs(commandArgs, rule)); +} diff --git a/src/mcp/shellicar/ruleMatchesArgs.ts b/src/mcp/shellicar/ruleMatchesArgs.ts new file mode 100644 index 0000000..b9cf8c1 --- /dev/null +++ b/src/mcp/shellicar/ruleMatchesArgs.ts @@ -0,0 +1,5 @@ +import type { ApproveRule } from './types'; + +export function ruleMatchesArgs(commandArgs: string[], rule: ApproveRule): boolean { + return !rule.args || rule.args.every((arg) => commandArgs.includes(arg)); +} diff --git a/src/mcp/shellicar/ruleMatchesProgram.ts b/src/mcp/shellicar/ruleMatchesProgram.ts new file mode 100644 index 0000000..396404c --- /dev/null +++ b/src/mcp/shellicar/ruleMatchesProgram.ts @@ -0,0 +1,19 @@ +import { basename } from 'node:path'; +import { expandPath } from '@shellicar/mcp-exec'; +import { globMatch } from './globMatch'; +import type { ApproveRule } from './types'; + +/** + * Match a resolved program path against a single approve rule. + * + * No slash in rule.program: basename match. With slash: glob path match + * (supports ~/$HOME expansion and * / ** globs). + */ +export function ruleMatchesProgram(resolvedPath: string, rule: ApproveRule, home: string): boolean { + const pattern = expandPath(rule.program, { home }); + if (pattern.includes('/')) { + return globMatch(resolvedPath, pattern); + } + const programName = basename(resolvedPath); + return programName === pattern; +} diff --git a/src/mcp/shellicar/segmentMatch.ts b/src/mcp/shellicar/segmentMatch.ts new file mode 100644 index 0000000..d1ceadc --- /dev/null +++ b/src/mcp/shellicar/segmentMatch.ts @@ -0,0 +1,15 @@ +import { escapeRegex } from './escapeRegex'; + +/** Match a path segment against a pattern segment with * wildcards (e.g. *.sh, test-*). */ +export function segmentMatch(value: string, pattern: string): boolean { + if (pattern === '*') { + return true; + } + if (!pattern.includes('*')) { + return value === pattern; + } + + // Convert segment pattern to regex: * → .*, escape the rest + const regex = new RegExp('^' + pattern.split('*').map(escapeRegex).join('.*') + '$'); + return regex.test(value); +} diff --git a/src/mcp/shellicar/types.ts b/src/mcp/shellicar/types.ts new file mode 100644 index 0000000..5db0fdb --- /dev/null +++ b/src/mcp/shellicar/types.ts @@ -0,0 +1,9 @@ +export interface ApproveRule { + program: string; + args?: string[]; +} + +export interface ExecPermissions { + presets?: string[]; + approve?: ApproveRule[]; +} diff --git a/test/autoApprove.spec.ts b/test/autoApprove.spec.ts index 9bce9cb..38a8aad 100644 --- a/test/autoApprove.spec.ts +++ b/test/autoApprove.spec.ts @@ -1,6 +1,6 @@ import { ExecInputSchema } from '@shellicar/mcp-exec'; import { describe, expect, it } from 'vitest'; -import { isExecAutoApproved } from '../src/mcp/shellicar/autoApprove'; +import { isExecAutoApproved } from '../src/mcp/shellicar/isExecAutoApproved'; const HOME = process.env.HOME ?? '/home/testuser'; diff --git a/test/cli-config.spec.ts b/test/cli-config.spec.ts index c3908ce..12ea73e 100644 --- a/test/cli-config.spec.ts +++ b/test/cli-config.spec.ts @@ -330,6 +330,75 @@ describe('validateRawConfig', () => { }); }); +describe('execPermissions merge', () => { + function mergeAndParse(home: Record, local: Record) { + return parseCliConfig(mergeRawConfigs(home, local)).execPermissions; + } + + describe('presets: last wins', () => { + it('local presets override home presets', () => { + const expected = ['defaults']; + + const actual = mergeAndParse({ execPermissions: { presets: [] } }, { execPermissions: { presets: ['defaults'] } })?.presets; + + expect(actual).toEqual(expected); + }); + + it('local empty array overrides home presets', () => { + const expected: string[] = []; + + const actual = mergeAndParse({ execPermissions: { presets: ['defaults'] } }, { execPermissions: { presets: [] } })?.presets; + + expect(actual).toEqual(expected); + }); + + it('null resets presets to default', () => { + const expected = ['defaults']; + + const actual = mergeAndParse({ execPermissions: { presets: [] } }, { execPermissions: { presets: null } })?.presets; + + expect(actual).toEqual(expected); + }); + + it('absent local presets keeps home value', () => { + const expected = ['defaults']; + + const actual = mergeAndParse({ execPermissions: { presets: ['defaults'] } }, { execPermissions: {} })?.presets; + + expect(actual).toEqual(expected); + }); + }); + + describe('approve: additive', () => { + const r1 = { program: 'git' }; + const r2 = { program: 'curl' }; + + it('local approve rules are appended to home rules', () => { + const expected = [r1, r2]; + + const actual = mergeAndParse({ execPermissions: { approve: [r1] } }, { execPermissions: { approve: [r2] } })?.approve; + + expect(actual).toEqual(expected); + }); + + it('null resets approve to default empty array', () => { + const expected: unknown[] = []; + + const actual = mergeAndParse({ execPermissions: { approve: [r1] } }, { execPermissions: { approve: null } })?.approve; + + expect(actual).toEqual(expected); + }); + + it('absent local approve keeps home rules', () => { + const expected = [r1]; + + const actual = mergeAndParse({ execPermissions: { approve: [r1] } }, { execPermissions: {} })?.approve; + + expect(actual).toEqual(expected); + }); + }); +}); + describe('diffConfig', () => { const defaults = parseCliConfig({}); diff --git a/test/execPermissions.spec.ts b/test/execPermissions.spec.ts new file mode 100644 index 0000000..d8ddd78 --- /dev/null +++ b/test/execPermissions.spec.ts @@ -0,0 +1,252 @@ +import { ExecInputSchema } from '@shellicar/mcp-exec'; +import { describe, expect, it } from 'vitest'; +import { isExecPermitted } from '../src/mcp/shellicar/isExecPermitted'; +import { matchRules } from '../src/mcp/shellicar/matchRules'; +import type { ApproveRule, ExecPermissions } from '../src/mcp/shellicar/types'; + +const HOME = '/home/testuser'; +const CWD = '/tmp'; + +function match(resolvedPath: string, commandArgs: string[], rules: ApproveRule[]) { + return matchRules(resolvedPath, commandArgs, rules, CWD, HOME); +} + +describe('matchRules', () => { + it('matches rule by program basename', () => { + const expected: ApproveRule = { program: 'git' }; + const rules: ApproveRule[] = [expected]; + + const actual = match('/usr/bin/git', [], rules); + + expect(actual).toEqual([expected]); + }); + + it('matches rule by exact absolute path', () => { + const expected: ApproveRule = { program: '/usr/bin/git' }; + const rules: ApproveRule[] = [expected]; + + const actual = match('/usr/bin/git', [], rules); + + expect(actual).toEqual([expected]); + }); + + it('expands ~ in rule program before matching', () => { + const expected: ApproveRule = { program: '~/.claude/skills/git-commit/scripts/info.sh' }; + const rules: ApproveRule[] = [expected]; + + const actual = match(`${HOME}/.claude/skills/git-commit/scripts/info.sh`, [], rules); + + expect(actual).toEqual([expected]); + }); + + it('matches glob pattern with * in rule program', () => { + const expected: ApproveRule = { program: '~/.claude/skills/*/scripts/*.sh' }; + const rules: ApproveRule[] = [expected]; + + const actual = match(`${HOME}/.claude/skills/git-commit/scripts/info.sh`, [], rules); + + expect(actual).toEqual([expected]); + }); + + it('matches when all rule args are present in command args', () => { + const expected: ApproveRule = { program: 'git', args: ['push'] }; + const rules: ApproveRule[] = [expected]; + + const actual = match('/usr/bin/git', ['push', 'origin', 'main'], rules); + + expect(actual).toEqual([expected]); + }); + + it('matches when rule has no args regardless of command args', () => { + const expected: ApproveRule = { program: 'git' }; + const rules: ApproveRule[] = [expected]; + + const actual = match('/usr/bin/git', ['push', '--force', 'origin'], rules); + + expect(actual).toEqual([expected]); + }); + + it('rejects when rule args are not all present in command args', () => { + const rules: ApproveRule[] = [{ program: 'git', args: ['push', '--force'] }]; + + const actual = match('/usr/bin/git', ['push', 'origin'], rules); + + expect(actual).toEqual([]); + }); + + // Args matching is subset membership, not exact equality. + // { args: ['push'] } matches ['push', '--force'] because 'push' is present. + // You cannot prevent force-push with approve rules alone. Deny rules are needed (not yet implemented). + it('args check is subset: approve push also matches push --force', () => { + const expected: ApproveRule = { program: 'git', args: ['push'] }; + const rules: ApproveRule[] = [expected]; + + const actual = match('/usr/bin/git', ['push', '--force', 'origin'], rules); + + expect(actual).toEqual([expected]); + }); + + // Matching is positional-unaware: 'push' as a flag value (git -c push ...) is + // indistinguishable from 'push' as a subcommand. An approve rule for + // { args: ['push'] } will match both. + // + // The long-term mitigation is tool-aware normalization (not yet implemented): + // before matching, known flag-value pairs are stripped for specific programs. + // For git, '-c ' would be removed, so 'git -c push remote remove origin' + // normalizes to args ['remote', 'remove', 'origin'] before rules are checked. + // This eliminates the ambiguity structurally rather than requiring an + // exhaustive deny list. + it('args check is positional-unaware: push as -c value falsely matches push approve rule', () => { + const expected: ApproveRule = { program: 'git', args: ['push'] }; + const rules: ApproveRule[] = [expected]; + + // git -c push remote remove origin -- 'push' is a -c flag value, not the subcommand + const actual = match('/usr/bin/git', ['-c', 'push', 'remote', 'remove', 'origin'], rules); + + expect(actual).toEqual([expected]); + }); + + it('rejects when rule requires args but command has none', () => { + const rules: ApproveRule[] = [{ program: 'git', args: ['status'] }]; + + const actual = match('/usr/bin/git', [], rules); + + expect(actual).toEqual([]); + }); + + it('rejects when program does not match any rule', () => { + const rules: ApproveRule[] = [{ program: 'git' }]; + + const actual = match('/usr/bin/curl', [], rules); + + expect(actual).toEqual([]); + }); + + it('returns multiple matching rules', () => { + const rule1: ApproveRule = { program: 'git' }; + const rule2: ApproveRule = { program: 'git', args: ['status'] }; + const rules: ApproveRule[] = [rule1, rule2]; + + const actual = match('/usr/bin/git', ['status'], rules); + + expect(actual).toEqual([rule1, rule2]); + }); +}); + +function input(steps: Array>) { + return ExecInputSchema.parse({ description: 'test', steps }); +} + +function permitted(execInput: ReturnType, permissions: ExecPermissions) { + return isExecPermitted(execInput, permissions, CWD, HOME); +} + +describe('isExecPermitted', () => { + it('permits when command matches an approve rule', () => { + const expected = true; + + const actual = permitted(input([{ commands: [{ program: '/usr/bin/git' }] }]), { + approve: [{ program: 'git' }], + }); + + expect(actual).toBe(expected); + }); + + it('denies when command does not match any approve rule', () => { + const expected = false; + + const actual = permitted(input([{ commands: [{ program: '/usr/bin/curl' }] }]), { + approve: [{ program: 'git' }], + }); + + expect(actual).toBe(expected); + }); + + it('denies when any command in a multi-step input is not permitted', () => { + const expected = false; + + const actual = permitted(input([{ commands: [{ program: '/usr/bin/git' }] }, { commands: [{ program: '/usr/bin/curl' }] }]), { approve: [{ program: 'git' }] }); + + expect(actual).toBe(expected); + }); + + it('denies when no approve rules exist', () => { + const expected = false; + + const actual = permitted(input([{ commands: [{ program: '/usr/bin/git' }] }]), {}); + + expect(actual).toBe(expected); + }); + + it('permits with args matching', () => { + const expected = true; + + const actual = permitted(input([{ commands: [{ program: '/usr/bin/git', args: ['status'] }] }]), { + approve: [{ program: 'git', args: ['status'] }], + }); + + expect(actual).toBe(expected); + }); + + it('denies when args do not match', () => { + const expected = false; + + const actual = permitted(input([{ commands: [{ program: '/usr/bin/git', args: ['push', '--force'] }] }]), { + approve: [{ program: 'git', args: ['status'] }], + }); + + expect(actual).toBe(expected); + }); + + it('permits skill scripts when "defaults" preset is active', () => { + const expected = true; + + const actual = permitted(input([{ commands: [{ program: `${HOME}/.claude/skills/git-commit/scripts/info.sh` }] }]), { presets: ['defaults'] }); + + expect(actual).toBe(expected); + }); + + it('does not permit non-skill paths with "defaults" preset', () => { + const expected = false; + + const actual = permitted(input([{ commands: [{ program: '/usr/bin/curl' }] }]), { presets: ['defaults'] }); + + expect(actual).toBe(expected); + }); + + it('combines preset and user-defined approve rules', () => { + const expected = true; + + const actual = permitted(input([{ commands: [{ program: `${HOME}/.claude/skills/git-commit/scripts/info.sh` }] }, { commands: [{ program: '/usr/bin/git', args: ['status'] }] }]), { presets: ['defaults'], approve: [{ program: 'git', args: ['status'] }] }); + + expect(actual).toBe(expected); + }); + + it('resolves relative program paths against default cwd', () => { + const expected = true; + + const actual = permitted(input([{ commands: [{ program: 'git' }] }]), { + approve: [{ program: `${CWD}/git` }], + }); + + expect(actual).toBe(expected); + }); + + it('ignores unknown preset names', () => { + const expected = false; + + const actual = permitted(input([{ commands: [{ program: '/usr/bin/git' }] }]), { + presets: ['nonexistent'], + }); + + expect(actual).toBe(expected); + }); + + it('permits skill scripts when program uses ~ prefix', () => { + const expected = true; + + const actual = permitted(input([{ commands: [{ program: '~/.claude/skills/git-commit/scripts/info.sh' }] }]), { presets: ['defaults'] }); + + expect(actual).toBe(expected); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 537e482..c106e67 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,9 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { + coverage: { + provider: 'v8', + }, include: ['test/**/*.spec.ts', 'src/**/*.test.ts'], }, });