From eccc19effca079ee33d25919bae26fd20c017b7a Mon Sep 17 00:00:00 2001 From: Daniel Wise Date: Sat, 21 Feb 2026 12:27:15 -0800 Subject: [PATCH 1/2] feat(ai): add bash-tool for ai assistant --- QUICKSTART.md | 2 +- README.md | 3 + package.json | 4 +- pnpm-lock.yaml | 821 +++++++++++++++++++++++++++++++- src/commands/ai.test.ts | 25 + src/commands/ai.ts | 16 +- src/lib/ai-bash-sandbox.test.ts | 50 ++ src/lib/ai-bash-sandbox.ts | 86 ++++ src/lib/ai-context.ts | 2 + 9 files changed, 995 insertions(+), 14 deletions(-) create mode 100644 src/lib/ai-bash-sandbox.test.ts create mode 100644 src/lib/ai-bash-sandbox.ts diff --git a/QUICKSTART.md b/QUICKSTART.md index de99c87..bb93879 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -253,7 +253,7 @@ dub undo | `dub continue` / `dub abort` | Resume/cancel interrupted operations | | `dub undo` | Undo last create/restack | | `dub config ai-assistant on` | Enable repo-local AI assistant | -| `dub ai ask "..."` | Ask AI assistant (streaming) | +| `dub ai ask "..."` | Ask AI assistant (streaming + repo bash tool) | | `dub history` | Show recent Dub command history | ## Next Step diff --git a/README.md b/README.md index e893fcc..7e0d0cc 100644 --- a/README.md +++ b/README.md @@ -560,6 +560,9 @@ dub ai ask "Summarize what this stack is changing" `dub ai ask` automatically includes a context packet (current branch/stack signals, git status, doctor summary, and recent Dub command history) so it can give better recovery guidance. +`dub ai ask` also exposes a `bash` tool to the model, so it can run repository commands (for example `git status`, `dub doctor`, `dub ready`) when command output is needed. +DubStack blocks clearly destructive command patterns like `rm -rf`, `git reset --hard`, and `git clean -fd` in tool execution. + Provider/key selection: - If `DUBSTACK_GEMINI_API_KEY` is set, DubStack uses direct Google provider access (`gemini-3-flash`). - Otherwise, if `DUBSTACK_AI_GATEWAY_API_KEY` is set, DubStack uses Vercel AI Gateway (`google/gemini-3-flash`). diff --git a/package.json b/package.json index 2e9ddff..cfd2daf 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,11 @@ "@ai-sdk/google": "^3.0.30", "@inquirer/search": "^4.1.3", "ai": "^6.0.97", + "bash-tool": "^1.3.15", "chalk": "^5.6.2", "commander": "^14.0.3", - "execa": "^9.6.1" + "execa": "^9.6.1", + "just-bash": "^2.10.2" }, "devDependencies": { "@biomejs/biome": "^2.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0dddb7b..4aa07c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: ai: specifier: ^6.0.97 version: 6.0.97(zod@4.3.6) + bash-tool: + specifier: ^1.3.15 + version: 1.3.15(ai@6.0.97(zod@4.3.6))(just-bash@2.10.2) chalk: specifier: ^5.6.2 version: 5.6.2 @@ -26,6 +29,9 @@ importers: execa: specifier: ^9.6.1 version: 9.6.1 + just-bash: + specifier: ^2.10.2 + version: 2.10.2 devDependencies: '@biomejs/biome': specifier: ^2.4.2 @@ -35,7 +41,7 @@ importers: version: 25.2.3 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.1(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -44,7 +50,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.2.3)(tsx@4.21.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) packages: @@ -127,6 +133,9 @@ packages: cpu: [x64] os: [win32] + '@borewit/text-codec@0.2.1': + resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} @@ -331,6 +340,25 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + + '@mongodb-js/zstd@7.0.0': + resolution: {integrity: sha512-mQ2s0pYYiav+tzCDR05Zptem8Ey2v8s11lri5RKGhTtL4COVCvVCk5vtyRYNT+9L8qSfyOqqefF9UtnW8mC5jA==} + engines: {node: '>= 20.19.0'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -483,6 +511,13 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -539,13 +574,54 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + amdefine@1.0.1: + resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} + engines: {node: '>=0.4.2'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + balanced-match@4.0.3: + resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} + engines: {node: 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bash-tool@1.3.15: + resolution: {integrity: sha512-rsUxbwTO1qU9LxLhsqu7d5MfoJAqWgw0+E1uDEHLxrZRcXXayZspgiqTSXwGM2yuCz6N42duMdbF95o5ZSw8lQ==} + engines: {node: '>=18'} + peerDependencies: + '@vercel/sandbox': '*' + ai: ^6.0.0 + just-bash: ^2.9.3 + peerDependenciesMeta: + '@vercel/sandbox': + optional: true + just-bash: + optional: true + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + brace-expansion@5.0.2: + resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -568,6 +644,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -576,10 +655,18 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + commander@2.8.1: + resolution: {integrity: sha512-+pJLBFVk+9ZZdlAOB5WuIElVPPth47hILFkmGym57aq8kwxsowvByvB0DHs1vQAhyMZzdcpTtF0VDKGkSDR4ZQ==} + engines: {node: '>= 0.6.x'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + compressjs@1.0.3: + resolution: {integrity: sha512-jpKJjBTretQACTGLNuvnozP1JdP2ZLrjdGdBgk/tz1VfXlUcBhhSZW6vEsuThmeot/yjvSrPQKEgfF3X2Lpi8Q==} + hasBin: true + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -600,6 +687,25 @@ packages: supports-color: optional: true + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@8.0.3: + resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} + engines: {node: '>=0.3.1'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -608,6 +714,11 @@ packages: engines: {node: '>=18'} hasBin: true + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -619,10 +730,22 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-string-truncated-width@3.0.3: resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} @@ -632,6 +755,13 @@ packages: fast-wrap-ansi@0.2.0: resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} + fast-xml-parser@5.3.7: + resolution: {integrity: sha512-JzVLro9NQv92pOM/jTCR6mHlJh2FGwtomH8ZQjhFj/R29P2Fnj38OgPJVtcvYw6SuKClhgYuwUZf5b3rd8u2mA==} + hasBin: true + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -645,9 +775,20 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} + file-type@21.3.0: + resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} + engines: {node: '>=20'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -660,10 +801,53 @@ packages: get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + graceful-readlink@1.0.1: + resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + human-signals@8.0.1: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -683,9 +867,21 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + just-bash@2.10.2: + resolution: {integrity: sha512-HvpSugoDj855tVJ+ITRI7B3YDB8Osg9yt6H1mBUaW4X3804SsoC3WOC75gY2KMKYMvxbk2O1uRL+05YeNdGuKg==} + hasBin: true + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -700,9 +896,35 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.2.2: + resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} + engines: {node: 18 || 20 || >=22} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + modern-tar@0.7.4: + resolution: {integrity: sha512-5ixBi7pY+H8z3MKExsipXPq6S/Q27KpSY0K+NnIyLQLr58mNeZVhT9TkYcqa74H52DabOyrmGLhT5D7TZ/x26Q==} + engines: {node: '>=18.0.0'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -718,6 +940,26 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.87.0: + resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} + engines: {node: '>=10'} + + node-addon-api@8.5.0: + resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} + engines: {node: ^18 || ^20 || >= 21} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-liblzma@2.2.0: + resolution: {integrity: sha512-s0KzNOWwOJJgPG6wxg6cKohnAl9Wk/oW1KrQaVzJBjQwVcUGPQCzpR46Ximygjqj/3KhOrtJXnYMp/xYAXp75g==} + engines: {node: '>=16.0.0'} + hasBin: true + npm-run-path@6.0.0: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} @@ -729,6 +971,12 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + papaparse@5.5.3: + resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -747,6 +995,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -780,10 +1032,37 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + pyodide@0.27.7: + resolution: {integrity: sha512-RUSVJlhQdfWfgO9hVHCiXoG+nVZQRS5D9FzgpLJ/VcgGBLSAKoPL8kTiOikxbHQm1kRISeWUBdulEgO26qpSRA==} + engines: {node: '>=18.0.0'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + re2js@1.2.2: + resolution: {integrity: sha512-xvy4uuynAZWg9SuHbg0lgQncOuK6wssLmbHs8L8+YRbWLKY8Pe1avaHjNaFLOjErq8Oh0HvwQRWqIOCRL7uDDw==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -795,11 +1074,30 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup@4.57.1: resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -815,6 +1113,16 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + smol-toml@1.6.0: + resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -823,21 +1131,55 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + sql.js@1.14.0: + resolution: {integrity: sha512-NXYh+kFqLiYRCNAaHD0PcbjFgXyjuolEKLMk5vRt2DgPENtF1kkNzzMlg42dUk5wIsH8MhUzsRhaUxIisoSlZQ==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-final-newline@4.0.0: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strnum@2.1.2: + resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} + + strtok3@10.3.4: + resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} + engines: {node: '>=18'} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -863,6 +1205,14 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -894,6 +1244,12 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + turndown@7.2.2: + resolution: {integrity: sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -902,6 +1258,10 @@ packages: ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -909,6 +1269,9 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -993,10 +1356,33 @@ packages: engines: {node: '>=8'} hasBin: true + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -1061,6 +1447,8 @@ snapshots: '@biomejs/cli-win32-x64@2.4.2': optional: true + '@borewit/text-codec@0.2.1': {} + '@esbuild/aix-ppc64@0.27.3': optional: true @@ -1181,6 +1569,26 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mixmark-io/domino@2.2.0': {} + + '@mongodb-js/zstd@7.0.0': + dependencies: + node-addon-api: 8.5.0 + prebuild-install: 7.1.3 + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@opentelemetry/api@1.9.0': {} '@rollup/rollup-android-arm-eabi@4.57.1': @@ -1264,6 +1672,15 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -1288,13 +1705,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0) + vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@4.0.18': dependencies: @@ -1328,10 +1745,51 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.6 + amdefine@1.0.1: {} + any-promise@1.3.0: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + assertion-error@2.0.1: {} + balanced-match@4.0.3: {} + + base64-js@1.5.1: + optional: true + + bash-tool@1.3.15(ai@6.0.97(zod@4.3.6))(just-bash@2.10.2): + dependencies: + ai: 6.0.97(zod@4.3.6) + fast-glob: 3.3.3 + gray-matter: 4.0.3 + zod: 3.25.76 + optionalDependencies: + just-bash: 2.10.2 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + brace-expansion@5.0.2: + dependencies: + balanced-match: 4.0.3 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + optional: true + bundle-require@5.1.0(esbuild@0.27.3): dependencies: esbuild: 0.27.3 @@ -1347,12 +1805,24 @@ snapshots: dependencies: readdirp: 4.1.2 + chownr@1.1.4: + optional: true + cli-width@4.1.0: {} commander@14.0.3: {} + commander@2.8.1: + dependencies: + graceful-readlink: 1.0.1 + commander@4.1.1: {} + compressjs@1.0.3: + dependencies: + amdefine: 1.0.1 + commander: 2.8.1 + confbox@0.1.8: {} consola@3.4.2: {} @@ -1367,6 +1837,24 @@ snapshots: dependencies: ms: 2.1.3 + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + optional: true + + deep-extend@0.6.0: + optional: true + + detect-libc@2.1.2: + optional: true + + diff@8.0.3: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + optional: true + es-module-lexer@1.7.0: {} esbuild@0.27.3: @@ -1398,6 +1886,8 @@ snapshots: '@esbuild/win32-ia32': 0.27.3 '@esbuild/win32-x64': 0.27.3 + esprima@4.0.1: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -1419,8 +1909,23 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expand-template@2.0.3: + optional: true + expect-type@1.3.0: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-string-truncated-width@3.0.3: {} fast-string-width@3.0.2: @@ -1431,6 +1936,14 @@ snapshots: dependencies: fast-string-width: 3.0.2 + fast-xml-parser@5.3.7: + dependencies: + strnum: 2.1.2 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -1439,12 +1952,28 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 + file-type@21.3.0: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.4 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 mlly: 1.8.0 rollup: 4.57.1 + fs-constants@1.0.0: + optional: true + fsevents@2.3.3: optional: true @@ -1457,8 +1986,44 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + github-from-package@0.0.0: + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + graceful-readlink@1.0.1: {} + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + human-signals@8.0.1: {} + ieee754@1.2.1: {} + + inherits@2.0.4: + optional: true + + ini@1.3.8: + optional: true + + ini@6.0.0: {} + + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + is-plain-obj@4.1.0: {} is-stream@4.0.1: {} @@ -1469,8 +2034,40 @@ snapshots: joycon@3.1.1: {} + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + json-schema@0.4.0: {} + just-bash@2.10.2: + dependencies: + compressjs: 1.0.3 + diff: 8.0.3 + fast-xml-parser: 5.3.7 + file-type: 21.3.0 + ini: 6.0.0 + minimatch: 10.2.2 + modern-tar: 0.7.4 + papaparse: 5.5.3 + pyodide: 0.27.7 + re2js: 1.2.2 + smol-toml: 1.6.0 + sprintf-js: 1.1.3 + sql.js: 1.14.0 + turndown: 7.2.2 + yaml: 2.8.2 + optionalDependencies: + '@mongodb-js/zstd': 7.0.0 + node-liblzma: 2.2.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + kind-of@6.0.3: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -1481,6 +2078,26 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-response@3.1.0: + optional: true + + minimatch@10.2.2: + dependencies: + brace-expansion: 5.0.2 + + minimist@1.2.8: + optional: true + + mkdirp-classic@0.5.3: + optional: true + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -1488,6 +2105,8 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 + modern-tar@0.7.4: {} + ms@2.1.3: {} mute-stream@3.0.0: {} @@ -1500,6 +2119,26 @@ snapshots: nanoid@3.3.11: {} + napi-build-utils@2.0.0: + optional: true + + node-abi@3.87.0: + dependencies: + semver: 7.7.4 + optional: true + + node-addon-api@8.5.0: + optional: true + + node-gyp-build@4.8.4: + optional: true + + node-liblzma@2.2.0: + dependencies: + node-addon-api: 8.5.0 + node-gyp-build: 4.8.4 + optional: true + npm-run-path@6.0.0: dependencies: path-key: 4.0.0 @@ -1509,6 +2148,13 @@ snapshots: obug@2.1.1: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optional: true + + papaparse@5.5.3: {} + parse-ms@4.0.0: {} path-key@3.1.1: {} @@ -1519,6 +2165,8 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.1: {} + picomatch@4.0.3: {} pirates@4.0.7: {} @@ -1529,12 +2177,13 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 - postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.21.0): + postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2): dependencies: lilconfig: 3.1.3 optionalDependencies: postcss: 8.5.6 tsx: 4.21.0 + yaml: 2.8.2 postcss@8.5.6: dependencies: @@ -1542,16 +2191,66 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.87.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + optional: true + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + optional: true + + pyodide@0.27.7: + dependencies: + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + queue-microtask@1.2.3: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + optional: true + + re2js@1.2.2: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true + readdirp@4.1.2: {} resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} + reusify@1.1.0: {} + rollup@4.57.1: dependencies: '@types/estree': 1.0.8 @@ -1583,6 +2282,21 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: + optional: true + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + semver@7.7.4: + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -1593,16 +2307,50 @@ snapshots: signal-exit@4.1.0: {} + simple-concat@1.0.1: + optional: true + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + + smol-toml@1.6.0: {} + source-map-js@1.2.1: {} source-map@0.7.6: {} + sprintf-js@1.0.3: {} + + sprintf-js@1.1.3: {} + + sql.js@1.14.0: {} + stackback@0.0.2: {} std-env@3.10.0: {} + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + strip-bom-string@1.0.0: {} + strip-final-newline@4.0.0: {} + strip-json-comments@2.0.1: + optional: true + + strnum@2.1.2: {} + + strtok3@10.3.4: + dependencies: + '@tokenizer/token': 0.3.0 + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -1613,6 +2361,23 @@ snapshots: tinyglobby: 0.2.15 ts-interface-checker: 0.1.13 + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + optional: true + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -1634,11 +2399,21 @@ snapshots: tinyrainbow@3.0.3: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.1 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + tree-kill@1.2.2: {} ts-interface-checker@0.1.13: {} - tsup@8.5.1(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3): + tsup@8.5.1(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -1649,7 +2424,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.6)(tsx@4.21.0) + postcss-load-config: 6.0.1(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) resolve-from: 5.0.0 rollup: 4.57.1 source-map: 0.7.6 @@ -1673,15 +2448,29 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + turndown@7.2.2: + dependencies: + '@mixmark-io/domino': 2.2.0 + typescript@5.9.3: {} ufo@1.6.3: {} + uint8array-extras@1.5.0: {} + undici-types@7.16.0: {} unicorn-magic@0.3.0: {} - vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0): + util-deprecate@1.0.2: + optional: true + + vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -1693,11 +2482,12 @@ snapshots: '@types/node': 25.2.3 fsevents: 2.3.3 tsx: 4.21.0 + yaml: 2.8.2 - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.2.3)(tsx@4.21.0): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.2.3)(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.2.3)(tsx@4.21.0)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -1714,7 +2504,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0) + vite: 7.3.1(@types/node@25.2.3)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -1741,6 +2531,15 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrappy@1.0.2: + optional: true + + ws@8.19.0: {} + + yaml@2.8.2: {} + yoctocolors@2.1.2: {} + zod@3.25.76: {} + zod@4.3.6: {} diff --git a/src/commands/ai.test.ts b/src/commands/ai.test.ts index aed7fdc..9cfe02b 100644 --- a/src/commands/ai.test.ts +++ b/src/commands/ai.test.ts @@ -43,6 +43,18 @@ function createOutputCapture() { }; } +function createBashToolMock() { + const bashTool = { id: 'bash-tool' } as const; + const createBashTool = vi.fn().mockResolvedValue({ + tools: { + bash: bashTool, + readFile: { id: 'read-file-tool' }, + writeFile: { id: 'write-file-tool' }, + }, + }); + return { createBashTool, bashTool }; +} + describe('askAi', () => { const fakeContext: AiContext = { generatedAt: '2026-02-21T00:00:00.000Z', @@ -74,6 +86,7 @@ describe('askAi', () => { const createGoogleGenerativeAI = vi.fn().mockReturnValue(googleModel); const createGateway = vi.fn(); const collectAiContext = vi.fn().mockResolvedValue(fakeContext); + const { createBashTool, bashTool } = createBashToolMock(); const output = createOutputCapture(); const result = await askAi('Explain this stack', dir, { @@ -83,6 +96,7 @@ describe('askAi', () => { createGoogleGenerativeAI, createGateway, collectAiContext, + createBashTool, }, }); @@ -91,6 +105,11 @@ describe('askAi', () => { }); expect(googleModel).toHaveBeenCalledWith('gemini-3-flash'); expect(createGateway).not.toHaveBeenCalled(); + expect(createBashTool).toHaveBeenCalledWith( + expect.objectContaining({ + destination: dir, + }), + ); expect(result.provider).toBe('google'); expect(output.writes.join('')).toBe('hello\n'); @@ -99,6 +118,10 @@ describe('askAi', () => { model: 'google-model', prompt: expect.stringContaining('Explain this stack'), system: expect.stringContaining('DubStack assistant'), + stopWhen: expect.any(Function), + tools: { + bash: bashTool, + }, providerOptions: { google: { thinkingConfig: { @@ -122,6 +145,7 @@ describe('askAi', () => { const gatewayModel = vi.fn().mockReturnValue('gateway-model'); const createGateway = vi.fn().mockReturnValue(gatewayModel); const collectAiContext = vi.fn().mockResolvedValue(fakeContext); + const { createBashTool } = createBashToolMock(); const output = createOutputCapture(); const result = await askAi('Explain this stack', dir, { @@ -131,6 +155,7 @@ describe('askAi', () => { createGoogleGenerativeAI, createGateway, collectAiContext, + createBashTool, }, }); diff --git a/src/commands/ai.ts b/src/commands/ai.ts index 23e697f..e7a7c8c 100644 --- a/src/commands/ai.ts +++ b/src/commands/ai.ts @@ -1,6 +1,8 @@ import { createGoogleGenerativeAI } from '@ai-sdk/google'; import type { LanguageModel } from 'ai'; -import { createGateway, streamText } from 'ai'; +import { createGateway, stepCountIs, streamText } from 'ai'; +import { createBashTool } from 'bash-tool'; +import { createLocalBashSandbox } from '../lib/ai-bash-sandbox'; import { buildAiSystemPrompt, buildAiUserPrompt, @@ -15,6 +17,7 @@ interface WritableLike { interface AskAiDependencies { streamText: typeof streamText; + createBashTool: typeof createBashTool; createGoogleGenerativeAI: typeof createGoogleGenerativeAI; createGateway: typeof createGateway; collectAiContext: typeof collectAiContext; @@ -32,6 +35,7 @@ interface AskAiResult { const DEFAULT_DEPS: AskAiDependencies = { streamText, + createBashTool, createGoogleGenerativeAI, createGateway, collectAiContext, @@ -67,11 +71,21 @@ export async function askAi( const resolved = resolveModel(deps); const context = await deps.collectAiContext(cwd); const contextPrompt = buildAiUserPrompt(normalizedPrompt, context); + const bashToolkit = await deps.createBashTool({ + destination: cwd, + sandbox: createLocalBashSandbox(cwd), + extraInstructions: + 'Safety: use bash only when command output is needed. Avoid destructive commands (for example, rm -rf, git reset --hard, git clean -fd) unless the user explicitly asks.', + }); const result = deps.streamText({ model: resolved.model, system: buildAiSystemPrompt(), prompt: contextPrompt, + stopWhen: stepCountIs(6), + tools: { + bash: bashToolkit.tools.bash, + }, providerOptions: THINKING_PROVIDER_OPTIONS, }); diff --git a/src/lib/ai-bash-sandbox.test.ts b/src/lib/ai-bash-sandbox.test.ts new file mode 100644 index 0000000..ffe6ebd --- /dev/null +++ b/src/lib/ai-bash-sandbox.test.ts @@ -0,0 +1,50 @@ +import { promises as fs } from 'node:fs'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { createTestRepo } from '../../test/helpers'; +import { createLocalBashSandbox } from './ai-bash-sandbox'; + +let dir: string; +let cleanup: () => Promise; + +beforeEach(async () => { + const repo = await createTestRepo(); + dir = repo.dir; + cleanup = repo.cleanup; +}); + +afterEach(async () => { + await cleanup(); +}); + +describe('createLocalBashSandbox', () => { + it('executes shell commands inside the repository root', async () => { + const sandbox = createLocalBashSandbox(dir); + const result = await sandbox.executeCommand('pwd'); + const realDir = await fs.realpath(dir); + + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toBe(realDir); + }); + + it('blocks clearly destructive command patterns', async () => { + const sandbox = createLocalBashSandbox(dir); + const result = await sandbox.executeCommand('rm -rf .'); + + expect(result.exitCode).toBe(2); + expect(result.stderr).toContain('blocked for safety'); + }); + + it('reads and writes files only within the repository root', async () => { + const sandbox = createLocalBashSandbox(dir); + await sandbox.writeFiles([ + { path: 'notes/a.txt', content: 'hello' }, + { path: `${dir}/notes/b.txt`, content: 'world' }, + ]); + + await expect(sandbox.readFile('notes/a.txt')).resolves.toBe('hello'); + await expect(sandbox.readFile(`${dir}/notes/b.txt`)).resolves.toBe('world'); + await expect(sandbox.readFile('../outside.txt')).rejects.toThrow( + 'outside the repository sandbox', + ); + }); +}); diff --git a/src/lib/ai-bash-sandbox.ts b/src/lib/ai-bash-sandbox.ts new file mode 100644 index 0000000..dbbbccc --- /dev/null +++ b/src/lib/ai-bash-sandbox.ts @@ -0,0 +1,86 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import type { Sandbox } from 'bash-tool'; +import { execa } from 'execa'; + +const COMMAND_TIMEOUT_MS = 60_000; +const BLOCKED_PATTERNS = [ + /\brm\s+-rf\b/, + /\bgit\s+reset\s+--hard\b/, + /\bgit\s+clean\s+-fd/, + /\bmkfs\b/, + /\bshutdown\b/, + /\breboot\b/, + /:\(\)\{:\|:&\};:/, +]; + +export function createLocalBashSandbox(cwd: string): Sandbox { + const root = path.resolve(cwd); + + return { + async executeCommand(command) { + const blockedPattern = findBlockedPattern(command); + if (blockedPattern) { + return { + stdout: '', + stderr: `Command blocked for safety by DubStack assistant policy: ${blockedPattern}`, + exitCode: 2, + }; + } + + const { stdout, stderr, exitCode } = await execa( + 'bash', + ['-lc', command], + { + cwd: root, + reject: false, + timeout: COMMAND_TIMEOUT_MS, + env: { + ...process.env, + CI: process.env.CI ?? '1', + GIT_TERMINAL_PROMPT: '0', + }, + }, + ); + + return { + stdout, + stderr, + exitCode: exitCode ?? 1, + }; + }, + + async readFile(filePath) { + const resolvedPath = resolveWithinRoot(root, filePath); + return fs.readFile(resolvedPath, 'utf8'); + }, + + async writeFiles(files) { + for (const file of files) { + const resolvedPath = resolveWithinRoot(root, file.path); + await fs.mkdir(path.dirname(resolvedPath), { recursive: true }); + await fs.writeFile(resolvedPath, file.content); + } + }, + }; +} + +function resolveWithinRoot(root: string, inputPath: string): string { + const resolved = path.resolve(root, inputPath); + const relative = path.relative(root, resolved); + + if (relative.startsWith('..') || path.isAbsolute(relative)) { + throw new Error(`Path is outside the repository sandbox: ${inputPath}`); + } + + return resolved; +} + +function findBlockedPattern(command: string): string | null { + for (const pattern of BLOCKED_PATTERNS) { + if (pattern.test(command)) { + return pattern.source; + } + } + return null; +} diff --git a/src/lib/ai-context.ts b/src/lib/ai-context.ts index dfc2a5e..60dfd0d 100644 --- a/src/lib/ai-context.ts +++ b/src/lib/ai-context.ts @@ -77,6 +77,8 @@ export function buildAiSystemPrompt(): string { return [ 'You are the DubStack assistant for a local git-stack CLI.', 'Prioritize safe, concrete, minimal-step guidance.', + 'When command output is needed, use the available bash tool to inspect the repository directly.', + 'Ask for explicit user confirmation before mutating git history, deleting files, or making other destructive changes.', "When recovery is needed, prefer DubStack commands like 'dub doctor', 'dub ready', 'dub continue', 'dub abort', 'dub sync', 'dub restack', and 'dub undo'.", 'Use the provided context packet as the source of truth and call out uncertainty if context is incomplete.', 'Never invent branch names, command output, or repo state not present in context.', From 37f22be157275777a864a9cd64ddd0c2084cda3c Mon Sep 17 00:00:00 2001 From: Daniel Wise Date: Sat, 21 Feb 2026 13:40:49 -0800 Subject: [PATCH 2/2] fix(ai): harden bash sandbox and align docs --- QUICKSTART.md | 2 +- README.md | 4 +- src/commands/ai.ts | 2 +- src/lib/ai-bash-sandbox.test.ts | 19 ++++ src/lib/ai-bash-sandbox.ts | 188 +++++++++++++++++++++++++------- src/lib/ai-context.ts | 1 + 6 files changed, 173 insertions(+), 43 deletions(-) diff --git a/QUICKSTART.md b/QUICKSTART.md index bb93879..91286e7 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -253,7 +253,7 @@ dub undo | `dub continue` / `dub abort` | Resume/cancel interrupted operations | | `dub undo` | Undo last create/restack | | `dub config ai-assistant on` | Enable repo-local AI assistant | -| `dub ai ask "..."` | Ask AI assistant (streaming + repo bash tool) | +| `dub ai ask "..."` | Ask AI assistant (streaming + constrained read-only repo shell tool) | | `dub history` | Show recent Dub command history | ## Next Step diff --git a/README.md b/README.md index 7e0d0cc..f3a0a67 100644 --- a/README.md +++ b/README.md @@ -560,8 +560,8 @@ dub ai ask "Summarize what this stack is changing" `dub ai ask` automatically includes a context packet (current branch/stack signals, git status, doctor summary, and recent Dub command history) so it can give better recovery guidance. -`dub ai ask` also exposes a `bash` tool to the model, so it can run repository commands (for example `git status`, `dub doctor`, `dub ready`) when command output is needed. -DubStack blocks clearly destructive command patterns like `rm -rf`, `git reset --hard`, and `git clean -fd` in tool execution. +To inspect your repository, `dub ai ask` can invoke a constrained shell tool limited to a strict allow-list of safe, read-only commands (for example `git status`, `dub doctor`, `dub ready`) when command output is needed. +The assistant cannot execute arbitrary shell commands; requests outside this allow-list are rejected, and additional safety checks block destructive command patterns. Provider/key selection: - If `DUBSTACK_GEMINI_API_KEY` is set, DubStack uses direct Google provider access (`gemini-3-flash`). diff --git a/src/commands/ai.ts b/src/commands/ai.ts index e7a7c8c..fa2fdbb 100644 --- a/src/commands/ai.ts +++ b/src/commands/ai.ts @@ -75,7 +75,7 @@ export async function askAi( destination: cwd, sandbox: createLocalBashSandbox(cwd), extraInstructions: - 'Safety: use bash only when command output is needed. Avoid destructive commands (for example, rm -rf, git reset --hard, git clean -fd) unless the user explicitly asks.', + 'Safety: use bash only when command output is needed. Do not run destructive commands (for example, rm -rf, git reset --hard, git clean -fd), even if the user explicitly asks. This sandbox only allows read-only command families. If the user insists on blocked actions, explain the command is blocked here and provide a manual command they can run themselves at their own risk.', }); const result = deps.streamText({ diff --git a/src/lib/ai-bash-sandbox.test.ts b/src/lib/ai-bash-sandbox.test.ts index ffe6ebd..a455a08 100644 --- a/src/lib/ai-bash-sandbox.test.ts +++ b/src/lib/ai-bash-sandbox.test.ts @@ -32,6 +32,23 @@ describe('createLocalBashSandbox', () => { expect(result.exitCode).toBe(2); expect(result.stderr).toContain('blocked for safety'); + expect(result.stderr).toContain('rm -rf'); + }); + + it('blocks commands outside the read-only allow-list', async () => { + const sandbox = createLocalBashSandbox(dir); + const result = await sandbox.executeCommand('node -v'); + + expect(result.exitCode).toBe(2); + expect(result.stderr).toContain('allow-listed commands'); + }); + + it('blocks shell operator chaining', async () => { + const sandbox = createLocalBashSandbox(dir); + const result = await sandbox.executeCommand('git status && ls'); + + expect(result.exitCode).toBe(2); + expect(result.stderr).toContain("Shell operator '&&'"); }); it('reads and writes files only within the repository root', async () => { @@ -39,10 +56,12 @@ describe('createLocalBashSandbox', () => { await sandbox.writeFiles([ { path: 'notes/a.txt', content: 'hello' }, { path: `${dir}/notes/b.txt`, content: 'world' }, + { path: '..safe/inside.txt', content: 'ok' }, ]); await expect(sandbox.readFile('notes/a.txt')).resolves.toBe('hello'); await expect(sandbox.readFile(`${dir}/notes/b.txt`)).resolves.toBe('world'); + await expect(sandbox.readFile('..safe/inside.txt')).resolves.toBe('ok'); await expect(sandbox.readFile('../outside.txt')).rejects.toThrow( 'outside the repository sandbox', ); diff --git a/src/lib/ai-bash-sandbox.ts b/src/lib/ai-bash-sandbox.ts index dbbbccc..fbfaf91 100644 --- a/src/lib/ai-bash-sandbox.ts +++ b/src/lib/ai-bash-sandbox.ts @@ -1,17 +1,47 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; -import type { Sandbox } from 'bash-tool'; +import type { CommandResult, Sandbox } from 'bash-tool'; import { execa } from 'execa'; const COMMAND_TIMEOUT_MS = 60_000; -const BLOCKED_PATTERNS = [ - /\brm\s+-rf\b/, - /\bgit\s+reset\s+--hard\b/, - /\bgit\s+clean\s+-fd/, - /\bmkfs\b/, - /\bshutdown\b/, - /\breboot\b/, - /:\(\)\{:\|:&\};:/, +const SAFE_COMMANDS = new Set([ + 'pwd', + 'ls', + 'find', + 'cat', + 'head', + 'tail', + 'wc', + 'grep', + 'rg', + 'sed', +]); +const SAFE_GIT_SUBCOMMANDS = new Set([ + 'status', + 'branch', + 'log', + 'show', + 'diff', + 'rev-parse', + 'symbolic-ref', + 'for-each-ref', + 'remote', +]); +const SAFE_DUB_SUBCOMMANDS = new Set(['doctor', 'ready', 'log', 'history']); +const BLOCKED_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [ + { pattern: /\brm\s+-rf\b/, reason: 'file deletion command family (rm -rf)' }, + { + pattern: /\bgit\s+reset\s+--hard\b/, + reason: 'destructive git history reset (git reset --hard)', + }, + { + pattern: /\bgit\s+clean\s+-fd/, + reason: 'destructive untracked file cleanup (git clean -fd)', + }, + { pattern: /\bmkfs\b/, reason: 'filesystem formatting command (mkfs)' }, + { pattern: /\bshutdown\b/, reason: 'system shutdown command (shutdown)' }, + { pattern: /\breboot\b/, reason: 'system reboot command (reboot)' }, + { pattern: /:\(\)\{:\|:&\};:/, reason: 'fork bomb pattern' }, ]; export function createLocalBashSandbox(cwd: string): Sandbox { @@ -19,35 +49,54 @@ export function createLocalBashSandbox(cwd: string): Sandbox { return { async executeCommand(command) { - const blockedPattern = findBlockedPattern(command); - if (blockedPattern) { + const policyViolation = validateCommandPolicy(command); + if (policyViolation) { + return blockedResult(policyViolation); + } + + try { + const { stdout, stderr, exitCode } = await execa( + 'bash', + ['-lc', command], + { + cwd: root, + reject: false, + timeout: COMMAND_TIMEOUT_MS, + env: { + ...process.env, + CI: process.env.CI ?? '1', + GIT_TERMINAL_PROMPT: '0', + }, + }, + ); + + return { + stdout, + stderr, + exitCode: exitCode ?? 1, + }; + } catch (error) { + const execaError = error as + | { + message?: string; + shortMessage?: string; + timedOut?: boolean; + } + | undefined; + const timedOut = Boolean(execaError?.timedOut); + const message = + execaError?.shortMessage ?? + execaError?.message ?? + 'Failed to execute command'; + return { stdout: '', - stderr: `Command blocked for safety by DubStack assistant policy: ${blockedPattern}`, - exitCode: 2, + stderr: timedOut + ? `Command execution timed out after ${COMMAND_TIMEOUT_MS}ms: ${message}` + : `Command execution failed: ${message}`, + exitCode: timedOut ? 124 : 1, }; } - - const { stdout, stderr, exitCode } = await execa( - 'bash', - ['-lc', command], - { - cwd: root, - reject: false, - timeout: COMMAND_TIMEOUT_MS, - env: { - ...process.env, - CI: process.env.CI ?? '1', - GIT_TERMINAL_PROMPT: '0', - }, - }, - ); - - return { - stdout, - stderr, - exitCode: exitCode ?? 1, - }; }, async readFile(filePath) { @@ -69,18 +118,79 @@ function resolveWithinRoot(root: string, inputPath: string): string { const resolved = path.resolve(root, inputPath); const relative = path.relative(root, resolved); - if (relative.startsWith('..') || path.isAbsolute(relative)) { + if ( + relative === '..' || + relative.startsWith(`..${path.sep}`) || + path.isAbsolute(relative) + ) { throw new Error(`Path is outside the repository sandbox: ${inputPath}`); } return resolved; } -function findBlockedPattern(command: string): string | null { - for (const pattern of BLOCKED_PATTERNS) { - if (pattern.test(command)) { - return pattern.source; +function validateCommandPolicy(command: string): string | null { + const trimmed = command.trim(); + if (trimmed.length === 0) { + return 'Empty commands are not allowed.'; + } + + const disallowedOperator = findDisallowedShellOperator(trimmed); + if (disallowedOperator) { + return `Shell operator '${disallowedOperator}' is not allowed in this sandbox.`; + } + + const blockedPattern = findBlockedPattern(trimmed); + if (blockedPattern) { + return `Blocked command pattern detected: ${blockedPattern.reason}.`; + } + + if (!isAllowlistedCommand(trimmed)) { + return "Only read-only allow-listed commands are supported: pwd, ls, find, cat, head, tail, wc, grep, rg, sed, 'git status|branch|log|show|diff|rev-parse|symbolic-ref|for-each-ref|remote', and 'dub doctor|ready|log|history'."; + } + + return null; +} + +function findDisallowedShellOperator(command: string): string | null { + if (command.includes('&&')) return '&&'; + if (command.includes('||')) return '||'; + if (command.includes('`')) return '`'; + if (command.includes('$(')) return '$('; + if (command.includes(';')) return ';'; + if (command.includes('|')) return '|'; + if (command.includes('>')) return '>'; + if (command.includes('<')) return '<'; + if (command.includes('\n') || command.includes('\r')) return 'newline'; + return null; +} + +function isAllowlistedCommand(command: string): boolean { + const [executable, subcommand] = command.split(/\s+/, 3); + if (!executable) return false; + if (SAFE_COMMANDS.has(executable)) return true; + if (executable === 'git') { + return Boolean(subcommand && SAFE_GIT_SUBCOMMANDS.has(subcommand)); + } + if (executable === 'dub') { + return Boolean(subcommand && SAFE_DUB_SUBCOMMANDS.has(subcommand)); + } + return false; +} + +function findBlockedPattern(command: string): { reason: string } | null { + for (const entry of BLOCKED_PATTERNS) { + if (entry.pattern.test(command)) { + return entry; } } return null; } + +function blockedResult(reason: string): CommandResult { + return { + stdout: '', + stderr: `Command blocked for safety by DubStack assistant policy: ${reason}`, + exitCode: 2, + }; +} diff --git a/src/lib/ai-context.ts b/src/lib/ai-context.ts index 60dfd0d..195da49 100644 --- a/src/lib/ai-context.ts +++ b/src/lib/ai-context.ts @@ -78,6 +78,7 @@ export function buildAiSystemPrompt(): string { 'You are the DubStack assistant for a local git-stack CLI.', 'Prioritize safe, concrete, minimal-step guidance.', 'When command output is needed, use the available bash tool to inspect the repository directly.', + 'The bash tool is restricted to read-only allow-listed commands; do not imply it can run arbitrary shell commands.', 'Ask for explicit user confirmation before mutating git history, deleting files, or making other destructive changes.', "When recovery is needed, prefer DubStack commands like 'dub doctor', 'dub ready', 'dub continue', 'dub abort', 'dub sync', 'dub restack', and 'dub undo'.", 'Use the provided context packet as the source of truth and call out uncertainty if context is incomplete.',