@vincemakes/kiso-tools-node 0.16.7 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +67 -32
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -105,6 +105,7 @@ export declare function listDirTool(opts: WorkspaceToolsOptions): Tool<{
|
|
|
105
105
|
export declare function searchTextTool(opts: WorkspaceToolsOptions): Tool<{
|
|
106
106
|
pattern: string;
|
|
107
107
|
path?: string;
|
|
108
|
+
caseSensitive?: boolean;
|
|
108
109
|
}>;
|
|
109
110
|
export declare function writeFileTool(opts: WorkspaceToolsOptions): Tool<{
|
|
110
111
|
path: string;
|
package/dist/index.js
CHANGED
|
@@ -382,12 +382,13 @@ export function listDirTool(opts) {
|
|
|
382
382
|
export function searchTextTool(opts) {
|
|
383
383
|
return defineTool({
|
|
384
384
|
name: "search_text",
|
|
385
|
-
description: "Search files under a workspace directory (recursive) for a regular expression (the workspace grep — prefer it over shell grep/rg). Returns matching file:line excerpts, capped at 50 — an overflow note states the count of further matches (narrow the pattern to see them).",
|
|
385
|
+
description: "Search files under a workspace directory (recursive), or a single file, for a regular expression (the workspace grep — prefer it over shell grep/rg). Returns matching file:line excerpts, capped at 50 — an overflow note states the count of further matches (narrow the pattern to see them). Case-insensitive unless caseSensitive is true.",
|
|
386
386
|
parameters: {
|
|
387
387
|
type: "object",
|
|
388
388
|
properties: {
|
|
389
389
|
pattern: { type: "string", description: "Regular expression to search for" },
|
|
390
|
-
path: { type: "string", description: "Workspace-relative
|
|
390
|
+
path: { type: "string", description: "Workspace-relative directory OR file (default: workspace root)" },
|
|
391
|
+
caseSensitive: { type: "boolean", description: "Match case exactly (default: false)" },
|
|
391
392
|
},
|
|
392
393
|
required: ["pattern"],
|
|
393
394
|
additionalProperties: false,
|
|
@@ -400,7 +401,7 @@ export function searchTextTool(opts) {
|
|
|
400
401
|
effects: { precommitSafe: true, concurrency: "shared" },
|
|
401
402
|
promptSnippet: "search_text — regex search over workspace files",
|
|
402
403
|
promptGuidelines: ["narrow the pattern when the result caps — never re-run a broad search"],
|
|
403
|
-
execute: async ({ pattern, path }) => {
|
|
404
|
+
execute: async ({ pattern, path, caseSensitive }) => {
|
|
404
405
|
let root;
|
|
405
406
|
try {
|
|
406
407
|
root = resolveWithinRoot(opts.workspaceRoot, path ?? ".");
|
|
@@ -410,7 +411,32 @@ export function searchTextTool(opts) {
|
|
|
410
411
|
return escapeResult(err.message);
|
|
411
412
|
throw err;
|
|
412
413
|
}
|
|
413
|
-
|
|
414
|
+
// DC-23 (the 0.16.7 dogfood): an INVALID pattern threw raw out
|
|
415
|
+
// of execute — `new RegExp` sat outside every try in this
|
|
416
|
+
// function, so a bad regex was a crash rather than a result the
|
|
417
|
+
// model could act on. And the "i" flag was hardcoded, so a
|
|
418
|
+
// case-sensitive search was not expressible at all.
|
|
419
|
+
let regex;
|
|
420
|
+
try {
|
|
421
|
+
regex = new RegExp(pattern, caseSensitive === true ? "" : "i");
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
return { content: `search_text failed: invalid pattern — ${err.message}`, isError: true, errorKind: "invalid_input" };
|
|
425
|
+
}
|
|
426
|
+
// DC-23: a FILE is a place text lives. The tool took only a
|
|
427
|
+
// directory and answered a file path with libuv's own words
|
|
428
|
+
// ("ENOTDIR: not a directory, scandir <path>"), which is the
|
|
429
|
+
// obvious thing to ask for — the file is already known and the
|
|
430
|
+
// question is where in it something is. The real dogfood model
|
|
431
|
+
// asked twice and learned nothing either time. One stat.
|
|
432
|
+
let single = null;
|
|
433
|
+
try {
|
|
434
|
+
if (statSync(root).isFile())
|
|
435
|
+
single = root;
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
return { content: `search_text failed: ${path ?? "."} — ${err.message}`, isError: true, errorKind: "invalid_input" };
|
|
439
|
+
}
|
|
414
440
|
// The walk NEVER early-aborts on the cap: the overflow note's count
|
|
415
441
|
// must be the file-true total, not a bound (the red line). The
|
|
416
442
|
// depth cap and the node_modules/dotfile skip stay.
|
|
@@ -436,6 +462,36 @@ export function searchTextTool(opts) {
|
|
|
436
462
|
sinceYield = 0;
|
|
437
463
|
await new Promise((r) => setImmediate(r));
|
|
438
464
|
};
|
|
465
|
+
// DC-23: the per-file scan is its own function now, because the
|
|
466
|
+
// single-file path and the walk must scan a file the SAME way —
|
|
467
|
+
// same inode boundary, same cap accounting, same excerpt shape.
|
|
468
|
+
// Two copies would be two answers to "what does searching this
|
|
469
|
+
// file mean".
|
|
470
|
+
const scanFile = async (full) => {
|
|
471
|
+
await breathe();
|
|
472
|
+
try {
|
|
473
|
+
// round 8: same inode boundary as read_file — a hard link
|
|
474
|
+
// to an external inode is not searched. round 4 (adversarial):
|
|
475
|
+
// the link count is verified against the WORKSPACE
|
|
476
|
+
// root, not the search subroot — a link that lives
|
|
477
|
+
// inside the workspace but outside the search dir is
|
|
478
|
+
// legal and must not be silently skipped.
|
|
479
|
+
if (inodeReadPolicy(opts.workspaceRoot, full) !== null)
|
|
480
|
+
return;
|
|
481
|
+
const text = readFileSync(full, "utf8");
|
|
482
|
+
for (const [i, line] of text.split("\n").entries()) {
|
|
483
|
+
if (regex.test(line)) {
|
|
484
|
+
totalMatches += 1;
|
|
485
|
+
if (matches.length < MAX_SEARCH_MATCHES) {
|
|
486
|
+
matches.push(`${full}:${i + 1}: ${line.trim().slice(0, 160)}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// unreadable file — skip
|
|
493
|
+
}
|
|
494
|
+
};
|
|
439
495
|
const walk = async (dir, depth) => {
|
|
440
496
|
if (depth > 8)
|
|
441
497
|
return;
|
|
@@ -443,38 +499,17 @@ export function searchTextTool(opts) {
|
|
|
443
499
|
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
444
500
|
continue;
|
|
445
501
|
const full = join(dir, entry.name);
|
|
446
|
-
if (entry.isDirectory())
|
|
502
|
+
if (entry.isDirectory())
|
|
447
503
|
await walk(full, depth + 1);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
await breathe();
|
|
451
|
-
try {
|
|
452
|
-
// round 8: same inode boundary as read_file — a hard link
|
|
453
|
-
// to an external inode is not searched. round 4 (adversarial):
|
|
454
|
-
// the link count is verified against the WORKSPACE
|
|
455
|
-
// root, not the search subroot — a link that lives
|
|
456
|
-
// inside the workspace but outside the search dir is
|
|
457
|
-
// legal and must not be silently skipped.
|
|
458
|
-
if (inodeReadPolicy(opts.workspaceRoot, full) !== null)
|
|
459
|
-
continue;
|
|
460
|
-
const text = readFileSync(full, "utf8");
|
|
461
|
-
for (const [i, line] of text.split("\n").entries()) {
|
|
462
|
-
if (regex.test(line)) {
|
|
463
|
-
totalMatches += 1;
|
|
464
|
-
if (matches.length < MAX_SEARCH_MATCHES) {
|
|
465
|
-
matches.push(`${full}:${i + 1}: ${line.trim().slice(0, 160)}`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
catch {
|
|
471
|
-
// unreadable file — skip
|
|
472
|
-
}
|
|
473
|
-
}
|
|
504
|
+
else if (entry.isFile())
|
|
505
|
+
await scanFile(full);
|
|
474
506
|
}
|
|
475
507
|
};
|
|
476
508
|
try {
|
|
477
|
-
|
|
509
|
+
if (single !== null)
|
|
510
|
+
await scanFile(single);
|
|
511
|
+
else
|
|
512
|
+
await walk(root, 0);
|
|
478
513
|
}
|
|
479
514
|
catch (err) {
|
|
480
515
|
return { content: `search_text failed: ${err.message}`, isError: true, errorKind: "fatal" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tools-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "kiso coding tools for Node hosts — read file, list directory, search text, write/edit file, shell command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"test": "vitest run"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@vincemakes/kiso-core": "0.
|
|
24
|
+
"@vincemakes/kiso-core": "0.17.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.2",
|