diff --git a/eslint.config.js b/eslint.config.js
index b716b9b3..e5ded380 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -30,7 +30,11 @@ export default tseslint.config(
'node_modules/*',
'.integration-tests/**',
'eslint.config.js',
- 'packages/**/dist/**',
+ 'packages/cli/dist/**',
+ 'packages/core/dist/**',
+ 'packages/server/dist/**',
+ 'packages/test-utils/dist/**',
+ 'packages/vscode-ide-companion/dist/**',
'bundle/**',
'package/bundle/**',
'.integration-tests/**',
diff --git a/integration-tests/test-helper.ts b/integration-tests/test-helper.ts
index a4d94ba7..6ad90ae0 100644
--- a/integration-tests/test-helper.ts
+++ b/integration-tests/test-helper.ts
@@ -249,7 +249,7 @@ export class TestRig {
if (env.GEMINI_SANDBOX === 'podman') {
// Remove telemetry JSON objects from output
// They are multi-line JSON objects that start with { and contain telemetry fields
- const lines = result.split('\n');
+ const lines = result.split(EOL);
const filteredLines = [];
let inTelemetryObject = false;
let braceDepth = 0;
@@ -510,7 +510,7 @@ export class TestRig {
// If no matches found with the simple pattern, try the JSON parsing approach
// in case the format changes
if (logs.length === 0) {
- const lines = stdout.split('\n');
+ const lines = stdout.split(EOL);
let currentObject = '';
let inObject = false;
let braceDepth = 0;
diff --git a/package-lock.json b/package-lock.json
index 797a80c6..82831917 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"packages/*"
],
"dependencies": {
+ "@lvce-editor/ripgrep": "^2.1.0",
"node-fetch": "^3.3.2",
"strip-ansi": "^7.1.0"
},
@@ -1468,6 +1469,41 @@
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
"license": "MIT"
},
+ "node_modules/@lvce-editor/ripgrep": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@lvce-editor/ripgrep/-/ripgrep-2.1.0.tgz",
+ "integrity": "sha512-pZObE9Y4x9Prn1c5cmTqHMv4ezHUaYi1qex9TD7YSXtkmoXUyDpYaDUWg56GBpeMUCprtPAhw9u26n6u8AKJyA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@lvce-editor/verror": "^1.6.0",
+ "execa": "^9.5.2",
+ "extract-zip": "^2.0.1",
+ "fs-extra": "^11.3.0",
+ "got": "^14.4.7",
+ "path-exists": "^5.0.0",
+ "tempy": "^3.1.0",
+ "xdg-basedir": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@lvce-editor/ripgrep/node_modules/path-exists": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
+ "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/@lvce-editor/verror": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@lvce-editor/verror/-/verror-1.7.0.tgz",
+ "integrity": "sha512-+LGuAEIC2L7pbvkyAQVWM2Go0dAy+UWEui28g07zNtZsCBhm+gusBK8PNwLJLV5Jay+TyUYuwLIbJdjLLzqEBg==",
+ "license": "MIT"
+ },
"node_modules/@lydell/node-pty": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.1.0.tgz",
@@ -2578,6 +2614,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@sec-ant/readable-stream": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
+ "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
+ "license": "MIT"
+ },
"node_modules/@selderee/plugin-htmlparser2": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
@@ -2598,6 +2640,42 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@sindresorhus/is": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz",
+ "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
+ "node_modules/@sindresorhus/merge-streams": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
+ "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@szmarczak/http-timer": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
+ "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
+ "license": "MIT",
+ "dependencies": {
+ "defer-to-connect": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=14.16"
+ }
+ },
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
@@ -2764,6 +2842,12 @@
"integrity": "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==",
"license": "MIT"
},
+ "node_modules/@types/http-cache-semantics": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
+ "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
+ "license": "MIT"
+ },
"node_modules/@types/http-errors": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
@@ -2988,6 +3072,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz",
@@ -3922,6 +4016,15 @@
"node": ">=8"
}
},
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -3962,6 +4065,33 @@
"node": ">=8"
}
},
+ "node_modules/cacheable-lookup": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
+ "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ }
+ },
+ "node_modules/cacheable-request": {
+ "version": "12.0.1",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz",
+ "integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-cache-semantics": "^4.0.4",
+ "get-stream": "^9.0.1",
+ "http-cache-semantics": "^4.1.1",
+ "keyv": "^4.5.4",
+ "mimic-response": "^4.0.0",
+ "normalize-url": "^8.0.1",
+ "responselike": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -4512,6 +4642,33 @@
"node": ">= 8"
}
},
+ "node_modules/crypto-random-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz",
+ "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==",
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/crypto-random-string/node_modules/type-fest": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+ "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/cssstyle": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
@@ -4634,6 +4791,33 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decompress-response/node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
@@ -4697,6 +4881,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/defer-to-connect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -4962,6 +5155,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -5648,6 +5850,44 @@
"node": ">=20.0.0"
}
},
+ "node_modules/execa": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz",
+ "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/merge-streams": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "figures": "^6.1.0",
+ "get-stream": "^9.0.0",
+ "human-signals": "^8.0.1",
+ "is-plain-obj": "^4.1.0",
+ "is-stream": "^4.0.1",
+ "npm-run-path": "^6.0.0",
+ "pretty-ms": "^9.2.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^4.0.0",
+ "yoctocolors": "^2.1.1"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/execa/node_modules/is-stream": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
+ "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/expect-type": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz",
@@ -5721,6 +5961,41 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
+ "node_modules/extract-zip/node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5796,6 +6071,15 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
@@ -5958,6 +6242,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/form-data-encoder": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz",
+ "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -5994,6 +6287,29 @@
"node": ">= 0.8"
}
},
+ "node_modules/fs-extra": {
+ "version": "11.3.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz",
+ "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/fs-extra/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -6189,6 +6505,34 @@
"node": ">= 0.4"
}
},
+ "node_modules/get-stream": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
+ "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@sec-ant/readable-stream": "^0.4.1",
+ "is-stream": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-stream/node_modules/is-stream": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
+ "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-symbol-description": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
@@ -6418,6 +6762,43 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/got": {
+ "version": "14.4.7",
+ "resolved": "https://registry.npmjs.org/got/-/got-14.4.7.tgz",
+ "integrity": "sha512-DI8zV1231tqiGzOiOzQWDhsBmncFW7oQDH6Zgy6pDPrqJuVZMtoSgPLLsBZQj8Jg4JFfwoOsDA8NGtLQLnIx2g==",
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/is": "^7.0.1",
+ "@szmarczak/http-timer": "^5.0.1",
+ "cacheable-lookup": "^7.0.0",
+ "cacheable-request": "^12.0.1",
+ "decompress-response": "^6.0.0",
+ "form-data-encoder": "^4.0.2",
+ "http2-wrapper": "^2.2.1",
+ "lowercase-keys": "^3.0.0",
+ "p-cancelable": "^4.0.1",
+ "responselike": "^3.0.0",
+ "type-fest": "^4.26.1"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/got?sponsor=1"
+ }
+ },
+ "node_modules/got/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -6699,6 +7080,12 @@
"entities": "^4.4.0"
}
},
+ "node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -6738,6 +7125,19 @@
"node": ">= 14"
}
},
+ "node_modules/http2-wrapper": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz",
+ "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
@@ -6751,6 +7151,15 @@
"node": ">= 14"
}
},
+ "node_modules/human-signals": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
+ "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
"node_modules/hyperdyperid": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
@@ -7554,6 +7963,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -7949,7 +8370,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/json-parse-better-errors": {
@@ -7985,6 +8405,27 @@
"json5": "lib/cli.js"
}
},
+ "node_modules/jsonfile": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonfile/node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -8026,7 +8467,6 @@
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
@@ -8191,6 +8631,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lowercase-keys": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
+ "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/lowlight": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
@@ -8398,6 +8850,18 @@
"node": ">=6"
}
},
+ "node_modules/mimic-response": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
+ "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -8659,6 +9123,18 @@
"node": ">=10"
}
},
+ "node_modules/normalize-url": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.2.tgz",
+ "integrity": "sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/npm-run-all": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
@@ -8871,6 +9347,46 @@
"which": "bin/which"
}
},
+ "node_modules/npm-run-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
+ "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^4.0.0",
+ "unicorn-magic": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/unicorn-magic": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/nwsapi": {
"version": "2.2.20",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
@@ -9102,6 +9618,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/p-cancelable": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz",
+ "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ }
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -9212,6 +9737,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parse-ms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
+ "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -9358,6 +9895,12 @@
"url": "https://ko-fi.com/killymxi"
}
},
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "license": "MIT"
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -9508,6 +10051,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/pretty-ms": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz",
+ "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==",
+ "license": "MIT",
+ "dependencies": {
+ "parse-ms": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -9575,6 +10133,16 @@
"url": "https://github.com/sponsors/lupomontero"
}
},
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -9642,6 +10210,18 @@
],
"license": "MIT"
},
+ "node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -9972,6 +10552,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/resolve-alpn": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+ "license": "MIT"
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -9992,6 +10578,21 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
+ "node_modules/responselike": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
+ "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
+ "license": "MIT",
+ "dependencies": {
+ "lowercase-keys": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/restore-cursor": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
@@ -10860,6 +11461,18 @@
"node": ">=4"
}
},
+ "node_modules/strip-final-newline": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
+ "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -10949,6 +11562,45 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/temp-dir": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz",
+ "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ }
+ },
+ "node_modules/tempy": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz",
+ "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==",
+ "license": "MIT",
+ "dependencies": {
+ "is-stream": "^3.0.0",
+ "temp-dir": "^3.0.0",
+ "type-fest": "^2.12.2",
+ "unique-string": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/tempy/node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/terminal-link": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-3.0.0.tgz",
@@ -11510,6 +12162,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/unique-string": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz",
+ "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "crypto-random-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
@@ -12424,6 +13091,16 @@
"node": ">=8"
}
},
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -12437,6 +13114,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/yoctocolors": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
+ "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/yoctocolors-cjs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz",
diff --git a/package.json b/package.json
index 6fcda594..92386a60 100644
--- a/package.json
+++ b/package.json
@@ -91,6 +91,7 @@
"yargs": "^17.7.2"
},
"dependencies": {
+ "@lvce-editor/ripgrep": "^2.1.0",
"node-fetch": "^3.3.2",
"strip-ansi": "^7.1.0"
},
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts
index c9426379..917c7446 100644
--- a/packages/cli/src/config/config.test.ts
+++ b/packages/cli/src/config/config.test.ts
@@ -1562,6 +1562,46 @@ describe('loadCliConfig chatCompression', () => {
});
});
+describe('loadCliConfig useRipgrep', () => {
+ const originalArgv = process.argv;
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
+ vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
+ });
+
+ afterEach(() => {
+ process.argv = originalArgv;
+ vi.unstubAllEnvs();
+ vi.restoreAllMocks();
+ });
+
+ it('should be false by default when useRipgrep is not set in settings', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = {};
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getUseRipgrep()).toBe(false);
+ });
+
+ it('should be true when useRipgrep is set to true in settings', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = { useRipgrep: true };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getUseRipgrep()).toBe(true);
+ });
+
+ it('should be false when useRipgrep is explicitly set to false in settings', async () => {
+ process.argv = ['node', 'script.js'];
+ const argv = await parseArguments();
+ const settings: Settings = { useRipgrep: false };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+ expect(config.getUseRipgrep()).toBe(false);
+ });
+});
+
describe('loadCliConfig tool exclusions', () => {
const originalArgv = process.argv;
const originalIsTTY = process.stdin.isTTY;
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index aaaf293d..498d0255 100644
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -552,6 +552,7 @@ export async function loadCliConfig(
folderTrust,
interactive,
trustedFolder,
+ useRipgrep: settings.useRipgrep,
shouldUseNodePtyShell: settings.shouldUseNodePtyShell,
skipNextSpeakerCheck: settings.skipNextSpeakerCheck,
enablePromptCompletion: settings.enablePromptCompletion ?? false,
diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts
index 43bf52c9..4c247e65 100644
--- a/packages/cli/src/config/settingsSchema.test.ts
+++ b/packages/cli/src/config/settingsSchema.test.ts
@@ -52,6 +52,7 @@ describe('SettingsSchema', () => {
'model',
'hasSeenIdeIntegrationNudge',
'folderTrustFeature',
+ 'useRipgrep',
];
expectedSettings.forEach((setting) => {
diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts
index 5f939b56..d4391cb3 100644
--- a/packages/cli/src/config/settingsSchema.ts
+++ b/packages/cli/src/config/settingsSchema.ts
@@ -534,6 +534,16 @@ export const SETTINGS_SCHEMA = {
description: 'Skip the next speaker check.',
showInDialog: true,
},
+ useRipgrep: {
+ type: 'boolean',
+ label: 'Use Ripgrep',
+ category: 'Tools',
+ requiresRestart: false,
+ default: false,
+ description:
+ 'Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance.',
+ showInDialog: true,
+ },
enablePromptCompletion: {
type: 'boolean',
label: 'Enable Prompt Completion',
diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx
index 9629b94b..a3c745cc 100644
--- a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx
+++ b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx
@@ -9,6 +9,7 @@ import { render } from 'ink-testing-library';
import { DiffRenderer } from './DiffRenderer.js';
import * as CodeColorizer from '../../utils/CodeColorizer.js';
import { vi } from 'vitest';
+import { EOL } from 'os';
describe('', () => {
const mockColorizeCode = vi.spyOn(CodeColorizer, 'colorizeCode');
@@ -29,7 +30,7 @@ index 0000000..e69de29
+++ b/test.py
@@ -0,0 +1 @@
+print("hello world")
-`;
+`.replace(/\n/g, EOL);
render(
@@ -109,7 +110,7 @@ index 0000001..0000002 100644
@@ -1 +1 @@
-old line
+new line
-`;
+`.replace(/\n/g, EOL);
const { lastFrame } = render(
', () => {
const baseProps = {
@@ -54,7 +55,7 @@ describe('', () => {
## Header 2
### Header 3
#### Header 4
-`;
+`.replace(/\n/g, EOL);
const { lastFrame } = render(
@@ -64,7 +65,10 @@ describe('', () => {
});
it('renders a fenced code block with a language', () => {
- const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```';
+ const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```'.replace(
+ /\n/g,
+ EOL,
+ );
const { lastFrame } = render(
@@ -74,7 +78,7 @@ describe('', () => {
});
it('renders a fenced code block without a language', () => {
- const text = '```\nplain text\n```';
+ const text = '```\nplain text\n```'.replace(/\n/g, EOL);
const { lastFrame } = render(
@@ -84,7 +88,7 @@ describe('', () => {
});
it('handles unclosed (pending) code blocks', () => {
- const text = '```typescript\nlet y = 2;';
+ const text = '```typescript\nlet y = 2;'.replace(/\n/g, EOL);
const { lastFrame } = render(
@@ -98,7 +102,7 @@ describe('', () => {
- item A
* item B
+ item C
-`;
+`.replace(/\n/g, EOL);
const { lastFrame } = render(
@@ -112,7 +116,7 @@ describe('', () => {
* Level 1
* Level 2
* Level 3
-`;
+`.replace(/\n/g, EOL);
const { lastFrame } = render(
@@ -125,7 +129,7 @@ describe('', () => {
const text = `
1. First item
2. Second item
-`;
+`.replace(/\n/g, EOL);
const { lastFrame } = render(
@@ -141,7 +145,7 @@ Hello
World
***
Test
-`;
+`.replace(/\n/g, EOL);
const { lastFrame } = render(
@@ -156,7 +160,7 @@ Test
|----------|:--------:|
| Cell 1 | Cell 2 |
| Cell 3 | Cell 4 |
-`;
+`.replace(/\n/g, EOL);
const { lastFrame } = render(
@@ -170,7 +174,7 @@ Test
Some text before.
| A | B |
|---|
-| 1 | 2 |`;
+| 1 | 2 |`.replace(/\n/g, EOL);
const { lastFrame } = render(
@@ -182,7 +186,7 @@ Some text before.
it('inserts a single space between paragraphs', () => {
const text = `Paragraph 1.
-Paragraph 2.`;
+Paragraph 2.`.replace(/\n/g, EOL);
const { lastFrame } = render(
@@ -205,7 +209,7 @@ some code
\`\`\`
Another paragraph.
-`;
+`.replace(/\n/g, EOL);
const { lastFrame } = render(
@@ -215,7 +219,7 @@ Another paragraph.
});
it('hides line numbers in code blocks when showLineNumbers is false', () => {
- const text = '```javascript\nconst x = 1;\n```';
+ const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, EOL);
const settings = new LoadedSettings(
{ path: '', settings: {} },
{ path: '', settings: { showLineNumbers: false } },
@@ -234,7 +238,7 @@ Another paragraph.
});
it('shows line numbers in code blocks by default', () => {
- const text = '```javascript\nconst x = 1;\n```';
+ const text = '```javascript\nconst x = 1;\n```'.replace(/\n/g, EOL);
const { lastFrame } = render(
diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx
index 7568e1f8..660ce2b1 100644
--- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx
+++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx
@@ -6,6 +6,7 @@
import React from 'react';
import { Text, Box } from 'ink';
+import { EOL } from 'os';
import { Colors } from '../colors.js';
import { colorizeCode } from './CodeColorizer.js';
import { TableRenderer } from './TableRenderer.js';
@@ -34,7 +35,7 @@ const MarkdownDisplayInternal: React.FC = ({
}) => {
if (!text) return <>>;
- const lines = text.split('\n');
+ const lines = text.split(EOL);
const headerRegex = /^ *(#{1,4}) +(.*)/;
const codeFenceRegex = /^ *(`{3,}|~{3,}) *(\w*?) *$/;
const ulItemRegex = /^([ \t]*)([-*+]) +(.*)/;
diff --git a/packages/cli/src/zed-integration/acp.ts b/packages/cli/src/zed-integration/acp.ts
index eef4e1ee..3ce9bd42 100644
--- a/packages/cli/src/zed-integration/acp.ts
+++ b/packages/cli/src/zed-integration/acp.ts
@@ -7,6 +7,7 @@
/* ACP defines a schema for a simple (experimental) JSON-RPC protocol that allows GUI applications to interact with agents. */
import { z } from 'zod';
+import { EOL } from 'os';
import * as schema from './schema.js';
export * from './schema.js';
@@ -172,7 +173,7 @@ class Connection {
const decoder = new TextDecoder();
for await (const chunk of output) {
content += decoder.decode(chunk, { stream: true });
- const lines = content.split('\n');
+ const lines = content.split(EOL);
content = lines.pop() || '';
for (const line of lines) {
diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts
index 91f91729..c1b81c5d 100644
--- a/packages/core/src/config/config.test.ts
+++ b/packages/core/src/config/config.test.ts
@@ -594,4 +594,38 @@ describe('Server Config (config.ts)', () => {
expect(config.getTelemetryOtlpProtocol()).toBe('grpc');
});
});
+
+ describe('UseRipgrep Configuration', () => {
+ it('should default useRipgrep to false when not provided', () => {
+ const config = new Config(baseParams);
+ expect(config.getUseRipgrep()).toBe(false);
+ });
+
+ it('should set useRipgrep to true when provided as true', () => {
+ const paramsWithRipgrep: ConfigParameters = {
+ ...baseParams,
+ useRipgrep: true,
+ };
+ const config = new Config(paramsWithRipgrep);
+ expect(config.getUseRipgrep()).toBe(true);
+ });
+
+ it('should set useRipgrep to false when explicitly provided as false', () => {
+ const paramsWithRipgrep: ConfigParameters = {
+ ...baseParams,
+ useRipgrep: false,
+ };
+ const config = new Config(paramsWithRipgrep);
+ expect(config.getUseRipgrep()).toBe(false);
+ });
+
+ it('should default useRipgrep to false when undefined', () => {
+ const paramsWithUndefinedRipgrep: ConfigParameters = {
+ ...baseParams,
+ useRipgrep: undefined,
+ };
+ const config = new Config(paramsWithUndefinedRipgrep);
+ expect(config.getUseRipgrep()).toBe(false);
+ });
+ });
});
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 227a17c0..20287b8d 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -16,6 +16,7 @@ import { ToolRegistry } from '../tools/tool-registry.js';
import { LSTool } from '../tools/ls.js';
import { ReadFileTool } from '../tools/read-file.js';
import { GrepTool } from '../tools/grep.js';
+import { RipGrepTool } from '../tools/ripGrep.js';
import { GlobTool } from '../tools/glob.js';
import { EditTool } from '../tools/edit.js';
import { ShellTool } from '../tools/shell.js';
@@ -199,6 +200,7 @@ export interface ConfigParameters {
chatCompression?: ChatCompressionSettings;
interactive?: boolean;
trustedFolder?: boolean;
+ useRipgrep?: boolean;
shouldUseNodePtyShell?: boolean;
skipNextSpeakerCheck?: boolean;
enablePromptCompletion?: boolean;
@@ -267,6 +269,7 @@ export class Config {
private readonly chatCompression: ChatCompressionSettings | undefined;
private readonly interactive: boolean;
private readonly trustedFolder: boolean | undefined;
+ private readonly useRipgrep: boolean;
private readonly shouldUseNodePtyShell: boolean;
private readonly skipNextSpeakerCheck: boolean;
private readonly enablePromptCompletion: boolean = false;
@@ -338,6 +341,7 @@ export class Config {
this.chatCompression = params.chatCompression;
this.interactive = params.interactive ?? false;
this.trustedFolder = params.trustedFolder;
+ this.useRipgrep = params.useRipgrep ?? false;
this.shouldUseNodePtyShell = params.shouldUseNodePtyShell ?? false;
this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? false;
this.storage = new Storage(this.targetDir);
@@ -727,6 +731,10 @@ export class Config {
return this.interactive;
}
+ getUseRipgrep(): boolean {
+ return this.useRipgrep;
+ }
+
getShouldUseNodePtyShell(): boolean {
return this.shouldUseNodePtyShell;
}
@@ -789,7 +797,13 @@ export class Config {
registerCoreTool(LSTool, this);
registerCoreTool(ReadFileTool, this);
- registerCoreTool(GrepTool, this);
+
+ if (this.getUseRipgrep()) {
+ registerCoreTool(RipGrepTool, this);
+ } else {
+ registerCoreTool(GrepTool, this);
+ }
+
registerCoreTool(GlobTool, this);
registerCoreTool(EditTool, this);
registerCoreTool(WriteFileTool, this);
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index afdba8fc..855b36fc 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -73,6 +73,7 @@ export * from './prompts/mcp-prompts.js';
export * from './tools/read-file.js';
export * from './tools/ls.js';
export * from './tools/grep.js';
+export * from './tools/ripGrep.js';
export * from './tools/glob.js';
export * from './tools/edit.js';
export * from './tools/write-file.js';
diff --git a/packages/core/src/tools/ripGrep.test.ts b/packages/core/src/tools/ripGrep.test.ts
new file mode 100644
index 00000000..6db4e065
--- /dev/null
+++ b/packages/core/src/tools/ripGrep.test.ts
@@ -0,0 +1,1224 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { RipGrepTool, RipGrepToolParams } from './ripGrep.js';
+import path from 'path';
+import fs from 'fs/promises';
+import os, { EOL } from 'os';
+import { Config } from '../config/config.js';
+import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
+import { spawn, ChildProcess } from 'child_process';
+
+// Mock @lvce-editor/ripgrep for testing
+vi.mock('@lvce-editor/ripgrep', () => ({
+ rgPath: '/mock/rg/path',
+}));
+
+// Mock child_process for ripgrep calls
+vi.mock('child_process', () => ({
+ spawn: vi.fn(),
+}));
+
+const mockSpawn = vi.mocked(spawn);
+
+// Helper function to create mock spawn implementations
+function createMockSpawn(
+ options: {
+ outputData?: string;
+ exitCode?: number;
+ signal?: string;
+ } = {},
+) {
+ const { outputData, exitCode = 0, signal } = options;
+
+ return () => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ // Set up event listeners immediately
+ setTimeout(() => {
+ const stdoutDataHandler = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+
+ const closeHandler = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (stdoutDataHandler && outputData) {
+ stdoutDataHandler(Buffer.from(outputData));
+ }
+
+ if (closeHandler) {
+ closeHandler(exitCode, signal);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ };
+}
+
+describe('RipGrepTool', () => {
+ let tempRootDir: string;
+ let grepTool: RipGrepTool;
+ const abortSignal = new AbortController().signal;
+
+ const mockConfig = {
+ getTargetDir: () => tempRootDir,
+ getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
+ getDebugMode: () => false,
+ } as unknown as Config;
+
+ beforeEach(async () => {
+ vi.clearAllMocks();
+ mockSpawn.mockClear();
+ tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-'));
+ grepTool = new RipGrepTool(mockConfig);
+
+ // Create some test files and directories
+ await fs.writeFile(
+ path.join(tempRootDir, 'fileA.txt'),
+ 'hello world\nsecond line with world',
+ );
+ await fs.writeFile(
+ path.join(tempRootDir, 'fileB.js'),
+ 'const foo = "bar";\nfunction baz() { return "hello"; }',
+ );
+ await fs.mkdir(path.join(tempRootDir, 'sub'));
+ await fs.writeFile(
+ path.join(tempRootDir, 'sub', 'fileC.txt'),
+ 'another world in sub dir',
+ );
+ await fs.writeFile(
+ path.join(tempRootDir, 'sub', 'fileD.md'),
+ '# Markdown file\nThis is a test.',
+ );
+ });
+
+ afterEach(async () => {
+ await fs.rm(tempRootDir, { recursive: true, force: true });
+ });
+
+ describe('validateToolParams', () => {
+ it('should return null for valid params (pattern only)', () => {
+ const params: RipGrepToolParams = { pattern: 'hello' };
+ expect(grepTool.validateToolParams(params)).toBeNull();
+ });
+
+ it('should return null for valid params (pattern and path)', () => {
+ const params: RipGrepToolParams = { pattern: 'hello', path: '.' };
+ expect(grepTool.validateToolParams(params)).toBeNull();
+ });
+
+ it('should return null for valid params (pattern, path, and include)', () => {
+ const params: RipGrepToolParams = {
+ pattern: 'hello',
+ path: '.',
+ include: '*.txt',
+ };
+ expect(grepTool.validateToolParams(params)).toBeNull();
+ });
+
+ it('should return error if pattern is missing', () => {
+ const params = { path: '.' } as unknown as RipGrepToolParams;
+ expect(grepTool.validateToolParams(params)).toBe(
+ `params must have required property 'pattern'`,
+ );
+ });
+
+ it('should return null for what would be an invalid regex pattern', () => {
+ const params: RipGrepToolParams = { pattern: '[[' };
+ expect(grepTool.validateToolParams(params)).toBeNull();
+ });
+
+ it('should return error if path does not exist', () => {
+ const params: RipGrepToolParams = {
+ pattern: 'hello',
+ path: 'nonexistent',
+ };
+ // Check for the core error message, as the full path might vary
+ expect(grepTool.validateToolParams(params)).toContain(
+ 'Failed to access path stats for',
+ );
+ expect(grepTool.validateToolParams(params)).toContain('nonexistent');
+ });
+
+ it('should return error if path is a file, not a directory', async () => {
+ const filePath = path.join(tempRootDir, 'fileA.txt');
+ const params: RipGrepToolParams = { pattern: 'hello', path: filePath };
+ expect(grepTool.validateToolParams(params)).toContain(
+ `Path is not a directory: ${filePath}`,
+ );
+ });
+ });
+
+ describe('execute', () => {
+ it('should find matches for a simple pattern in all files', async () => {
+ mockSpawn.mockImplementationOnce(
+ createMockSpawn({
+ outputData: `fileA.txt:1:hello world${EOL}fileA.txt:2:second line with world${EOL}sub/fileC.txt:1:another world in sub dir${EOL}`,
+ exitCode: 0,
+ }),
+ );
+
+ const params: RipGrepToolParams = { pattern: 'world' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+ expect(result.llmContent).toContain(
+ 'Found 3 matches for pattern "world" in the workspace directory',
+ );
+ expect(result.llmContent).toContain('File: fileA.txt');
+ expect(result.llmContent).toContain('L1: hello world');
+ expect(result.llmContent).toContain('L2: second line with world');
+ expect(result.llmContent).toContain(
+ `File: ${path.join('sub', 'fileC.txt')}`,
+ );
+ expect(result.llmContent).toContain('L1: another world in sub dir');
+ expect(result.returnDisplay).toBe('Found 3 matches');
+ });
+
+ it('should find matches in a specific path', async () => {
+ // Setup specific mock for this test - searching in 'sub' should only return matches from that directory
+ mockSpawn.mockImplementationOnce(
+ createMockSpawn({
+ outputData: `fileC.txt:1:another world in sub dir${EOL}`,
+ exitCode: 0,
+ }),
+ );
+
+ const params: RipGrepToolParams = { pattern: 'world', path: 'sub' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+ expect(result.llmContent).toContain(
+ 'Found 1 match for pattern "world" in path "sub"',
+ );
+ expect(result.llmContent).toContain('File: fileC.txt'); // Path relative to 'sub'
+ expect(result.llmContent).toContain('L1: another world in sub dir');
+ expect(result.returnDisplay).toBe('Found 1 match');
+ });
+
+ it('should find matches with an include glob', async () => {
+ // Setup specific mock for this test
+ mockSpawn.mockImplementationOnce(
+ createMockSpawn({
+ outputData: `fileB.js:2:function baz() { return "hello"; }${EOL}`,
+ exitCode: 0,
+ }),
+ );
+
+ const params: RipGrepToolParams = { pattern: 'hello', include: '*.js' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+ expect(result.llmContent).toContain(
+ 'Found 1 match for pattern "hello" in the workspace directory (filter: "*.js"):',
+ );
+ expect(result.llmContent).toContain('File: fileB.js');
+ expect(result.llmContent).toContain(
+ 'L2: function baz() { return "hello"; }',
+ );
+ expect(result.returnDisplay).toBe('Found 1 match');
+ });
+
+ it('should find matches with an include glob and path', async () => {
+ await fs.writeFile(
+ path.join(tempRootDir, 'sub', 'another.js'),
+ 'const greeting = "hello";',
+ );
+
+ // Setup specific mock for this test - searching for 'hello' in 'sub' with '*.js' filter
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onData = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onData) {
+ // Only return match from the .js file in sub directory
+ onData(Buffer.from(`another.js:1:const greeting = "hello";${EOL}`));
+ }
+ if (onClose) {
+ onClose(0);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const params: RipGrepToolParams = {
+ pattern: 'hello',
+ path: 'sub',
+ include: '*.js',
+ };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+ expect(result.llmContent).toContain(
+ 'Found 1 match for pattern "hello" in path "sub" (filter: "*.js")',
+ );
+ expect(result.llmContent).toContain('File: another.js');
+ expect(result.llmContent).toContain('L1: const greeting = "hello";');
+ expect(result.returnDisplay).toBe('Found 1 match');
+ });
+
+ it('should return "No matches found" when pattern does not exist', async () => {
+ // Setup specific mock for no matches
+ mockSpawn.mockImplementationOnce(
+ createMockSpawn({
+ exitCode: 1, // No matches found
+ }),
+ );
+
+ const params: RipGrepToolParams = { pattern: 'nonexistentpattern' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+ expect(result.llmContent).toContain(
+ 'No matches found for pattern "nonexistentpattern" in the workspace directory.',
+ );
+ expect(result.returnDisplay).toBe('No matches found');
+ });
+
+ it('should return an error from ripgrep for invalid regex pattern', async () => {
+ mockSpawn.mockImplementationOnce(
+ createMockSpawn({
+ exitCode: 2,
+ }),
+ );
+
+ const params: RipGrepToolParams = { pattern: '[[' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+ expect(result.llmContent).toContain('ripgrep exited with code 2');
+ expect(result.returnDisplay).toContain(
+ 'Error: ripgrep exited with code 2',
+ );
+ });
+
+ it('should handle regex special characters correctly', async () => {
+ // Setup specific mock for this test - regex pattern 'foo.*bar' should match 'const foo = "bar";'
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onData = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onData) {
+ // Return match for the regex pattern
+ onData(Buffer.from(`fileB.js:1:const foo = "bar";${EOL}`));
+ }
+ if (onClose) {
+ onClose(0);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const params: RipGrepToolParams = { pattern: 'foo.*bar' }; // Matches 'const foo = "bar";'
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+ expect(result.llmContent).toContain(
+ 'Found 1 match for pattern "foo.*bar" in the workspace directory:',
+ );
+ expect(result.llmContent).toContain('File: fileB.js');
+ expect(result.llmContent).toContain('L1: const foo = "bar";');
+ });
+
+ it('should be case-insensitive by default (JS fallback)', async () => {
+ // Setup specific mock for this test - case insensitive search for 'HELLO'
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onData = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onData) {
+ // Return case-insensitive matches for 'HELLO'
+ onData(
+ Buffer.from(
+ `fileA.txt:1:hello world${EOL}fileB.js:2:function baz() { return "hello"; }${EOL}`,
+ ),
+ );
+ }
+ if (onClose) {
+ onClose(0);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const params: RipGrepToolParams = { pattern: 'HELLO' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+ expect(result.llmContent).toContain(
+ 'Found 2 matches for pattern "HELLO" in the workspace directory:',
+ );
+ expect(result.llmContent).toContain('File: fileA.txt');
+ expect(result.llmContent).toContain('L1: hello world');
+ expect(result.llmContent).toContain('File: fileB.js');
+ expect(result.llmContent).toContain(
+ 'L2: function baz() { return "hello"; }',
+ );
+ });
+
+ it('should throw an error if params are invalid', async () => {
+ const params = { path: '.' } as unknown as RipGrepToolParams; // Invalid: pattern missing
+ expect(() => grepTool.build(params)).toThrow(
+ /params must have required property 'pattern'/,
+ );
+ });
+ });
+
+ describe('multi-directory workspace', () => {
+ it('should search across all workspace directories when no path is specified', async () => {
+ // Create additional directory with test files
+ const secondDir = await fs.mkdtemp(
+ path.join(os.tmpdir(), 'grep-tool-second-'),
+ );
+ await fs.writeFile(
+ path.join(secondDir, 'other.txt'),
+ 'hello from second directory\nworld in second',
+ );
+ await fs.writeFile(
+ path.join(secondDir, 'another.js'),
+ 'function world() { return "test"; }',
+ );
+
+ // Create a mock config with multiple directories
+ const multiDirConfig = {
+ getTargetDir: () => tempRootDir,
+ getWorkspaceContext: () =>
+ createMockWorkspaceContext(tempRootDir, [secondDir]),
+ getDebugMode: () => false,
+ } as unknown as Config;
+
+ // Setup specific mock for this test - multi-directory search for 'world'
+ // Mock will be called twice - once for each directory
+ let callCount = 0;
+ mockSpawn.mockImplementation(() => {
+ callCount++;
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const stdoutDataHandler = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+
+ const closeHandler = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ let outputData = '';
+ if (callCount === 1) {
+ // First directory (tempRootDir)
+ outputData =
+ [
+ 'fileA.txt:1:hello world',
+ 'fileA.txt:2:second line with world',
+ 'sub/fileC.txt:1:another world in sub dir',
+ ].join(EOL) + EOL;
+ } else if (callCount === 2) {
+ // Second directory (secondDir)
+ outputData =
+ [
+ 'other.txt:2:world in second',
+ 'another.js:1:function world() { return "test"; }',
+ ].join(EOL) + EOL;
+ }
+
+ if (stdoutDataHandler && outputData) {
+ stdoutDataHandler(Buffer.from(outputData));
+ }
+
+ if (closeHandler) {
+ closeHandler(0);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const multiDirGrepTool = new RipGrepTool(multiDirConfig);
+ const params: RipGrepToolParams = { pattern: 'world' };
+ const invocation = multiDirGrepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+
+ // Should find matches in both directories
+ expect(result.llmContent).toContain(
+ 'Found 5 matches for pattern "world"',
+ );
+
+ // Matches from first directory
+ expect(result.llmContent).toContain('fileA.txt');
+ expect(result.llmContent).toContain('L1: hello world');
+ expect(result.llmContent).toContain('L2: second line with world');
+ expect(result.llmContent).toContain('fileC.txt');
+ expect(result.llmContent).toContain('L1: another world in sub dir');
+
+ // Matches from both directories
+ expect(result.llmContent).toContain('other.txt');
+ expect(result.llmContent).toContain('L2: world in second');
+ expect(result.llmContent).toContain('another.js');
+ expect(result.llmContent).toContain('L1: function world()');
+
+ // Clean up
+ await fs.rm(secondDir, { recursive: true, force: true });
+ mockSpawn.mockClear();
+ });
+
+ it('should search only specified path within workspace directories', async () => {
+ // Create additional directory
+ const secondDir = await fs.mkdtemp(
+ path.join(os.tmpdir(), 'grep-tool-second-'),
+ );
+ await fs.mkdir(path.join(secondDir, 'sub'));
+ await fs.writeFile(
+ path.join(secondDir, 'sub', 'test.txt'),
+ 'hello from second sub directory',
+ );
+
+ // Create a mock config with multiple directories
+ const multiDirConfig = {
+ getTargetDir: () => tempRootDir,
+ getWorkspaceContext: () =>
+ createMockWorkspaceContext(tempRootDir, [secondDir]),
+ getDebugMode: () => false,
+ } as unknown as Config;
+
+ // Setup specific mock for this test - searching in 'sub' should only return matches from that directory
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onData = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onData) {
+ onData(Buffer.from(`fileC.txt:1:another world in sub dir${EOL}`));
+ }
+ if (onClose) {
+ onClose(0);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const multiDirGrepTool = new RipGrepTool(multiDirConfig);
+
+ // Search only in the 'sub' directory of the first workspace
+ const params: RipGrepToolParams = { pattern: 'world', path: 'sub' };
+ const invocation = multiDirGrepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+
+ // Should only find matches in the specified sub directory
+ expect(result.llmContent).toContain(
+ 'Found 1 match for pattern "world" in path "sub"',
+ );
+ expect(result.llmContent).toContain('File: fileC.txt');
+ expect(result.llmContent).toContain('L1: another world in sub dir');
+
+ // Should not contain matches from second directory
+ expect(result.llmContent).not.toContain('test.txt');
+
+ // Clean up
+ await fs.rm(secondDir, { recursive: true, force: true });
+ });
+ });
+
+ describe('abort signal handling', () => {
+ it('should handle AbortSignal during search', async () => {
+ const controller = new AbortController();
+ const params: RipGrepToolParams = { pattern: 'world' };
+ const invocation = grepTool.build(params);
+
+ controller.abort();
+
+ const result = await invocation.execute(controller.signal);
+ expect(result).toBeDefined();
+ });
+
+ it('should abort streaming search when signal is triggered', async () => {
+ // Setup specific mock for this test - simulate process being killed due to abort
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ // Simulate process being aborted - use setTimeout to ensure handlers are registered first
+ setTimeout(() => {
+ const closeHandler = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (closeHandler) {
+ // Simulate process killed by signal (code is null, signal is SIGTERM)
+ closeHandler(null, 'SIGTERM');
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const controller = new AbortController();
+ const params: RipGrepToolParams = { pattern: 'test' };
+ const invocation = grepTool.build(params);
+
+ // Abort immediately before starting the search
+ controller.abort();
+
+ const result = await invocation.execute(controller.signal);
+ expect(result.llmContent).toContain(
+ 'Error during grep search operation: ripgrep exited with code null',
+ );
+ expect(result.returnDisplay).toContain(
+ 'Error: ripgrep exited with code null',
+ );
+ });
+ });
+
+ describe('error handling and edge cases', () => {
+ it('should handle workspace boundary violations', () => {
+ const params: RipGrepToolParams = { pattern: 'test', path: '../outside' };
+ expect(() => grepTool.build(params)).toThrow(/Path validation failed/);
+ });
+
+ it('should handle empty directories gracefully', async () => {
+ const emptyDir = path.join(tempRootDir, 'empty');
+ await fs.mkdir(emptyDir);
+
+ // Setup specific mock for this test - searching in empty directory should return no matches
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onClose) {
+ onClose(1);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const params: RipGrepToolParams = { pattern: 'test', path: 'empty' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+
+ expect(result.llmContent).toContain('No matches found');
+ expect(result.returnDisplay).toBe('No matches found');
+ });
+
+ it('should handle empty files correctly', async () => {
+ await fs.writeFile(path.join(tempRootDir, 'empty.txt'), '');
+
+ // Setup specific mock for this test - searching for anything in empty files should return no matches
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onClose) {
+ onClose(1);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const params: RipGrepToolParams = { pattern: 'anything' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+
+ expect(result.llmContent).toContain('No matches found');
+ });
+
+ it('should handle special characters in file names', async () => {
+ const specialFileName = 'file with spaces & symbols!.txt';
+ await fs.writeFile(
+ path.join(tempRootDir, specialFileName),
+ 'hello world with special chars',
+ );
+
+ // Setup specific mock for this test - searching for 'world' should find the file with special characters
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onData = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onData) {
+ onData(
+ Buffer.from(
+ `${specialFileName}:1:hello world with special chars${EOL}`,
+ ),
+ );
+ }
+ if (onClose) {
+ onClose(0);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const params: RipGrepToolParams = { pattern: 'world' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+
+ expect(result.llmContent).toContain(specialFileName);
+ expect(result.llmContent).toContain('hello world with special chars');
+ });
+
+ it('should handle deeply nested directories', async () => {
+ const deepPath = path.join(tempRootDir, 'a', 'b', 'c', 'd', 'e');
+ await fs.mkdir(deepPath, { recursive: true });
+ await fs.writeFile(
+ path.join(deepPath, 'deep.txt'),
+ 'content in deep directory',
+ );
+
+ // Setup specific mock for this test - searching for 'deep' should find the deeply nested file
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onData = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onData) {
+ onData(
+ Buffer.from(
+ `a/b/c/d/e/deep.txt:1:content in deep directory${EOL}`,
+ ),
+ );
+ }
+ if (onClose) {
+ onClose(0);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const params: RipGrepToolParams = { pattern: 'deep' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+
+ expect(result.llmContent).toContain('deep.txt');
+ expect(result.llmContent).toContain('content in deep directory');
+ });
+ });
+
+ describe('regex pattern validation', () => {
+ it('should handle complex regex patterns', async () => {
+ await fs.writeFile(
+ path.join(tempRootDir, 'code.js'),
+ 'function getName() { return "test"; }\nconst getValue = () => "value";',
+ );
+
+ // Setup specific mock for this test - regex pattern should match function declarations
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onData = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onData) {
+ onData(
+ Buffer.from(
+ `code.js:1:function getName() { return "test"; }${EOL}`,
+ ),
+ );
+ }
+ if (onClose) {
+ onClose(0);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const params: RipGrepToolParams = { pattern: 'function\\s+\\w+\\s*\\(' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+
+ expect(result.llmContent).toContain('function getName()');
+ expect(result.llmContent).not.toContain('const getValue');
+ });
+
+ it('should handle case sensitivity correctly in JS fallback', async () => {
+ await fs.writeFile(
+ path.join(tempRootDir, 'case.txt'),
+ 'Hello World\nhello world\nHELLO WORLD',
+ );
+
+ // Setup specific mock for this test - case insensitive search should match all variants
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onData = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onData) {
+ onData(
+ Buffer.from(
+ `case.txt:1:Hello World${EOL}case.txt:2:hello world${EOL}case.txt:3:HELLO WORLD${EOL}`,
+ ),
+ );
+ }
+ if (onClose) {
+ onClose(0);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const params: RipGrepToolParams = { pattern: 'hello' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+
+ expect(result.llmContent).toContain('Hello World');
+ expect(result.llmContent).toContain('hello world');
+ expect(result.llmContent).toContain('HELLO WORLD');
+ });
+
+ it('should handle escaped regex special characters', async () => {
+ await fs.writeFile(
+ path.join(tempRootDir, 'special.txt'),
+ 'Price: $19.99\nRegex: [a-z]+ pattern\nEmail: test@example.com',
+ );
+
+ // Setup specific mock for this test - escaped regex pattern should match price format
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onData = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onData) {
+ onData(Buffer.from(`special.txt:1:Price: $19.99${EOL}`));
+ }
+ if (onClose) {
+ onClose(0);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const params: RipGrepToolParams = { pattern: '\\$\\d+\\.\\d+' };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+
+ expect(result.llmContent).toContain('Price: $19.99');
+ expect(result.llmContent).not.toContain('Email: test@example.com');
+ });
+ });
+
+ describe('include pattern filtering', () => {
+ it('should handle multiple file extensions in include pattern', async () => {
+ await fs.writeFile(
+ path.join(tempRootDir, 'test.ts'),
+ 'typescript content',
+ );
+ await fs.writeFile(path.join(tempRootDir, 'test.tsx'), 'tsx content');
+ await fs.writeFile(
+ path.join(tempRootDir, 'test.js'),
+ 'javascript content',
+ );
+ await fs.writeFile(path.join(tempRootDir, 'test.txt'), 'text content');
+
+ // Setup specific mock for this test - include pattern should filter to only ts/tsx files
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onData = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onData) {
+ onData(
+ Buffer.from(
+ `test.ts:1:typescript content${EOL}test.tsx:1:tsx content${EOL}`,
+ ),
+ );
+ }
+ if (onClose) {
+ onClose(0);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const params: RipGrepToolParams = {
+ pattern: 'content',
+ include: '*.{ts,tsx}',
+ };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+
+ expect(result.llmContent).toContain('test.ts');
+ expect(result.llmContent).toContain('test.tsx');
+ expect(result.llmContent).not.toContain('test.js');
+ expect(result.llmContent).not.toContain('test.txt');
+ });
+
+ it('should handle directory patterns in include', async () => {
+ await fs.mkdir(path.join(tempRootDir, 'src'), { recursive: true });
+ await fs.writeFile(
+ path.join(tempRootDir, 'src', 'main.ts'),
+ 'source code',
+ );
+ await fs.writeFile(path.join(tempRootDir, 'other.ts'), 'other code');
+
+ // Setup specific mock for this test - include pattern should filter to only src/** files
+ mockSpawn.mockImplementationOnce(() => {
+ const mockProcess = {
+ stdout: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ stderr: {
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ },
+ on: vi.fn(),
+ removeListener: vi.fn(),
+ kill: vi.fn(),
+ };
+
+ setTimeout(() => {
+ const onData = mockProcess.stdout.on.mock.calls.find(
+ (call) => call[0] === 'data',
+ )?.[1];
+ const onClose = mockProcess.on.mock.calls.find(
+ (call) => call[0] === 'close',
+ )?.[1];
+
+ if (onData) {
+ onData(Buffer.from(`src/main.ts:1:source code${EOL}`));
+ }
+ if (onClose) {
+ onClose(0);
+ }
+ }, 0);
+
+ return mockProcess as unknown as ChildProcess;
+ });
+
+ const params: RipGrepToolParams = {
+ pattern: 'code',
+ include: 'src/**',
+ };
+ const invocation = grepTool.build(params);
+ const result = await invocation.execute(abortSignal);
+
+ expect(result.llmContent).toContain('main.ts');
+ expect(result.llmContent).not.toContain('other.ts');
+ });
+ });
+
+ describe('getDescription', () => {
+ it('should generate correct description with pattern only', () => {
+ const params: RipGrepToolParams = { pattern: 'testPattern' };
+ const invocation = grepTool.build(params);
+ expect(invocation.getDescription()).toBe("'testPattern'");
+ });
+
+ it('should generate correct description with pattern and include', () => {
+ const params: RipGrepToolParams = {
+ pattern: 'testPattern',
+ include: '*.ts',
+ };
+ const invocation = grepTool.build(params);
+ expect(invocation.getDescription()).toBe("'testPattern' in *.ts");
+ });
+
+ it('should generate correct description with pattern and path', async () => {
+ const dirPath = path.join(tempRootDir, 'src', 'app');
+ await fs.mkdir(dirPath, { recursive: true });
+ const params: RipGrepToolParams = {
+ pattern: 'testPattern',
+ path: path.join('src', 'app'),
+ };
+ const invocation = grepTool.build(params);
+ // The path will be relative to the tempRootDir, so we check for containment.
+ expect(invocation.getDescription()).toContain("'testPattern' within");
+ expect(invocation.getDescription()).toContain(path.join('src', 'app'));
+ });
+
+ it('should indicate searching across all workspace directories when no path specified', () => {
+ // Create a mock config with multiple directories
+ const multiDirConfig = {
+ getTargetDir: () => tempRootDir,
+ getWorkspaceContext: () =>
+ createMockWorkspaceContext(tempRootDir, ['/another/dir']),
+ getDebugMode: () => false,
+ } as unknown as Config;
+
+ const multiDirGrepTool = new RipGrepTool(multiDirConfig);
+ const params: RipGrepToolParams = { pattern: 'testPattern' };
+ const invocation = multiDirGrepTool.build(params);
+ expect(invocation.getDescription()).toBe(
+ "'testPattern' across all workspace directories",
+ );
+ });
+
+ it('should generate correct description with pattern, include, and path', async () => {
+ const dirPath = path.join(tempRootDir, 'src', 'app');
+ await fs.mkdir(dirPath, { recursive: true });
+ const params: RipGrepToolParams = {
+ pattern: 'testPattern',
+ include: '*.ts',
+ path: path.join('src', 'app'),
+ };
+ const invocation = grepTool.build(params);
+ expect(invocation.getDescription()).toContain(
+ "'testPattern' in *.ts within",
+ );
+ expect(invocation.getDescription()).toContain(path.join('src', 'app'));
+ });
+
+ it('should use ./ for root path in description', () => {
+ const params: RipGrepToolParams = { pattern: 'testPattern', path: '.' };
+ const invocation = grepTool.build(params);
+ expect(invocation.getDescription()).toBe("'testPattern' within ./");
+ });
+ });
+});
diff --git a/packages/core/src/tools/ripGrep.ts b/packages/core/src/tools/ripGrep.ts
new file mode 100644
index 00000000..e5d8974b
--- /dev/null
+++ b/packages/core/src/tools/ripGrep.ts
@@ -0,0 +1,502 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import path from 'path';
+import { EOL } from 'os';
+import { spawn } from 'child_process';
+import { rgPath } from '@lvce-editor/ripgrep';
+import {
+ BaseDeclarativeTool,
+ BaseToolInvocation,
+ Kind,
+ ToolInvocation,
+ ToolResult,
+} from './tools.js';
+import { SchemaValidator } from '../utils/schemaValidator.js';
+import { makeRelative, shortenPath } from '../utils/paths.js';
+import { getErrorMessage, isNodeError } from '../utils/errors.js';
+import { Config } from '../config/config.js';
+
+const DEFAULT_TOTAL_MAX_MATCHES = 20000;
+
+/**
+ * Parameters for the GrepTool
+ */
+export interface RipGrepToolParams {
+ /**
+ * The regular expression pattern to search for in file contents
+ */
+ pattern: string;
+
+ /**
+ * The directory to search in (optional, defaults to current directory relative to root)
+ */
+ path?: string;
+
+ /**
+ * File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")
+ */
+ include?: string;
+}
+
+/**
+ * Result object for a single grep match
+ */
+interface GrepMatch {
+ filePath: string;
+ lineNumber: number;
+ line: string;
+}
+
+class GrepToolInvocation extends BaseToolInvocation<
+ RipGrepToolParams,
+ ToolResult
+> {
+ constructor(
+ private readonly config: Config,
+ params: RipGrepToolParams,
+ ) {
+ super(params);
+ }
+
+ /**
+ * Checks if a path is within the root directory and resolves it.
+ * @param relativePath Path relative to the root directory (or undefined for root).
+ * @returns The absolute path if valid and exists, or null if no path specified (to search all directories).
+ * @throws {Error} If path is outside root, doesn't exist, or isn't a directory.
+ */
+ private resolveAndValidatePath(relativePath?: string): string | null {
+ // If no path specified, return null to indicate searching all workspace directories
+ if (!relativePath) {
+ return null;
+ }
+
+ const targetPath = path.resolve(this.config.getTargetDir(), relativePath);
+
+ // Security Check: Ensure the resolved path is within workspace boundaries
+ const workspaceContext = this.config.getWorkspaceContext();
+ if (!workspaceContext.isPathWithinWorkspace(targetPath)) {
+ const directories = workspaceContext.getDirectories();
+ throw new Error(
+ `Path validation failed: Attempted path "${relativePath}" resolves outside the allowed workspace directories: ${directories.join(', ')}`,
+ );
+ }
+
+ // Check existence and type after resolving
+ try {
+ const stats = fs.statSync(targetPath);
+ if (!stats.isDirectory()) {
+ throw new Error(`Path is not a directory: ${targetPath}`);
+ }
+ } catch (error: unknown) {
+ if (isNodeError(error) && error.code !== 'ENOENT') {
+ throw new Error(`Path does not exist: ${targetPath}`);
+ }
+ throw new Error(
+ `Failed to access path stats for ${targetPath}: ${error}`,
+ );
+ }
+
+ return targetPath;
+ }
+
+ async execute(signal: AbortSignal): Promise {
+ try {
+ const workspaceContext = this.config.getWorkspaceContext();
+ const searchDirAbs = this.resolveAndValidatePath(this.params.path);
+ const searchDirDisplay = this.params.path || '.';
+
+ // Determine which directories to search
+ let searchDirectories: readonly string[];
+ if (searchDirAbs === null) {
+ // No path specified - search all workspace directories
+ searchDirectories = workspaceContext.getDirectories();
+ } else {
+ // Specific path provided - search only that directory
+ searchDirectories = [searchDirAbs];
+ }
+
+ let allMatches: GrepMatch[] = [];
+ const totalMaxMatches = DEFAULT_TOTAL_MAX_MATCHES;
+
+ if (this.config.getDebugMode()) {
+ console.log(`[GrepTool] Total result limit: ${totalMaxMatches}`);
+ }
+
+ for (const searchDir of searchDirectories) {
+ const searchResult = await this.performRipgrepSearch({
+ pattern: this.params.pattern,
+ path: searchDir,
+ include: this.params.include,
+ signal,
+ });
+
+ if (searchDirectories.length > 1) {
+ const dirName = path.basename(searchDir);
+ searchResult.forEach((match) => {
+ match.filePath = path.join(dirName, match.filePath);
+ });
+ }
+
+ allMatches = allMatches.concat(searchResult);
+
+ if (allMatches.length >= totalMaxMatches) {
+ allMatches = allMatches.slice(0, totalMaxMatches);
+ break;
+ }
+ }
+
+ let searchLocationDescription: string;
+ if (searchDirAbs === null) {
+ const numDirs = workspaceContext.getDirectories().length;
+ searchLocationDescription =
+ numDirs > 1
+ ? `across ${numDirs} workspace directories`
+ : `in the workspace directory`;
+ } else {
+ searchLocationDescription = `in path "${searchDirDisplay}"`;
+ }
+
+ if (allMatches.length === 0) {
+ const noMatchMsg = `No matches found for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}.`;
+ return { llmContent: noMatchMsg, returnDisplay: `No matches found` };
+ }
+
+ const wasTruncated = allMatches.length >= totalMaxMatches;
+
+ const matchesByFile = allMatches.reduce(
+ (acc, match) => {
+ const fileKey = match.filePath;
+ if (!acc[fileKey]) {
+ acc[fileKey] = [];
+ }
+ acc[fileKey].push(match);
+ acc[fileKey].sort((a, b) => a.lineNumber - b.lineNumber);
+ return acc;
+ },
+ {} as Record,
+ );
+
+ const matchCount = allMatches.length;
+ const matchTerm = matchCount === 1 ? 'match' : 'matches';
+
+ let llmContent = `Found ${matchCount} ${matchTerm} for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}`;
+
+ if (wasTruncated) {
+ llmContent += ` (results limited to ${totalMaxMatches} matches for performance)`;
+ }
+
+ llmContent += `:\n---\n`;
+
+ for (const filePath in matchesByFile) {
+ llmContent += `File: ${filePath}\n`;
+ matchesByFile[filePath].forEach((match) => {
+ const trimmedLine = match.line.trim();
+ llmContent += `L${match.lineNumber}: ${trimmedLine}\n`;
+ });
+ llmContent += '---\n';
+ }
+
+ let displayMessage = `Found ${matchCount} ${matchTerm}`;
+ if (wasTruncated) {
+ displayMessage += ` (limited)`;
+ }
+
+ return {
+ llmContent: llmContent.trim(),
+ returnDisplay: displayMessage,
+ };
+ } catch (error) {
+ console.error(`Error during GrepLogic execution: ${error}`);
+ const errorMessage = getErrorMessage(error);
+ return {
+ llmContent: `Error during grep search operation: ${errorMessage}`,
+ returnDisplay: `Error: ${errorMessage}`,
+ };
+ }
+ }
+
+ private parseRipgrepOutput(output: string, basePath: string): GrepMatch[] {
+ const results: GrepMatch[] = [];
+ if (!output) return results;
+
+ const lines = output.split(EOL);
+
+ for (const line of lines) {
+ if (!line.trim()) continue;
+
+ const firstColonIndex = line.indexOf(':');
+ if (firstColonIndex === -1) continue;
+
+ const secondColonIndex = line.indexOf(':', firstColonIndex + 1);
+ if (secondColonIndex === -1) continue;
+
+ const filePathRaw = line.substring(0, firstColonIndex);
+ const lineNumberStr = line.substring(
+ firstColonIndex + 1,
+ secondColonIndex,
+ );
+ const lineContent = line.substring(secondColonIndex + 1);
+
+ const lineNumber = parseInt(lineNumberStr, 10);
+
+ if (!isNaN(lineNumber)) {
+ const absoluteFilePath = path.resolve(basePath, filePathRaw);
+ const relativeFilePath = path.relative(basePath, absoluteFilePath);
+
+ results.push({
+ filePath: relativeFilePath || path.basename(absoluteFilePath),
+ lineNumber,
+ line: lineContent,
+ });
+ }
+ }
+ return results;
+ }
+
+ private async performRipgrepSearch(options: {
+ pattern: string;
+ path: string;
+ include?: string;
+ signal: AbortSignal;
+ }): Promise {
+ const { pattern, path: absolutePath, include } = options;
+
+ const rgArgs = [
+ '--line-number',
+ '--no-heading',
+ '--with-filename',
+ '--ignore-case',
+ '--regexp',
+ pattern,
+ ];
+
+ if (include) {
+ rgArgs.push('--glob', include);
+ }
+
+ const excludes = [
+ '.git',
+ 'node_modules',
+ 'bower_components',
+ '*.log',
+ '*.tmp',
+ 'build',
+ 'dist',
+ 'coverage',
+ ];
+ excludes.forEach((exclude) => {
+ rgArgs.push('--glob', `!${exclude}`);
+ });
+
+ rgArgs.push('--threads', '4');
+ rgArgs.push(absolutePath);
+
+ try {
+ const output = await new Promise((resolve, reject) => {
+ const child = spawn(rgPath, rgArgs, {
+ windowsHide: true,
+ });
+
+ const stdoutChunks: Buffer[] = [];
+ const stderrChunks: Buffer[] = [];
+
+ const cleanup = () => {
+ if (options.signal.aborted) {
+ child.kill();
+ }
+ };
+
+ options.signal.addEventListener('abort', cleanup, { once: true });
+
+ child.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
+ child.stderr.on('data', (chunk) => stderrChunks.push(chunk));
+
+ child.on('error', (err) => {
+ options.signal.removeEventListener('abort', cleanup);
+ reject(
+ new Error(
+ `Failed to start ripgrep: ${err.message}. Please ensure @lvce-editor/ripgrep is properly installed.`,
+ ),
+ );
+ });
+
+ child.on('close', (code) => {
+ options.signal.removeEventListener('abort', cleanup);
+ const stdoutData = Buffer.concat(stdoutChunks).toString('utf8');
+ const stderrData = Buffer.concat(stderrChunks).toString('utf8');
+
+ if (code === 0) {
+ resolve(stdoutData);
+ } else if (code === 1) {
+ resolve(''); // No matches found
+ } else {
+ reject(
+ new Error(`ripgrep exited with code ${code}: ${stderrData}`),
+ );
+ }
+ });
+ });
+
+ return this.parseRipgrepOutput(output, absolutePath);
+ } catch (error: unknown) {
+ console.error(`GrepLogic: ripgrep failed: ${getErrorMessage(error)}`);
+ throw error;
+ }
+ }
+
+ /**
+ * Gets a description of the grep operation
+ * @param params Parameters for the grep operation
+ * @returns A string describing the grep
+ */
+ getDescription(): string {
+ let description = `'${this.params.pattern}'`;
+ if (this.params.include) {
+ description += ` in ${this.params.include}`;
+ }
+ if (this.params.path) {
+ const resolvedPath = path.resolve(
+ this.config.getTargetDir(),
+ this.params.path,
+ );
+ if (
+ resolvedPath === this.config.getTargetDir() ||
+ this.params.path === '.'
+ ) {
+ description += ` within ./`;
+ } else {
+ const relativePath = makeRelative(
+ resolvedPath,
+ this.config.getTargetDir(),
+ );
+ description += ` within ${shortenPath(relativePath)}`;
+ }
+ } else {
+ // When no path is specified, indicate searching all workspace directories
+ const workspaceContext = this.config.getWorkspaceContext();
+ const directories = workspaceContext.getDirectories();
+ if (directories.length > 1) {
+ description += ` across all workspace directories`;
+ }
+ }
+ return description;
+ }
+}
+
+/**
+ * Implementation of the Grep tool logic (moved from CLI)
+ */
+export class RipGrepTool extends BaseDeclarativeTool<
+ RipGrepToolParams,
+ ToolResult
+> {
+ static readonly Name = 'search_file_content';
+
+ constructor(private readonly config: Config) {
+ super(
+ RipGrepTool.Name,
+ 'SearchText',
+ 'Searches for a regular expression pattern within the content of files in a specified directory (or current working directory). Can filter files by a glob pattern. Returns the lines containing matches, along with their file paths and line numbers. Total results limited to 20,000 matches like VSCode.',
+ Kind.Search,
+ {
+ properties: {
+ pattern: {
+ description:
+ "The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').",
+ type: 'string',
+ },
+ path: {
+ description:
+ 'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.',
+ type: 'string',
+ },
+ include: {
+ description:
+ "Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).",
+ type: 'string',
+ },
+ },
+ required: ['pattern'],
+ type: 'object',
+ },
+ );
+ }
+
+ /**
+ * Checks if a path is within the root directory and resolves it.
+ * @param relativePath Path relative to the root directory (or undefined for root).
+ * @returns The absolute path if valid and exists, or null if no path specified (to search all directories).
+ * @throws {Error} If path is outside root, doesn't exist, or isn't a directory.
+ */
+ private resolveAndValidatePath(relativePath?: string): string | null {
+ // If no path specified, return null to indicate searching all workspace directories
+ if (!relativePath) {
+ return null;
+ }
+
+ const targetPath = path.resolve(this.config.getTargetDir(), relativePath);
+
+ // Security Check: Ensure the resolved path is within workspace boundaries
+ const workspaceContext = this.config.getWorkspaceContext();
+ if (!workspaceContext.isPathWithinWorkspace(targetPath)) {
+ const directories = workspaceContext.getDirectories();
+ throw new Error(
+ `Path validation failed: Attempted path "${relativePath}" resolves outside the allowed workspace directories: ${directories.join(', ')}`,
+ );
+ }
+
+ // Check existence and type after resolving
+ try {
+ const stats = fs.statSync(targetPath);
+ if (!stats.isDirectory()) {
+ throw new Error(`Path is not a directory: ${targetPath}`);
+ }
+ } catch (error: unknown) {
+ if (isNodeError(error) && error.code !== 'ENOENT') {
+ throw new Error(`Path does not exist: ${targetPath}`);
+ }
+ throw new Error(
+ `Failed to access path stats for ${targetPath}: ${error}`,
+ );
+ }
+
+ return targetPath;
+ }
+
+ /**
+ * Validates the parameters for the tool
+ * @param params Parameters to validate
+ * @returns An error message string if invalid, null otherwise
+ */
+ override validateToolParams(params: RipGrepToolParams): string | null {
+ const errors = SchemaValidator.validate(
+ this.schema.parametersJsonSchema,
+ params,
+ );
+ if (errors) {
+ return errors;
+ }
+
+ // Only validate path if one is provided
+ if (params.path) {
+ try {
+ this.resolveAndValidatePath(params.path);
+ } catch (error) {
+ return getErrorMessage(error);
+ }
+ }
+
+ return null; // Parameters are valid
+ }
+
+ protected createInvocation(
+ params: RipGrepToolParams,
+ ): ToolInvocation {
+ return new GrepToolInvocation(this.config, params);
+ }
+}
diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts
index 79354cf8..8685616e 100644
--- a/packages/core/src/tools/shell.test.ts
+++ b/packages/core/src/tools/shell.test.ts
@@ -32,6 +32,7 @@ import {
} from '../services/shellExecutionService.js';
import * as fs from 'fs';
import * as os from 'os';
+import { EOL } from 'os';
import * as path from 'path';
import * as crypto from 'crypto';
import * as summarizer from '../utils/summarizer.js';
@@ -141,7 +142,7 @@ describe('ShellTool', () => {
resolveShellExecution({ pid: 54321 });
vi.mocked(fs.existsSync).mockReturnValue(true);
- vi.mocked(fs.readFileSync).mockReturnValue('54321\n54322\n'); // Service PID and background PID
+ vi.mocked(fs.readFileSync).mockReturnValue(`54321${EOL}54322${EOL}`); // Service PID and background PID
const result = await promise;
diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts
index 3fce7c2d..5e64d030 100644
--- a/packages/core/src/tools/shell.ts
+++ b/packages/core/src/tools/shell.ts
@@ -6,7 +6,7 @@
import fs from 'fs';
import path from 'path';
-import os from 'os';
+import os, { EOL } from 'os';
import crypto from 'crypto';
import { Config } from '../config/config.js';
import {
@@ -192,7 +192,7 @@ class ShellToolInvocation extends BaseToolInvocation<
if (fs.existsSync(tempFilePath)) {
const pgrepLines = fs
.readFileSync(tempFilePath, 'utf8')
- .split('\n')
+ .split(EOL)
.filter(Boolean);
for (const line of pgrepLines) {
if (!/^\d+$/.test(line)) {