@wrongstack/tools 0.275.0 → 0.276.2
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/{background-indexer-BoTUw0EM.d.ts → background-indexer-BeDBxfSh.d.ts} +6 -0
- package/dist/builtin.js +1007 -204
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.d.ts +30 -2
- package/dist/codebase-index/index.js +201 -24
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +196 -23
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/document.js +2 -2
- package/dist/document.js.map +1 -1
- package/dist/edit.js +52 -15
- package/dist/edit.js.map +1 -1
- package/dist/fetch.js +89 -18
- package/dist/fetch.js.map +1 -1
- package/dist/glob.js +35 -1
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +15 -4
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1041 -213
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +7 -0
- package/dist/install.js +6 -0
- package/dist/install.js.map +1 -1
- package/dist/json.d.ts +26 -1
- package/dist/json.js +396 -44
- package/dist/json.js.map +1 -1
- package/dist/memory.js +26 -4
- package/dist/memory.js.map +1 -1
- package/dist/outdated.js +2 -2
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +1007 -204
- package/dist/pack.js.map +1 -1
- package/dist/read.js +36 -6
- package/dist/read.js.map +1 -1
- package/dist/replace.js +27 -9
- package/dist/replace.js.map +1 -1
- package/dist/search.d.ts +5 -1
- package/dist/search.js +179 -62
- package/dist/search.js.map +1 -1
- package/dist/tool-help.js +2 -2
- package/dist/tool-help.js.map +1 -1
- package/dist/tool-search.js +2 -2
- package/dist/tool-search.js.map +1 -1
- package/dist/write.js +13 -3
- package/dist/write.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs7 from 'node:fs/promises';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { toErrorMessage, atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, assessCommitSafety, recordPackageAction, detectPackageEcosystem, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
3
|
+
import { ToolValidationError, FsError, toErrorMessage, atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, ToolError, FetchError, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, assessCommitSafety, deepMerge, recordPackageAction, detectPackageEcosystem, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
6
6
|
import { spawn, execFileSync } from 'node:child_process';
|
|
@@ -207,19 +207,49 @@ var readTool = {
|
|
|
207
207
|
required: ["path"]
|
|
208
208
|
},
|
|
209
209
|
async execute(input, ctx) {
|
|
210
|
-
if (!input?.path)
|
|
210
|
+
if (!input?.path) {
|
|
211
|
+
throw new ToolValidationError({
|
|
212
|
+
message: "read: path is required",
|
|
213
|
+
field: "path"
|
|
214
|
+
});
|
|
215
|
+
}
|
|
211
216
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
212
217
|
let stat11;
|
|
213
218
|
try {
|
|
214
219
|
stat11 = await fs7.stat(absPath);
|
|
215
220
|
} catch (err) {
|
|
216
221
|
const code = err.code;
|
|
217
|
-
if (code === "ENOENT")
|
|
218
|
-
|
|
222
|
+
if (code === "ENOENT") {
|
|
223
|
+
throw new FsError({
|
|
224
|
+
message: `read: file not found "${input.path}"`,
|
|
225
|
+
code: "FS_READ_FAILED",
|
|
226
|
+
path: absPath,
|
|
227
|
+
context: { errno: "ENOENT" }
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
throw new FsError({
|
|
231
|
+
message: `read: failed to stat "${input.path}": ${toErrorMessage(err)}`,
|
|
232
|
+
code: "FS_READ_FAILED",
|
|
233
|
+
path: absPath,
|
|
234
|
+
context: { errno: code },
|
|
235
|
+
cause: err
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (!stat11.isFile()) {
|
|
239
|
+
throw new FsError({
|
|
240
|
+
message: `read: "${input.path}" is not a regular file`,
|
|
241
|
+
code: "FS_READ_FAILED",
|
|
242
|
+
path: absPath,
|
|
243
|
+
context: { reason: "not-a-regular-file" }
|
|
244
|
+
});
|
|
219
245
|
}
|
|
220
|
-
if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
221
246
|
if (stat11.size > MAX_BYTES) {
|
|
222
|
-
throw new
|
|
247
|
+
throw new FsError({
|
|
248
|
+
message: `read: file too large (${stat11.size} bytes, limit ${MAX_BYTES})`,
|
|
249
|
+
code: "FS_READ_FAILED",
|
|
250
|
+
path: absPath,
|
|
251
|
+
context: { size: stat11.size, limit: MAX_BYTES, reason: "too-large" }
|
|
252
|
+
});
|
|
223
253
|
}
|
|
224
254
|
const offset = Math.max(1, input.offset ?? 1);
|
|
225
255
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
@@ -364,8 +394,18 @@ var writeTool = {
|
|
|
364
394
|
required: ["path", "content"]
|
|
365
395
|
},
|
|
366
396
|
async execute(input, ctx) {
|
|
367
|
-
if (!input?.path)
|
|
368
|
-
|
|
397
|
+
if (!input?.path) {
|
|
398
|
+
throw new ToolValidationError({
|
|
399
|
+
message: "write: path is required",
|
|
400
|
+
field: "path"
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (input.content === void 0) {
|
|
404
|
+
throw new ToolValidationError({
|
|
405
|
+
message: "write: content is required",
|
|
406
|
+
field: "content"
|
|
407
|
+
});
|
|
408
|
+
}
|
|
369
409
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
370
410
|
let existed = false;
|
|
371
411
|
let prev = "";
|
|
@@ -425,28 +465,62 @@ var editTool = {
|
|
|
425
465
|
required: ["path", "old_string", "new_string"]
|
|
426
466
|
},
|
|
427
467
|
async execute(input, ctx) {
|
|
428
|
-
if (!input?.path)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (input.old_string ===
|
|
468
|
+
if (!input?.path) {
|
|
469
|
+
throw new ToolValidationError({ message: "edit: path is required", field: "path" });
|
|
470
|
+
}
|
|
471
|
+
if (input.old_string === void 0) {
|
|
472
|
+
throw new ToolValidationError({
|
|
473
|
+
message: "edit: old_string is required",
|
|
474
|
+
field: "old_string"
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (input.new_string === void 0) {
|
|
478
|
+
throw new ToolValidationError({
|
|
479
|
+
message: "edit: new_string is required",
|
|
480
|
+
field: "new_string"
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
if (input.old_string === "") {
|
|
484
|
+
throw new ToolValidationError({
|
|
485
|
+
message: "edit: old_string cannot be empty",
|
|
486
|
+
field: "old_string"
|
|
487
|
+
});
|
|
488
|
+
}
|
|
432
489
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
433
490
|
const stat11 = await fs7.stat(absPath).catch((err) => {
|
|
434
491
|
if (err.code === "ENOENT") {
|
|
435
|
-
throw new
|
|
492
|
+
throw new ToolValidationError({
|
|
493
|
+
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
494
|
+
field: "path",
|
|
495
|
+
context: { exists: false }
|
|
496
|
+
});
|
|
436
497
|
}
|
|
437
498
|
throw err;
|
|
438
499
|
});
|
|
439
|
-
if (!stat11.isFile())
|
|
500
|
+
if (!stat11.isFile()) {
|
|
501
|
+
throw new ToolValidationError({
|
|
502
|
+
message: `edit: "${input.path}" is not a regular file`,
|
|
503
|
+
field: "path"
|
|
504
|
+
});
|
|
505
|
+
}
|
|
440
506
|
const autoRead = !ctx.hasRead(absPath);
|
|
441
507
|
const original = await fs7.readFile(absPath, "utf8");
|
|
442
508
|
const updated = await fs7.stat(absPath);
|
|
443
509
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
444
510
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
445
511
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
446
|
-
throw new
|
|
512
|
+
throw new ToolValidationError({
|
|
513
|
+
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
514
|
+
field: "path",
|
|
515
|
+
context: { reason: "external_modification" }
|
|
516
|
+
});
|
|
447
517
|
}
|
|
448
518
|
if (autoRead && updated.mtimeMs > stat11.mtimeMs + mtimeTolerance) {
|
|
449
|
-
throw new
|
|
519
|
+
throw new ToolValidationError({
|
|
520
|
+
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
521
|
+
field: "path",
|
|
522
|
+
context: { reason: "auto_read_race" }
|
|
523
|
+
});
|
|
450
524
|
}
|
|
451
525
|
const autoReadNote = autoRead ? `No prior read was recorded for "${input.path}"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.` : void 0;
|
|
452
526
|
const style = detectNewlineStyle(original);
|
|
@@ -472,15 +546,18 @@ var editTool = {
|
|
|
472
546
|
}
|
|
473
547
|
if (count === 0) {
|
|
474
548
|
const hint = findSimilarity(fileLf, oldLf);
|
|
475
|
-
throw new
|
|
476
|
-
`edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}.` : ""}
|
|
477
|
-
|
|
549
|
+
throw new ToolValidationError({
|
|
550
|
+
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}.` : ""}`,
|
|
551
|
+
field: "old_string"
|
|
552
|
+
});
|
|
478
553
|
}
|
|
479
554
|
if (count > 1 && !input.replace_all) {
|
|
480
555
|
const lines = lineNumbersFor(fileLf, matches);
|
|
481
|
-
throw new
|
|
482
|
-
`edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true
|
|
483
|
-
|
|
556
|
+
throw new ToolValidationError({
|
|
557
|
+
message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true.`,
|
|
558
|
+
field: "old_string",
|
|
559
|
+
context: { occurrences: count }
|
|
560
|
+
});
|
|
484
561
|
}
|
|
485
562
|
const newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
486
563
|
const newFile = toStyle(newFileLf, style);
|
|
@@ -580,8 +657,8 @@ var DEFAULT_IGNORE = ["node_modules", ".git", "dist", "build", ".next", "coverag
|
|
|
580
657
|
var replaceTool = {
|
|
581
658
|
name: "replace",
|
|
582
659
|
category: "Transform",
|
|
583
|
-
description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool.
|
|
584
|
-
usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1.
|
|
660
|
+
description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool. Dry-run is ON by default \u2014 set `dry_run: false` to apply changes.",
|
|
661
|
+
usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
|
|
585
662
|
permission: "confirm",
|
|
586
663
|
mutating: true,
|
|
587
664
|
capabilities: ["fs.write"],
|
|
@@ -601,22 +678,40 @@ var replaceTool = {
|
|
|
601
678
|
type: "boolean",
|
|
602
679
|
description: "Replace all occurrences in each file (default: true)"
|
|
603
680
|
},
|
|
604
|
-
dry_run: { type: "boolean", description: "Preview changes without writing" }
|
|
681
|
+
dry_run: { type: "boolean", description: "Preview changes without writing (default: true)" }
|
|
605
682
|
},
|
|
606
683
|
required: ["pattern", "replacement", "files"]
|
|
607
684
|
},
|
|
608
685
|
async execute(input, ctx) {
|
|
609
|
-
if (!input?.pattern)
|
|
610
|
-
|
|
611
|
-
|
|
686
|
+
if (!input?.pattern) {
|
|
687
|
+
throw new ToolValidationError({
|
|
688
|
+
message: "replace: pattern is required",
|
|
689
|
+
field: "pattern"
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
if (input.replacement === void 0) {
|
|
693
|
+
throw new ToolValidationError({
|
|
694
|
+
message: "replace: replacement is required",
|
|
695
|
+
field: "replacement"
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
if (!input?.files) {
|
|
699
|
+
throw new ToolValidationError({
|
|
700
|
+
message: "replace: files is required",
|
|
701
|
+
field: "files"
|
|
702
|
+
});
|
|
703
|
+
}
|
|
612
704
|
const replaceAll = input.replace_all ?? true;
|
|
613
705
|
const compiled = compileUserRegex(input.pattern, "g");
|
|
614
706
|
if (!compiled.ok) {
|
|
615
|
-
throw new
|
|
707
|
+
throw new ToolValidationError({
|
|
708
|
+
message: `replace: ${compiled.reason}`,
|
|
709
|
+
field: "pattern"
|
|
710
|
+
});
|
|
616
711
|
}
|
|
617
712
|
const re = compiled.regex;
|
|
618
713
|
const globRe = input.glob ? compileGlob(input.glob) : null;
|
|
619
|
-
const dryRun = input.dry_run ??
|
|
714
|
+
const dryRun = input.dry_run ?? true;
|
|
620
715
|
const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
|
|
621
716
|
const fileList = await resolveFiles(filesInput, ctx, globRe);
|
|
622
717
|
const realRoot = await fs7.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
@@ -830,7 +925,7 @@ var globTool = {
|
|
|
830
925
|
},
|
|
831
926
|
async execute(input, ctx) {
|
|
832
927
|
if (!input?.pattern) throw new Error("glob: pattern is required");
|
|
833
|
-
const base = input.path ?
|
|
928
|
+
const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
|
|
834
929
|
const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
|
|
835
930
|
const ignored = await readGitignore(base);
|
|
836
931
|
const re = compileGlob(input.pattern);
|
|
@@ -885,8 +980,12 @@ var globTool = {
|
|
|
885
980
|
try {
|
|
886
981
|
const st = await fs7.stat(full);
|
|
887
982
|
if (st.isDirectory()) {
|
|
983
|
+
const real = await fs7.realpath(full);
|
|
984
|
+
await assertRealInsideRoot(real, ctx);
|
|
888
985
|
subdirs.push({ full, rel });
|
|
889
986
|
} else if (st.isFile()) {
|
|
987
|
+
const real = await fs7.realpath(full);
|
|
988
|
+
await assertRealInsideRoot(real, ctx);
|
|
890
989
|
re.lastIndex = 0;
|
|
891
990
|
const relMatch = re.test(rel);
|
|
892
991
|
re.lastIndex = 0;
|
|
@@ -977,13 +1076,21 @@ var grepTool = {
|
|
|
977
1076
|
return final;
|
|
978
1077
|
},
|
|
979
1078
|
async *executeStream(input, ctx, opts) {
|
|
980
|
-
if (!input?.pattern)
|
|
1079
|
+
if (!input?.pattern) {
|
|
1080
|
+
throw new ToolValidationError({
|
|
1081
|
+
message: "grep: pattern is required",
|
|
1082
|
+
field: "pattern"
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
981
1085
|
const base = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
|
|
982
1086
|
const mode = input.output_mode ?? "content";
|
|
983
1087
|
const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
|
|
984
1088
|
const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
|
|
985
1089
|
if (!validation.ok) {
|
|
986
|
-
throw new
|
|
1090
|
+
throw new ToolValidationError({
|
|
1091
|
+
message: `grep: ${validation.reason}`,
|
|
1092
|
+
field: "pattern"
|
|
1093
|
+
});
|
|
987
1094
|
}
|
|
988
1095
|
const rgAvailable = await detectRg(opts.signal);
|
|
989
1096
|
if (rgAvailable) {
|
|
@@ -1139,7 +1246,10 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
1139
1246
|
const flags = input.case_insensitive ? "i" : "";
|
|
1140
1247
|
const compiled = compileUserRegex(input.pattern, flags);
|
|
1141
1248
|
if (!compiled.ok) {
|
|
1142
|
-
throw new
|
|
1249
|
+
throw new ToolValidationError({
|
|
1250
|
+
message: `grep: ${compiled.reason}`,
|
|
1251
|
+
field: "pattern"
|
|
1252
|
+
});
|
|
1143
1253
|
}
|
|
1144
1254
|
const re = compiled.regex;
|
|
1145
1255
|
const globRe = input.glob ? compileGlob(input.glob) : null;
|
|
@@ -3620,10 +3730,16 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
3620
3730
|
for (; ; ) {
|
|
3621
3731
|
const parsed = new URL(currentUrl);
|
|
3622
3732
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
3623
|
-
throw new
|
|
3733
|
+
throw new ToolValidationError({
|
|
3734
|
+
message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
|
|
3735
|
+
field: "url"
|
|
3736
|
+
});
|
|
3624
3737
|
}
|
|
3625
3738
|
if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
3626
|
-
throw new
|
|
3739
|
+
throw new ToolValidationError({
|
|
3740
|
+
message: "fetch: redirect to http:// blocked (HTTPS required by default)",
|
|
3741
|
+
field: "url"
|
|
3742
|
+
});
|
|
3627
3743
|
}
|
|
3628
3744
|
await assertNotPrivate(parsed.hostname);
|
|
3629
3745
|
const init = {
|
|
@@ -3638,11 +3754,19 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
3638
3754
|
}
|
|
3639
3755
|
redirectCount++;
|
|
3640
3756
|
if (redirectCount > maxRedirects) {
|
|
3641
|
-
throw new
|
|
3757
|
+
throw new FetchError({
|
|
3758
|
+
message: `fetch: exceeded ${maxRedirects} redirects`,
|
|
3759
|
+
status: res.status,
|
|
3760
|
+
context: { url: currentUrl, maxRedirects, redirectCount }
|
|
3761
|
+
});
|
|
3642
3762
|
}
|
|
3643
3763
|
const location = res.headers.get("location");
|
|
3644
3764
|
if (!location) {
|
|
3645
|
-
throw new
|
|
3765
|
+
throw new FetchError({
|
|
3766
|
+
message: "fetch: redirect status with no location header",
|
|
3767
|
+
status: res.status,
|
|
3768
|
+
context: { url: currentUrl, redirectCount }
|
|
3769
|
+
});
|
|
3646
3770
|
}
|
|
3647
3771
|
currentUrl = new URL(location, currentUrl).toString();
|
|
3648
3772
|
}
|
|
@@ -3681,26 +3805,53 @@ var fetchTool = {
|
|
|
3681
3805
|
async execute(input, ctx, opts) {
|
|
3682
3806
|
let final;
|
|
3683
3807
|
const executeStream = fetchTool.executeStream;
|
|
3684
|
-
if (!executeStream)
|
|
3808
|
+
if (!executeStream) {
|
|
3809
|
+
throw new ToolError({
|
|
3810
|
+
message: "fetchTool: stream execution unavailable",
|
|
3811
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
3812
|
+
toolName: "fetch"
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3685
3815
|
for await (const ev of executeStream(input, ctx, opts)) {
|
|
3686
3816
|
if (ev.type === "final") final = ev.output;
|
|
3687
3817
|
}
|
|
3688
|
-
if (!final)
|
|
3818
|
+
if (!final) {
|
|
3819
|
+
throw new ToolError({
|
|
3820
|
+
message: "fetch: stream ended without final event",
|
|
3821
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
3822
|
+
toolName: "fetch"
|
|
3823
|
+
});
|
|
3824
|
+
}
|
|
3689
3825
|
return final;
|
|
3690
3826
|
},
|
|
3691
3827
|
async *executeStream(input, ctx, opts) {
|
|
3692
|
-
if (!input?.url)
|
|
3828
|
+
if (!input?.url) {
|
|
3829
|
+
throw new ToolValidationError({
|
|
3830
|
+
message: "fetch: url is required",
|
|
3831
|
+
field: "url"
|
|
3832
|
+
});
|
|
3833
|
+
}
|
|
3693
3834
|
const u = new URL(input.url);
|
|
3694
3835
|
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
3695
|
-
throw new
|
|
3836
|
+
throw new ToolValidationError({
|
|
3837
|
+
message: `fetch: unsupported protocol "${u.protocol}"`,
|
|
3838
|
+
field: "url"
|
|
3839
|
+
});
|
|
3696
3840
|
}
|
|
3697
3841
|
if (u.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
3698
|
-
throw new
|
|
3842
|
+
throw new ToolValidationError({
|
|
3843
|
+
message: "fetch: http:// blocked (HTTPS required by default)",
|
|
3844
|
+
field: "url"
|
|
3845
|
+
});
|
|
3699
3846
|
}
|
|
3700
3847
|
await assertNotPrivate(u.hostname);
|
|
3701
3848
|
yield { type: "log", text: `GET ${input.url}` };
|
|
3702
3849
|
const ctrl = new AbortController();
|
|
3703
|
-
const timer = setTimeout(() => ctrl.abort(new
|
|
3850
|
+
const timer = setTimeout(() => ctrl.abort(new ToolError({
|
|
3851
|
+
message: "fetch timeout",
|
|
3852
|
+
code: "TOOL_TIMEOUT",
|
|
3853
|
+
toolName: "fetch"
|
|
3854
|
+
})), TIMEOUT_MS);
|
|
3704
3855
|
const combined = combineSignals([opts.signal, ctrl.signal]);
|
|
3705
3856
|
try {
|
|
3706
3857
|
let res;
|
|
@@ -3712,7 +3863,11 @@ var fetchTool = {
|
|
|
3712
3863
|
}
|
|
3713
3864
|
const ct = res.headers.get("content-type") ?? "application/octet-stream";
|
|
3714
3865
|
if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
|
|
3715
|
-
throw new
|
|
3866
|
+
throw new FetchError({
|
|
3867
|
+
message: `fetch: refusing to read binary content-type "${ct}"`,
|
|
3868
|
+
status: res.status,
|
|
3869
|
+
context: { url: res.url, contentType: ct }
|
|
3870
|
+
});
|
|
3716
3871
|
}
|
|
3717
3872
|
yield {
|
|
3718
3873
|
type: "log",
|
|
@@ -3777,16 +3932,25 @@ async function assertNotPrivate(hostname4) {
|
|
|
3777
3932
|
if (ALLOW_PRIVATE) return;
|
|
3778
3933
|
const host = hostname4.startsWith("[") && hostname4.endsWith("]") ? hostname4.slice(1, -1) : hostname4;
|
|
3779
3934
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
3780
|
-
throw new
|
|
3935
|
+
throw new ToolValidationError({
|
|
3936
|
+
message: "fetch: blocked localhost target",
|
|
3937
|
+
field: "url"
|
|
3938
|
+
});
|
|
3781
3939
|
}
|
|
3782
3940
|
const ipVersion = net.isIP(host);
|
|
3783
3941
|
if (ipVersion === 4) {
|
|
3784
3942
|
if (isPrivateIPv4(host)) {
|
|
3785
|
-
throw new
|
|
3943
|
+
throw new ToolValidationError({
|
|
3944
|
+
message: `fetch: blocked private/loopback address "${host}"`,
|
|
3945
|
+
field: "url"
|
|
3946
|
+
});
|
|
3786
3947
|
}
|
|
3787
3948
|
} else if (ipVersion === 6) {
|
|
3788
3949
|
if (isPrivateIPv6(host)) {
|
|
3789
|
-
throw new
|
|
3950
|
+
throw new ToolValidationError({
|
|
3951
|
+
message: `fetch: blocked private/loopback address "${host}"`,
|
|
3952
|
+
field: "url"
|
|
3953
|
+
});
|
|
3790
3954
|
}
|
|
3791
3955
|
} else {
|
|
3792
3956
|
try {
|
|
@@ -3794,7 +3958,10 @@ async function assertNotPrivate(hostname4) {
|
|
|
3794
3958
|
for (const r of records) {
|
|
3795
3959
|
const bad = r.family === 4 ? isPrivateIPv4(r.address) : isPrivateIPv6(r.address);
|
|
3796
3960
|
if (bad) {
|
|
3797
|
-
throw new
|
|
3961
|
+
throw new ToolValidationError({
|
|
3962
|
+
message: `fetch: resolved to private address ${r.address}`,
|
|
3963
|
+
field: "url"
|
|
3964
|
+
});
|
|
3798
3965
|
}
|
|
3799
3966
|
}
|
|
3800
3967
|
} catch (err) {
|
|
@@ -3804,7 +3971,13 @@ async function assertNotPrivate(hostname4) {
|
|
|
3804
3971
|
}
|
|
3805
3972
|
function describeFetchError(err, url, timedOut) {
|
|
3806
3973
|
if (timedOut) {
|
|
3807
|
-
return new
|
|
3974
|
+
return new ToolError({
|
|
3975
|
+
message: `fetch: GET ${url} timed out after ${TIMEOUT_MS}ms`,
|
|
3976
|
+
code: "TOOL_TIMEOUT",
|
|
3977
|
+
toolName: "fetch",
|
|
3978
|
+
context: { url, timedOut: true, timeoutMs: TIMEOUT_MS },
|
|
3979
|
+
cause: err
|
|
3980
|
+
});
|
|
3808
3981
|
}
|
|
3809
3982
|
const parts = [];
|
|
3810
3983
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3817,7 +3990,15 @@ function describeFetchError(err, url, timedOut) {
|
|
|
3817
3990
|
cur = cur.cause;
|
|
3818
3991
|
}
|
|
3819
3992
|
const detail = parts.length > 0 ? parts.join(" \u2192 ") : "fetch failed";
|
|
3820
|
-
return new
|
|
3993
|
+
return new FetchError({
|
|
3994
|
+
message: `fetch: GET ${url} failed \u2014 ${detail}`,
|
|
3995
|
+
status: 502,
|
|
3996
|
+
context: { url, timedOut: false, transportErrors: parts },
|
|
3997
|
+
// Preserve the original undici / DNS / TLS chain so callers can inspect
|
|
3998
|
+
// it via `err.cause` and structured `instanceof` checks. The flattened
|
|
3999
|
+
// text version stays in the message for human readability.
|
|
4000
|
+
cause: err
|
|
4001
|
+
});
|
|
3821
4002
|
}
|
|
3822
4003
|
function prettyJson(s) {
|
|
3823
4004
|
try {
|
|
@@ -3829,11 +4010,13 @@ function prettyJson(s) {
|
|
|
3829
4010
|
var DEFAULT_NUM = 10;
|
|
3830
4011
|
var MAX_RESULTS = 50;
|
|
3831
4012
|
var TIMEOUT_MS2 = 15e3;
|
|
4013
|
+
var CACHE_TTL_MS = 3e5;
|
|
4014
|
+
var cache = /* @__PURE__ */ new Map();
|
|
3832
4015
|
var searchTool = {
|
|
3833
4016
|
name: "search",
|
|
3834
4017
|
category: "Search",
|
|
3835
|
-
description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase.",
|
|
3836
|
-
usageHint: "Good for: API documentation, error messages, library usage examples, current best practices.\n\n- Prefer specific queries over very broad ones.\n- Results go through the guarded fetch system (same protections as the `fetch` tool).\n- This is often better than the model trying to recall outdated knowledge.",
|
|
4018
|
+
description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.",
|
|
4019
|
+
usageHint: "Good for: API documentation, error messages, library usage examples, current best practices.\n\n- Prefer specific queries over very broad ones.\n- Results go through the guarded fetch system (same protections as the `fetch` tool).\n- Supports duckduckgo (default), google, and bing sources.\n- Set `skip_cache: true` to force a fresh search.\n- This is often better than the model trying to recall outdated knowledge.",
|
|
3837
4020
|
permission: "confirm",
|
|
3838
4021
|
mutating: false,
|
|
3839
4022
|
capabilities: ["net.outbound"],
|
|
@@ -3853,6 +4036,10 @@ var searchTool = {
|
|
|
3853
4036
|
type: "string",
|
|
3854
4037
|
enum: ["duckduckgo", "google", "bing"],
|
|
3855
4038
|
description: "Search engine to use (default: duckduckgo)"
|
|
4039
|
+
},
|
|
4040
|
+
skip_cache: {
|
|
4041
|
+
type: "boolean",
|
|
4042
|
+
description: "Skip the in-memory cache and force a fresh search (default: false)"
|
|
3856
4043
|
}
|
|
3857
4044
|
},
|
|
3858
4045
|
required: ["query"]
|
|
@@ -3868,57 +4055,135 @@ var searchTool = {
|
|
|
3868
4055
|
return final;
|
|
3869
4056
|
},
|
|
3870
4057
|
async *executeStream(input, _ctx, opts) {
|
|
3871
|
-
if (!input?.query
|
|
4058
|
+
if (!input?.query || input.query.trim() === "") {
|
|
4059
|
+
throw new ToolValidationError({
|
|
4060
|
+
message: "search: query is required and must be a non-empty string",
|
|
4061
|
+
field: "query"
|
|
4062
|
+
});
|
|
4063
|
+
}
|
|
3872
4064
|
const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));
|
|
3873
4065
|
const source = input.source ?? "duckduckgo";
|
|
4066
|
+
const skipCache = input.skip_cache ?? false;
|
|
4067
|
+
const cacheKey = `${source}:${input.query}`;
|
|
4068
|
+
if (!skipCache) {
|
|
4069
|
+
const entry = cache.get(cacheKey);
|
|
4070
|
+
if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {
|
|
4071
|
+
const results = entry.results.map((r) => ({
|
|
4072
|
+
title: r.title,
|
|
4073
|
+
url: r.url,
|
|
4074
|
+
snippet: r.snippet
|
|
4075
|
+
}));
|
|
4076
|
+
yield {
|
|
4077
|
+
type: "log",
|
|
4078
|
+
text: `Cache hit for "${input.query}" (${source})`,
|
|
4079
|
+
data: { source, query: input.query, cached: true }
|
|
4080
|
+
};
|
|
4081
|
+
yield {
|
|
4082
|
+
type: "partial_output",
|
|
4083
|
+
text: `${results.length} cached results from ${source}`,
|
|
4084
|
+
data: { count: results.length, cached: true }
|
|
4085
|
+
};
|
|
4086
|
+
yield {
|
|
4087
|
+
type: "final",
|
|
4088
|
+
output: {
|
|
4089
|
+
query: input.query,
|
|
4090
|
+
results: results.slice(0, num),
|
|
4091
|
+
source,
|
|
4092
|
+
truncated: results.length >= num,
|
|
4093
|
+
cached: true
|
|
4094
|
+
}
|
|
4095
|
+
};
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
3874
4099
|
yield {
|
|
3875
4100
|
type: "log",
|
|
3876
4101
|
text: `Querying ${source} for "${input.query}"\u2026`,
|
|
3877
|
-
data: { source, query: input.query }
|
|
4102
|
+
data: { source, query: input.query, cached: false }
|
|
3878
4103
|
};
|
|
3879
|
-
let
|
|
4104
|
+
let rawResults;
|
|
3880
4105
|
switch (source) {
|
|
3881
4106
|
case "duckduckgo":
|
|
3882
|
-
|
|
4107
|
+
rawResults = await duckduckgoSearch(input.query, num, opts.signal);
|
|
3883
4108
|
break;
|
|
3884
4109
|
case "google":
|
|
3885
|
-
|
|
4110
|
+
rawResults = await googleSearch(input.query, num, opts.signal);
|
|
3886
4111
|
break;
|
|
3887
4112
|
case "bing":
|
|
3888
|
-
|
|
4113
|
+
rawResults = await bingSearch(input.query, num, opts.signal);
|
|
3889
4114
|
break;
|
|
3890
4115
|
default:
|
|
3891
|
-
throw new
|
|
4116
|
+
throw new ToolValidationError({
|
|
4117
|
+
message: `search: unknown source "${source}"`,
|
|
4118
|
+
field: "source"
|
|
4119
|
+
});
|
|
3892
4120
|
}
|
|
4121
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
4122
|
+
const deduped = [];
|
|
4123
|
+
for (const r of rawResults) {
|
|
4124
|
+
const noQuery = r.url.split("?")[0] ?? r.url;
|
|
4125
|
+
const normalized = noQuery.split("#")[0] ?? r.url;
|
|
4126
|
+
if (!seenUrls.has(normalized) && r.url.startsWith("http")) {
|
|
4127
|
+
seenUrls.add(normalized);
|
|
4128
|
+
deduped.push(r);
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
const ranked = scoreResults(deduped, input.query);
|
|
4132
|
+
const finalResults = ranked.slice(0, num);
|
|
4133
|
+
cache.set(cacheKey, { results: ranked, timestamp: Date.now() });
|
|
4134
|
+
pruneStaleCacheEntries();
|
|
3893
4135
|
yield {
|
|
3894
4136
|
type: "partial_output",
|
|
3895
|
-
text: `${
|
|
3896
|
-
data: { count:
|
|
4137
|
+
text: `${finalResults.length} results from ${source}`,
|
|
4138
|
+
data: { count: finalResults.length, cached: false }
|
|
4139
|
+
};
|
|
4140
|
+
yield {
|
|
4141
|
+
type: "final",
|
|
4142
|
+
output: {
|
|
4143
|
+
query: input.query,
|
|
4144
|
+
results: finalResults.map((r) => ({
|
|
4145
|
+
title: r.title,
|
|
4146
|
+
url: r.url,
|
|
4147
|
+
snippet: r.snippet
|
|
4148
|
+
})),
|
|
4149
|
+
source,
|
|
4150
|
+
truncated: finalResults.length >= num,
|
|
4151
|
+
cached: false
|
|
4152
|
+
}
|
|
3897
4153
|
};
|
|
3898
|
-
yield { type: "final", output };
|
|
3899
4154
|
}
|
|
3900
4155
|
};
|
|
3901
|
-
|
|
3902
|
-
const
|
|
4156
|
+
function pruneStaleCacheEntries() {
|
|
4157
|
+
const cutoff = Date.now() - CACHE_TTL_MS * 2;
|
|
4158
|
+
for (const [key, entry] of cache.entries()) {
|
|
4159
|
+
if (entry.timestamp < cutoff) cache.delete(key);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
function scoreResults(results, query) {
|
|
4163
|
+
const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
|
|
4164
|
+
return results.map((r) => {
|
|
4165
|
+
const titleLower = r.title.toLowerCase();
|
|
4166
|
+
const snippetLower = r.snippet.toLowerCase();
|
|
4167
|
+
let score = r.score;
|
|
4168
|
+
for (const term of terms) {
|
|
4169
|
+
if (titleLower.includes(term)) score += 2;
|
|
4170
|
+
if (snippetLower.includes(term)) score += 1;
|
|
4171
|
+
}
|
|
4172
|
+
return { ...r, score };
|
|
4173
|
+
}).sort((a, b) => b.score - a.score);
|
|
4174
|
+
}
|
|
4175
|
+
async function duckduckgoSearch(query, num, signal) {
|
|
4176
|
+
const encoded = encodeURIComponent(query);
|
|
3903
4177
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
3904
4178
|
try {
|
|
3905
4179
|
const response = await fetchWithTimeout(url, signal, TIMEOUT_MS2);
|
|
3906
4180
|
const html = await response.text();
|
|
3907
|
-
|
|
3908
|
-
return {
|
|
3909
|
-
query: query2,
|
|
3910
|
-
results,
|
|
3911
|
-
source: "duckduckgo",
|
|
3912
|
-
truncated: results.length >= num
|
|
3913
|
-
};
|
|
4181
|
+
return parseDuckDuckGo(html, num);
|
|
3914
4182
|
} catch (err) {
|
|
3915
|
-
console.log(
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
source: "duckduckgo",
|
|
3920
|
-
truncated: false
|
|
3921
|
-
};
|
|
4183
|
+
console.log(
|
|
4184
|
+
JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage$2(err) })
|
|
4185
|
+
);
|
|
4186
|
+
return [{ title: "Search unavailable", url: "https://duckduckgo.com/unavailable", snippet: "Could not reach DuckDuckGo", score: 0 }];
|
|
3922
4187
|
}
|
|
3923
4188
|
}
|
|
3924
4189
|
function takeFrom(iter, max) {
|
|
@@ -3943,25 +4208,22 @@ function parseDuckDuckGo(html, num) {
|
|
|
3943
4208
|
);
|
|
3944
4209
|
for (let i = 0; i < linkMatches.length && i < num; i++) {
|
|
3945
4210
|
const entry = linkMatches[i];
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
4211
|
+
if (entry) {
|
|
4212
|
+
results.push({
|
|
4213
|
+
title: entry.title ?? "",
|
|
4214
|
+
url: entry.url ?? "",
|
|
4215
|
+
snippet: snippetMatches[i] ?? "",
|
|
4216
|
+
score: 1
|
|
4217
|
+
});
|
|
4218
|
+
}
|
|
3951
4219
|
}
|
|
3952
4220
|
return results;
|
|
3953
4221
|
}
|
|
3954
|
-
async function googleSearch(
|
|
3955
|
-
const encoded = encodeURIComponent(
|
|
4222
|
+
async function googleSearch(query, num, signal) {
|
|
4223
|
+
const encoded = encodeURIComponent(query);
|
|
3956
4224
|
const url = `https://www.google.com/search?q=${encoded}&hl=en`;
|
|
3957
4225
|
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS2).then((r) => r.text()).catch(() => "");
|
|
3958
|
-
|
|
3959
|
-
return {
|
|
3960
|
-
query: query2,
|
|
3961
|
-
results,
|
|
3962
|
-
source: "google",
|
|
3963
|
-
truncated: results.length >= num
|
|
3964
|
-
};
|
|
4226
|
+
return parseGoogleResults(html, num);
|
|
3965
4227
|
}
|
|
3966
4228
|
function parseGoogleResults(html, num) {
|
|
3967
4229
|
const results = [];
|
|
@@ -3984,22 +4246,17 @@ function parseGoogleResults(html, num) {
|
|
|
3984
4246
|
results.push({
|
|
3985
4247
|
title: titles[i] ?? "",
|
|
3986
4248
|
url: urls[i] ?? "",
|
|
3987
|
-
snippet: snippets[i] ?? ""
|
|
4249
|
+
snippet: snippets[i] ?? "",
|
|
4250
|
+
score: 1
|
|
3988
4251
|
});
|
|
3989
4252
|
}
|
|
3990
4253
|
return results;
|
|
3991
4254
|
}
|
|
3992
|
-
async function bingSearch(
|
|
3993
|
-
const encoded = encodeURIComponent(
|
|
4255
|
+
async function bingSearch(query, num, signal) {
|
|
4256
|
+
const encoded = encodeURIComponent(query);
|
|
3994
4257
|
const url = `https://www.bing.com/search?q=${encoded}`;
|
|
3995
4258
|
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS2).then((r) => r.text()).catch(() => "");
|
|
3996
|
-
|
|
3997
|
-
return {
|
|
3998
|
-
query: query2,
|
|
3999
|
-
results,
|
|
4000
|
-
source: "bing",
|
|
4001
|
-
truncated: results.length >= num
|
|
4002
|
-
};
|
|
4259
|
+
return parseBingResults(html, num);
|
|
4003
4260
|
}
|
|
4004
4261
|
function parseBingResults(html, num) {
|
|
4005
4262
|
const results = [];
|
|
@@ -4014,11 +4271,15 @@ function parseBingResults(html, num) {
|
|
|
4014
4271
|
num
|
|
4015
4272
|
);
|
|
4016
4273
|
for (let i = 0; i < entries.length; i++) {
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4274
|
+
const entry = entries[i];
|
|
4275
|
+
if (entry) {
|
|
4276
|
+
results.push({
|
|
4277
|
+
title: entry.title ?? "",
|
|
4278
|
+
url: entry.url ?? "",
|
|
4279
|
+
snippet: snippets[i] ?? "",
|
|
4280
|
+
score: 1
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
4022
4283
|
}
|
|
4023
4284
|
return results;
|
|
4024
4285
|
}
|
|
@@ -4034,7 +4295,15 @@ async function fetchWithTimeout(url, signal, timeoutMs) {
|
|
|
4034
4295
|
return res;
|
|
4035
4296
|
} catch (e) {
|
|
4036
4297
|
clearTimeout(timer);
|
|
4037
|
-
|
|
4298
|
+
if (e instanceof FetchError) {
|
|
4299
|
+
throw e;
|
|
4300
|
+
}
|
|
4301
|
+
throw new FetchError({
|
|
4302
|
+
message: `search: failed to fetch ${url}`,
|
|
4303
|
+
status: 0,
|
|
4304
|
+
context: { url },
|
|
4305
|
+
cause: e
|
|
4306
|
+
});
|
|
4038
4307
|
}
|
|
4039
4308
|
}
|
|
4040
4309
|
function anySignal(...signals) {
|
|
@@ -4803,8 +5072,8 @@ function extractPatchedFiles(output) {
|
|
|
4803
5072
|
var jsonTool = {
|
|
4804
5073
|
name: "json",
|
|
4805
5074
|
category: "Data",
|
|
4806
|
-
description: "Parse, pretty-print, query,
|
|
4807
|
-
usageHint:
|
|
5075
|
+
description: "Parse, pretty-print, query, validate, transform, and merge JSON/JSON5/YAML. Use `action` to select the operation: parse (default), query, validate, transform, or merge.",
|
|
5076
|
+
usageHint: 'VERY USEFUL FOR DATA INSPECTION:\n\n- `action: "parse"` (default): read/pretty-print/convert JSON, JSON5, or YAML from `file` or `data`.\n- `action: "query"`: JMESPath-like query (`a.b[0].c`, `items[*].name`, filters, functions).\n- `action: "validate"`: validate data against a JSON Schema (`schema` param).\n- `action: "transform"`: chain multiple JMESPath transforms (`transforms` param).\n- `action: "merge"`: deep merge `base` and `patch` objects (`conflictResolution` param).\nPrefer this over raw `read` + manual parsing when dealing with configuration or data files.',
|
|
4808
5077
|
permission: "auto",
|
|
4809
5078
|
mutating: false,
|
|
4810
5079
|
timeoutMs: 5e3,
|
|
@@ -4813,69 +5082,420 @@ var jsonTool = {
|
|
|
4813
5082
|
inputSchema: {
|
|
4814
5083
|
type: "object",
|
|
4815
5084
|
properties: {
|
|
4816
|
-
|
|
4817
|
-
data: { type: "string", description: "JSON/JSON5/YAML string (alternative to file)" },
|
|
4818
|
-
query: {
|
|
5085
|
+
action: {
|
|
4819
5086
|
type: "string",
|
|
4820
|
-
|
|
5087
|
+
enum: ["parse", "query", "validate", "transform", "merge"],
|
|
5088
|
+
description: "Operation (default: parse). parse=read/pretty-print, query=JMESPath, validate=schema, transform=chained queries, merge=deep merge."
|
|
4821
5089
|
},
|
|
5090
|
+
file: { type: "string", description: "Path to JSON/JSON5/YAML file (parse/query/validate)" },
|
|
5091
|
+
data: { type: "string", description: "JSON/JSON5/YAML string (parse/query/validate, alternative to file)" },
|
|
4822
5092
|
format: {
|
|
4823
5093
|
type: "string",
|
|
4824
5094
|
enum: ["json", "json5", "yaml"],
|
|
4825
|
-
description: "Output format (default: json)"
|
|
5095
|
+
description: "Output format for parse/query/transform (default: json)"
|
|
5096
|
+
},
|
|
5097
|
+
query: {
|
|
5098
|
+
type: "string",
|
|
5099
|
+
description: "JMESPath-like query expression (query action)"
|
|
5100
|
+
},
|
|
5101
|
+
transforms: {
|
|
5102
|
+
type: "array",
|
|
5103
|
+
items: { type: "string" },
|
|
5104
|
+
description: "Ordered JMESPath query strings (transform action)"
|
|
5105
|
+
},
|
|
5106
|
+
schema: {
|
|
5107
|
+
type: "object",
|
|
5108
|
+
description: "JSON Schema to validate against (validate action)"
|
|
5109
|
+
},
|
|
5110
|
+
base: { description: "Base JSON object (merge action)" },
|
|
5111
|
+
patch: { description: "Patch JSON object to merge in (merge action)" },
|
|
5112
|
+
conflictResolution: {
|
|
5113
|
+
type: "string",
|
|
5114
|
+
enum: ["prefer-base", "prefer-patch"],
|
|
5115
|
+
description: "Merge conflict resolution (default: prefer-patch)"
|
|
4826
5116
|
},
|
|
4827
5117
|
validate: {
|
|
4828
5118
|
type: "boolean",
|
|
4829
|
-
description: "Validate syntax only, no output (default: false)"
|
|
5119
|
+
description: "Validate syntax only, no output (parse action, default: false)"
|
|
4830
5120
|
}
|
|
4831
5121
|
}
|
|
4832
5122
|
},
|
|
4833
5123
|
async execute(input) {
|
|
4834
|
-
const
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
return
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
5124
|
+
const action = input.action ?? "parse";
|
|
5125
|
+
switch (action) {
|
|
5126
|
+
case "query":
|
|
5127
|
+
return executeQuery(input);
|
|
5128
|
+
case "validate":
|
|
5129
|
+
return executeValidate(input);
|
|
5130
|
+
case "transform":
|
|
5131
|
+
return executeTransform(input);
|
|
5132
|
+
case "merge":
|
|
5133
|
+
return executeMerge(input);
|
|
5134
|
+
case "parse":
|
|
5135
|
+
default:
|
|
5136
|
+
return executeParse(input);
|
|
4847
5137
|
}
|
|
5138
|
+
}
|
|
5139
|
+
};
|
|
5140
|
+
async function executeParse(input) {
|
|
5141
|
+
const format = input.format ?? "json";
|
|
5142
|
+
let parsed;
|
|
5143
|
+
let raw;
|
|
5144
|
+
if (input.file) {
|
|
4848
5145
|
try {
|
|
4849
|
-
|
|
4850
|
-
} catch
|
|
4851
|
-
return {
|
|
4852
|
-
data: null,
|
|
4853
|
-
formatted: "",
|
|
4854
|
-
type: "unknown",
|
|
4855
|
-
/* v8 ignore next -- JSON.parse only throws SyntaxError (an Error); the String(e) side is defensive. */
|
|
4856
|
-
error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
4857
|
-
};
|
|
4858
|
-
}
|
|
4859
|
-
if (input.validate) {
|
|
4860
|
-
return {
|
|
4861
|
-
data: parsed,
|
|
4862
|
-
formatted: "valid",
|
|
4863
|
-
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
4864
|
-
keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
|
|
4865
|
-
};
|
|
5146
|
+
raw = await fs7.readFile(input.file, "utf8");
|
|
5147
|
+
} catch {
|
|
5148
|
+
return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
|
|
4866
5149
|
}
|
|
4867
|
-
|
|
4868
|
-
|
|
5150
|
+
} else if (input.data) {
|
|
5151
|
+
raw = input.data;
|
|
5152
|
+
} else {
|
|
5153
|
+
return { data: null, formatted: "", type: "unknown", action: "parse", error: "Provide file or data" };
|
|
5154
|
+
}
|
|
5155
|
+
try {
|
|
5156
|
+
parsed = JSON.parse(raw);
|
|
5157
|
+
} catch (e) {
|
|
5158
|
+
return {
|
|
5159
|
+
data: null,
|
|
5160
|
+
formatted: "",
|
|
5161
|
+
type: "unknown",
|
|
5162
|
+
action: "parse",
|
|
5163
|
+
/* v8 ignore next -- JSON.parse only throws SyntaxError (an Error); the String(e) side is defensive. */
|
|
5164
|
+
error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
5165
|
+
};
|
|
5166
|
+
}
|
|
5167
|
+
if (input.validate) {
|
|
5168
|
+
return {
|
|
5169
|
+
data: parsed,
|
|
5170
|
+
formatted: "valid",
|
|
5171
|
+
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
5172
|
+
action: "parse",
|
|
5173
|
+
keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
|
|
5174
|
+
};
|
|
5175
|
+
}
|
|
5176
|
+
if (input.query) {
|
|
5177
|
+
const queryResult = simpleQuery(parsed, input.query);
|
|
5178
|
+
const formatted2 = formatOutput(queryResult, format);
|
|
4869
5179
|
return {
|
|
4870
5180
|
data: parsed,
|
|
4871
|
-
formatted,
|
|
5181
|
+
formatted: formatted2,
|
|
4872
5182
|
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
5183
|
+
action: "parse",
|
|
4873
5184
|
keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0,
|
|
4874
5185
|
query_result: queryResult
|
|
4875
5186
|
};
|
|
4876
5187
|
}
|
|
4877
|
-
|
|
4878
|
-
|
|
5188
|
+
const formatted = formatOutput(parsed, format);
|
|
5189
|
+
return {
|
|
5190
|
+
data: parsed,
|
|
5191
|
+
formatted,
|
|
5192
|
+
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
5193
|
+
action: "parse",
|
|
5194
|
+
keys: typeof parsed === "object" && parsed !== null ? Object.keys(parsed) : void 0
|
|
5195
|
+
};
|
|
5196
|
+
}
|
|
5197
|
+
async function executeQuery(input) {
|
|
5198
|
+
if (!input.query) {
|
|
5199
|
+
return { data: null, formatted: "", type: "unknown", action: "query", error: "query is required for action: query" };
|
|
5200
|
+
}
|
|
5201
|
+
let parsed;
|
|
5202
|
+
if (input.file) {
|
|
5203
|
+
try {
|
|
5204
|
+
const raw = await fs7.readFile(input.file, "utf8");
|
|
5205
|
+
parsed = JSON.parse(raw);
|
|
5206
|
+
} catch {
|
|
5207
|
+
return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
|
|
5208
|
+
}
|
|
5209
|
+
} else if (input.data) {
|
|
5210
|
+
try {
|
|
5211
|
+
parsed = JSON.parse(input.data);
|
|
5212
|
+
} catch {
|
|
5213
|
+
return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not parse data string" };
|
|
5214
|
+
}
|
|
5215
|
+
} else {
|
|
5216
|
+
return { data: null, formatted: "", type: "unknown", action: "query", error: "Provide file or data" };
|
|
5217
|
+
}
|
|
5218
|
+
try {
|
|
5219
|
+
const result = jmespathSearch(parsed, input.query);
|
|
5220
|
+
const format = input.format ?? "json";
|
|
5221
|
+
return {
|
|
5222
|
+
data: parsed,
|
|
5223
|
+
formatted: formatOutput(result, format),
|
|
5224
|
+
type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result,
|
|
5225
|
+
action: "query",
|
|
5226
|
+
query_result: result
|
|
5227
|
+
};
|
|
5228
|
+
} catch (e) {
|
|
5229
|
+
return {
|
|
5230
|
+
data: null,
|
|
5231
|
+
formatted: "",
|
|
5232
|
+
type: "unknown",
|
|
5233
|
+
action: "query",
|
|
5234
|
+
/* v8 ignore next -- defensive String(e) */
|
|
5235
|
+
error: `Query failed: ${e instanceof Error ? e.message : String(e)}`
|
|
5236
|
+
};
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
async function executeValidate(input) {
|
|
5240
|
+
if (!input.schema) {
|
|
5241
|
+
return { data: null, formatted: "", type: "unknown", action: "validate", error: "schema is required for action: validate" };
|
|
5242
|
+
}
|
|
5243
|
+
let parsed;
|
|
5244
|
+
if (input.file) {
|
|
5245
|
+
try {
|
|
5246
|
+
const raw = await fs7.readFile(input.file, "utf8");
|
|
5247
|
+
parsed = JSON.parse(raw);
|
|
5248
|
+
} catch {
|
|
5249
|
+
return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
|
|
5250
|
+
}
|
|
5251
|
+
} else if (input.data) {
|
|
5252
|
+
try {
|
|
5253
|
+
parsed = JSON.parse(input.data);
|
|
5254
|
+
} catch {
|
|
5255
|
+
return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not parse data string" };
|
|
5256
|
+
}
|
|
5257
|
+
} else {
|
|
5258
|
+
return { data: null, formatted: "", type: "unknown", action: "validate", error: "Provide file or data" };
|
|
5259
|
+
}
|
|
5260
|
+
try {
|
|
5261
|
+
const { valid, errors } = validateJsonSchema(parsed, input.schema);
|
|
5262
|
+
return {
|
|
5263
|
+
data: parsed,
|
|
5264
|
+
formatted: valid ? "valid" : "invalid",
|
|
5265
|
+
type: Array.isArray(parsed) ? "array" : typeof parsed,
|
|
5266
|
+
action: "validate",
|
|
5267
|
+
valid,
|
|
5268
|
+
errors
|
|
5269
|
+
};
|
|
5270
|
+
} catch (e) {
|
|
5271
|
+
return {
|
|
5272
|
+
data: null,
|
|
5273
|
+
formatted: "",
|
|
5274
|
+
type: "unknown",
|
|
5275
|
+
action: "validate",
|
|
5276
|
+
/* v8 ignore next -- defensive String(e) */
|
|
5277
|
+
error: `Validation failed: ${e instanceof Error ? e.message : String(e)}`
|
|
5278
|
+
};
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
async function executeTransform(input) {
|
|
5282
|
+
if (!input.transforms || input.transforms.length === 0) {
|
|
5283
|
+
return { data: null, formatted: "", type: "unknown", action: "transform", error: "transforms array is required for action: transform" };
|
|
5284
|
+
}
|
|
5285
|
+
let parsed;
|
|
5286
|
+
if (input.file) {
|
|
5287
|
+
try {
|
|
5288
|
+
const raw = await fs7.readFile(input.file, "utf8");
|
|
5289
|
+
parsed = JSON.parse(raw);
|
|
5290
|
+
} catch {
|
|
5291
|
+
return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
|
|
5292
|
+
}
|
|
5293
|
+
} else if (input.data) {
|
|
5294
|
+
try {
|
|
5295
|
+
parsed = JSON.parse(input.data);
|
|
5296
|
+
} catch {
|
|
5297
|
+
return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not parse data string" };
|
|
5298
|
+
}
|
|
5299
|
+
} else {
|
|
5300
|
+
return { data: null, formatted: "", type: "unknown", action: "transform", error: "Provide file or data" };
|
|
5301
|
+
}
|
|
5302
|
+
try {
|
|
5303
|
+
let current = parsed;
|
|
5304
|
+
const steps = [];
|
|
5305
|
+
for (const t of input.transforms) {
|
|
5306
|
+
current = jmespathSearch(current, t);
|
|
5307
|
+
steps.push({ transform: t, result: current });
|
|
5308
|
+
}
|
|
5309
|
+
const format = input.format ?? "json";
|
|
5310
|
+
return {
|
|
5311
|
+
data: parsed,
|
|
5312
|
+
formatted: formatOutput(current, format),
|
|
5313
|
+
type: current === null ? "null" : Array.isArray(current) ? "array" : typeof current,
|
|
5314
|
+
action: "transform",
|
|
5315
|
+
result: current,
|
|
5316
|
+
steps
|
|
5317
|
+
};
|
|
5318
|
+
} catch (e) {
|
|
5319
|
+
return {
|
|
5320
|
+
data: null,
|
|
5321
|
+
formatted: "",
|
|
5322
|
+
type: "unknown",
|
|
5323
|
+
action: "transform",
|
|
5324
|
+
/* v8 ignore next -- defensive String(e) */
|
|
5325
|
+
error: `Transform failed: ${e instanceof Error ? e.message : String(e)}`
|
|
5326
|
+
};
|
|
5327
|
+
}
|
|
5328
|
+
}
|
|
5329
|
+
async function executeMerge(input) {
|
|
5330
|
+
if (input.base === void 0 || input.patch === void 0) {
|
|
5331
|
+
return { data: null, formatted: "", type: "unknown", action: "merge", error: "base and patch are required for action: merge" };
|
|
5332
|
+
}
|
|
5333
|
+
const conflictResolution = input.conflictResolution ?? "prefer-patch";
|
|
5334
|
+
try {
|
|
5335
|
+
const result = deepMerge(input.base, input.patch, { conflictResolution });
|
|
5336
|
+
const format = input.format ?? "json";
|
|
5337
|
+
return {
|
|
5338
|
+
data: result,
|
|
5339
|
+
formatted: formatOutput(result, format),
|
|
5340
|
+
type: result === null ? "null" : Array.isArray(result) ? "array" : typeof result,
|
|
5341
|
+
action: "merge",
|
|
5342
|
+
result
|
|
5343
|
+
};
|
|
5344
|
+
} catch (e) {
|
|
5345
|
+
return {
|
|
5346
|
+
data: null,
|
|
5347
|
+
formatted: "",
|
|
5348
|
+
type: "unknown",
|
|
5349
|
+
action: "merge",
|
|
5350
|
+
/* v8 ignore next -- defensive String(e) */
|
|
5351
|
+
error: `Merge failed: ${e instanceof Error ? e.message : String(e)}`
|
|
5352
|
+
};
|
|
5353
|
+
}
|
|
5354
|
+
}
|
|
5355
|
+
function jmespathSearch(data, query) {
|
|
5356
|
+
if (!query || query === "@") return data;
|
|
5357
|
+
if (query === "$") return data;
|
|
5358
|
+
const dotMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(?:\.(.+))?$/);
|
|
5359
|
+
if (dotMatch) {
|
|
5360
|
+
const key = dotMatch[1];
|
|
5361
|
+
const rest = dotMatch[2];
|
|
5362
|
+
const val = data?.[key];
|
|
5363
|
+
if (rest === void 0) return val;
|
|
5364
|
+
return jmespathSearch(val, rest);
|
|
5365
|
+
}
|
|
5366
|
+
const arrMatch = query.match(/^\[(\d+)\](?:\.(.+))?$/);
|
|
5367
|
+
if (arrMatch) {
|
|
5368
|
+
const idx = Number.parseInt(arrMatch[1], 10);
|
|
5369
|
+
const rest = arrMatch[2];
|
|
5370
|
+
const arr = data;
|
|
5371
|
+
const val = arr?.[idx];
|
|
5372
|
+
if (rest === void 0) return val;
|
|
5373
|
+
return jmespathSearch(val, rest);
|
|
5374
|
+
}
|
|
5375
|
+
if (query === "[*]") {
|
|
5376
|
+
if (Array.isArray(data)) {
|
|
5377
|
+
return data;
|
|
5378
|
+
}
|
|
5379
|
+
return data;
|
|
5380
|
+
}
|
|
5381
|
+
const multiMatch = query.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\[\*\](?:\.(.+))?$/);
|
|
5382
|
+
if (multiMatch) {
|
|
5383
|
+
const key = multiMatch[1];
|
|
5384
|
+
const rest = multiMatch[2];
|
|
5385
|
+
const arr = data?.[key];
|
|
5386
|
+
if (!Array.isArray(arr)) return [];
|
|
5387
|
+
if (rest === void 0) return arr;
|
|
5388
|
+
return arr.map((item) => jmespathSearch(item, rest));
|
|
5389
|
+
}
|
|
5390
|
+
const filterMatch = query.match(/^\[\\?([a-zA-Z_][a-zA-Z0-9_]*)(==|!=|<|>|<=|>=)(`[^`]+`|'[^']*')\](?:\.(.+))?$/);
|
|
5391
|
+
if (filterMatch) {
|
|
5392
|
+
const field = filterMatch[1];
|
|
5393
|
+
const op = filterMatch[2];
|
|
5394
|
+
const rawVal = filterMatch[3];
|
|
5395
|
+
const rest = filterMatch[4];
|
|
5396
|
+
const cmpVal = JSON.parse(rawVal.slice(1, -1));
|
|
5397
|
+
const arr = data;
|
|
5398
|
+
if (!Array.isArray(arr)) return [];
|
|
5399
|
+
const filtered = arr.filter((item) => {
|
|
5400
|
+
const itemVal = item[field];
|
|
5401
|
+
switch (op) {
|
|
5402
|
+
case "==":
|
|
5403
|
+
return itemVal === cmpVal;
|
|
5404
|
+
case "!=":
|
|
5405
|
+
return itemVal !== cmpVal;
|
|
5406
|
+
case ">":
|
|
5407
|
+
return Number(itemVal) > Number(cmpVal);
|
|
5408
|
+
case "<":
|
|
5409
|
+
return Number(itemVal) < Number(cmpVal);
|
|
5410
|
+
case ">=":
|
|
5411
|
+
return Number(itemVal) >= Number(cmpVal);
|
|
5412
|
+
case "<=":
|
|
5413
|
+
return Number(itemVal) <= Number(cmpVal);
|
|
5414
|
+
/* v8 ignore next -- op is constrained to the six operators by the filter regex; default is unreachable. */
|
|
5415
|
+
default:
|
|
5416
|
+
return true;
|
|
5417
|
+
}
|
|
5418
|
+
});
|
|
5419
|
+
if (rest === void 0) return filtered;
|
|
5420
|
+
return filtered.map((item) => jmespathSearch(item, rest));
|
|
5421
|
+
}
|
|
5422
|
+
const fnMatch = query.match(/^(length|keys|values|type)\(@\)$/);
|
|
5423
|
+
if (fnMatch) {
|
|
5424
|
+
const fn = fnMatch[1];
|
|
5425
|
+
switch (fn) {
|
|
5426
|
+
case "length":
|
|
5427
|
+
if (Array.isArray(data)) return data.length;
|
|
5428
|
+
if (typeof data === "string") return data.length;
|
|
5429
|
+
if (typeof data === "object" && data !== null) return Object.keys(data).length;
|
|
5430
|
+
return 0;
|
|
5431
|
+
case "keys":
|
|
5432
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.keys(data);
|
|
5433
|
+
return [];
|
|
5434
|
+
case "values":
|
|
5435
|
+
if (typeof data === "object" && data !== null && !Array.isArray(data)) return Object.values(data);
|
|
5436
|
+
return [];
|
|
5437
|
+
case "type":
|
|
5438
|
+
if (data === null) return "null";
|
|
5439
|
+
if (Array.isArray(data)) return "array";
|
|
5440
|
+
return typeof data;
|
|
5441
|
+
/* v8 ignore next 2 -- fn is constrained to the four names by the function regex; default is unreachable. */
|
|
5442
|
+
default:
|
|
5443
|
+
return null;
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
return null;
|
|
5447
|
+
}
|
|
5448
|
+
function validateJsonSchema(data, schema) {
|
|
5449
|
+
const errors = [];
|
|
5450
|
+
function check(value, s, path22) {
|
|
5451
|
+
if (s["type"]) {
|
|
5452
|
+
const expectedType = s["type"];
|
|
5453
|
+
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
5454
|
+
if (expectedType === "integer") {
|
|
5455
|
+
if (!Number.isInteger(value)) errors.push(`${path22}: expected integer, got ${actualType}`);
|
|
5456
|
+
} else if (expectedType !== actualType) {
|
|
5457
|
+
errors.push(`${path22}: expected ${expectedType}, got ${actualType}`);
|
|
5458
|
+
}
|
|
5459
|
+
}
|
|
5460
|
+
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
5461
|
+
try {
|
|
5462
|
+
new URL(value);
|
|
5463
|
+
} catch {
|
|
5464
|
+
errors.push(`${path22}: not a valid URI`);
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
5467
|
+
if (typeof value === "string" && s["pattern"]) {
|
|
5468
|
+
const re = new RegExp(s["pattern"]);
|
|
5469
|
+
if (!re.test(value)) errors.push(`${path22}: does not match pattern ${s["pattern"]}`);
|
|
5470
|
+
}
|
|
5471
|
+
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
5472
|
+
errors.push(`${path22}: string too short (min ${s["minLength"]})`);
|
|
5473
|
+
}
|
|
5474
|
+
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
5475
|
+
errors.push(`${path22}: string too long (max ${s["maxLength"]})`);
|
|
5476
|
+
}
|
|
5477
|
+
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
5478
|
+
errors.push(`${path22}: below minimum ${s["minimum"]}`);
|
|
5479
|
+
}
|
|
5480
|
+
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
5481
|
+
errors.push(`${path22}: above maximum ${s["maximum"]}`);
|
|
5482
|
+
}
|
|
5483
|
+
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
5484
|
+
for (let i = 0; i < value.length; i++) {
|
|
5485
|
+
check(value[i], s["items"], `${path22}[${i}]`);
|
|
5486
|
+
}
|
|
5487
|
+
}
|
|
5488
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
5489
|
+
const props = s["properties"];
|
|
5490
|
+
for (const [k, propSchema] of Object.entries(props)) {
|
|
5491
|
+
check(value[k], propSchema, `${path22}.${k}`);
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
}
|
|
5495
|
+
check(data, schema, "$");
|
|
5496
|
+
return { valid: errors.length === 0, errors };
|
|
5497
|
+
}
|
|
5498
|
+
function simpleQuery(data, path22) {
|
|
4879
5499
|
const parts = path22.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
4880
5500
|
let current = data;
|
|
4881
5501
|
for (const part of parts) {
|
|
@@ -5889,6 +6509,10 @@ var installTool = {
|
|
|
5889
6509
|
global: {
|
|
5890
6510
|
type: "boolean",
|
|
5891
6511
|
description: "Whether to perform a global install (use with caution)."
|
|
6512
|
+
},
|
|
6513
|
+
lifecycleScripts: {
|
|
6514
|
+
type: "boolean",
|
|
6515
|
+
description: "Opt in to running package lifecycle scripts (preinstall / install / postinstall / prepare / \u2026). Default: false \u2014 installs pass --ignore-scripts so a malicious package cannot execute arbitrary code at install time. Set true to opt back in to the legacy npm/pnpm/yarn default."
|
|
5892
6516
|
}
|
|
5893
6517
|
}
|
|
5894
6518
|
},
|
|
@@ -5908,8 +6532,10 @@ var installTool = {
|
|
|
5908
6532
|
yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
|
|
5909
6533
|
const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
|
|
5910
6534
|
const globalFlag = input.global ? ["-g"] : [];
|
|
6535
|
+
const ignoreScripts = input.lifecycleScripts !== true;
|
|
5911
6536
|
const args = [];
|
|
5912
6537
|
if (input.dry_run) args.push("--dry-run");
|
|
6538
|
+
if (ignoreScripts) args.push("--ignore-scripts");
|
|
5913
6539
|
if (pkgManager === "pnpm") {
|
|
5914
6540
|
if (save) args.push(save);
|
|
5915
6541
|
args.push("add", ...globalFlag);
|
|
@@ -6114,8 +6740,8 @@ var outdatedTool = {
|
|
|
6114
6740
|
// read-only, but `outdated` makes outbound HTTP calls to the
|
|
6115
6741
|
// registry. The 'confirm' permission routes the call through the
|
|
6116
6742
|
// tool.confirm_needed flow on every invocation. M-1 originally
|
|
6117
|
-
// fixed four sibling tools (mcp_control, shellcheck,
|
|
6118
|
-
//
|
|
6743
|
+
// fixed four sibling tools (mcp_control, shellcheck, shellcheck (scan mode),
|
|
6744
|
+
// search) but missed this one; applying the same contract here.
|
|
6119
6745
|
mutating: true,
|
|
6120
6746
|
// Capability is outbound network — the tool only hits the package
|
|
6121
6747
|
// registry over HTTP, never touches the filesystem or runs shell.
|
|
@@ -6428,8 +7054,8 @@ function parseLine(line) {
|
|
|
6428
7054
|
var documentTool = {
|
|
6429
7055
|
name: "document",
|
|
6430
7056
|
category: "Project",
|
|
6431
|
-
description: "
|
|
6432
|
-
usageHint: "
|
|
7057
|
+
description: "DEPRECATED \u2014 use the `auto_doc` tool with `dryRun: true` instead. This tool is a read-only preview stub that returns `skipped` candidates without generating real docstrings.",
|
|
7058
|
+
usageHint: "Deprecated: prefer `auto_doc` with `dryRun: true` for previewing, or `auto_doc` without dryRun for writing. This tool only lists undocumented symbols with placeholder comments \u2014 it does not generate real JSDoc/TSDoc.",
|
|
6433
7059
|
permission: "auto",
|
|
6434
7060
|
mutating: false,
|
|
6435
7061
|
timeoutMs: 3e4,
|
|
@@ -6983,8 +7609,8 @@ Replace off-palette colors with kit tokens (or the materialized CSS vars / token
|
|
|
6983
7609
|
var toolSearchTool = {
|
|
6984
7610
|
name: "tool_search",
|
|
6985
7611
|
category: "Meta",
|
|
6986
|
-
description: "Search the catalog of available tools
|
|
6987
|
-
usageHint: "SELF-DISCOVERY TOOL:\n\n- Use when you need to find the right tool for a job.\n- `query` searches names and descriptions.\n- You can filter by `tags` (category), `permission`, or `mutating`.\nCall this before guessing tool names. It helps you discover the best tool for the current situation.",
|
|
7612
|
+
description: "Search the catalog of available tools by name or description. Use this to discover which tool to use for a task. For the full schema and usage details of a specific tool, use `tool_help` instead.",
|
|
7613
|
+
usageHint: "SELF-DISCOVERY TOOL:\n\n- Use when you need to find the right tool for a job.\n- `query` searches names and descriptions.\n- You can filter by `tags` (category), `permission`, or `mutating`.\n- Once you find the right tool name, use `tool_help` with that name for full schema details.\nCall this before guessing tool names. It helps you discover the best tool for the current situation.",
|
|
6988
7614
|
permission: "auto",
|
|
6989
7615
|
mutating: false,
|
|
6990
7616
|
timeoutMs: 1e3,
|
|
@@ -7022,9 +7648,9 @@ var toolSearchTool = {
|
|
|
7022
7648
|
async execute(input, ctx) {
|
|
7023
7649
|
const limit = Math.min(input.limit ?? 20, 100);
|
|
7024
7650
|
const tools = ctx.tools;
|
|
7025
|
-
const
|
|
7651
|
+
const query = input.query?.toLowerCase() ?? "";
|
|
7026
7652
|
const filtered = tools.filter((t) => {
|
|
7027
|
-
if (
|
|
7653
|
+
if (query && !t.name.toLowerCase().includes(query) && !t.description.toLowerCase().includes(query)) {
|
|
7028
7654
|
return false;
|
|
7029
7655
|
}
|
|
7030
7656
|
if (input.tags && input.tags.length > 0) {
|
|
@@ -7048,7 +7674,7 @@ var toolSearchTool = {
|
|
|
7048
7674
|
mutating: t.mutating
|
|
7049
7675
|
}));
|
|
7050
7676
|
const totalAvailable = tools.length;
|
|
7051
|
-
const hint = results.length === 0 &&
|
|
7677
|
+
const hint = results.length === 0 && query ? `No tools matched "${input.query}". Use tool-help (without arguments) to see all ${totalAvailable} available tools.` : void 0;
|
|
7052
7678
|
return {
|
|
7053
7679
|
tools: results,
|
|
7054
7680
|
total: filtered.length,
|
|
@@ -7240,8 +7866,8 @@ async function executeSingle(call, ctx, opts) {
|
|
|
7240
7866
|
var toolHelpTool = {
|
|
7241
7867
|
name: "tool_help",
|
|
7242
7868
|
category: "Meta",
|
|
7243
|
-
description: "Get detailed help for
|
|
7244
|
-
usageHint: "USE WHEN YOU NEED PRECISE TOOL INFORMATION:\n\n- Call with a specific `tool` name when you want the full schema and current usageHint.\n- Omit `tool`
|
|
7869
|
+
description: "Get detailed help for a specific tool, including its full input schema and usage guidance. If you do not know which tool to use, search with `tool_search` first, then call this with the tool name.",
|
|
7870
|
+
usageHint: "USE WHEN YOU NEED PRECISE TOOL INFORMATION:\n\n- Call with a specific `tool` name when you want the full schema and current usageHint.\n- Omit `tool` to get an overview of all available tools.\n- Different `format` options give you different levels of detail.\n- Tip: use `tool_search` to find the right tool name, then `tool_help` for the full schema.\nThis tool is extremely valuable for self-correction when you are unsure about a tool's interface.",
|
|
7245
7871
|
permission: "auto",
|
|
7246
7872
|
mutating: false,
|
|
7247
7873
|
timeoutMs: 5e3,
|
|
@@ -7359,8 +7985,6 @@ function formatAllToolsMarkdown(tools) {
|
|
|
7359
7985
|
}
|
|
7360
7986
|
return lines.join("\n");
|
|
7361
7987
|
}
|
|
7362
|
-
|
|
7363
|
-
// src/memory.ts
|
|
7364
7988
|
function rememberTool(memory) {
|
|
7365
7989
|
return {
|
|
7366
7990
|
name: "remember",
|
|
@@ -7403,7 +8027,12 @@ function rememberTool(memory) {
|
|
|
7403
8027
|
required: ["text"]
|
|
7404
8028
|
},
|
|
7405
8029
|
async execute(input) {
|
|
7406
|
-
if (!input?.text)
|
|
8030
|
+
if (!input?.text) {
|
|
8031
|
+
throw new ToolValidationError({
|
|
8032
|
+
message: "remember: text is required",
|
|
8033
|
+
field: "text"
|
|
8034
|
+
});
|
|
8035
|
+
}
|
|
7407
8036
|
const scope = input.scope ?? "project-memory";
|
|
7408
8037
|
await memory.remember(input.text, scope, {
|
|
7409
8038
|
type: input.type,
|
|
@@ -7433,7 +8062,12 @@ function forgetTool(memory) {
|
|
|
7433
8062
|
required: ["query"]
|
|
7434
8063
|
},
|
|
7435
8064
|
async execute(input) {
|
|
7436
|
-
if (!input?.query)
|
|
8065
|
+
if (!input?.query) {
|
|
8066
|
+
throw new ToolValidationError({
|
|
8067
|
+
message: "forget: query is required",
|
|
8068
|
+
field: "query"
|
|
8069
|
+
});
|
|
8070
|
+
}
|
|
7437
8071
|
const scope = input.scope ?? "project-memory";
|
|
7438
8072
|
const removed = await memory.forget(input.query, scope);
|
|
7439
8073
|
return { removed, scope };
|
|
@@ -7470,7 +8104,12 @@ function searchMemoryTool(memory) {
|
|
|
7470
8104
|
required: ["query"]
|
|
7471
8105
|
},
|
|
7472
8106
|
async execute(input) {
|
|
7473
|
-
if (!input?.query)
|
|
8107
|
+
if (!input?.query) {
|
|
8108
|
+
throw new ToolValidationError({
|
|
8109
|
+
message: "search_memory: query is required",
|
|
8110
|
+
field: "query"
|
|
8111
|
+
});
|
|
8112
|
+
}
|
|
7474
8113
|
const scope = input.scope ?? "project-memory";
|
|
7475
8114
|
const limit = Math.min(input.limit ?? 5, 20);
|
|
7476
8115
|
const entries = await memory.search(input.query, scope, limit);
|
|
@@ -7517,7 +8156,12 @@ function relatedMemoryTool(memory) {
|
|
|
7517
8156
|
required: ["text"]
|
|
7518
8157
|
},
|
|
7519
8158
|
async execute(input) {
|
|
7520
|
-
if (!input?.text)
|
|
8159
|
+
if (!input?.text) {
|
|
8160
|
+
throw new ToolValidationError({
|
|
8161
|
+
message: "find_related_memories: text is required",
|
|
8162
|
+
field: "text"
|
|
8163
|
+
});
|
|
8164
|
+
}
|
|
7521
8165
|
const scope = input.scope ?? "project-memory";
|
|
7522
8166
|
const limit = Math.min(input.limit ?? 5, 20);
|
|
7523
8167
|
const entries = memory.findRelated ? await memory.findRelated(input.text, scope, limit) : await memory.search(input.text, scope, limit);
|
|
@@ -7758,16 +8402,23 @@ var ProcessGuardian = class {
|
|
|
7758
8402
|
event: "process_guardian.uncaught_exception",
|
|
7759
8403
|
error: err.message,
|
|
7760
8404
|
stack: err.stack,
|
|
7761
|
-
instanceId: this.instanceId
|
|
8405
|
+
instanceId: this.instanceId,
|
|
8406
|
+
fatal: true
|
|
7762
8407
|
}));
|
|
8408
|
+
this.stop();
|
|
8409
|
+
process.exit(1);
|
|
7763
8410
|
});
|
|
7764
8411
|
process.on("unhandledRejection", (reason) => {
|
|
8412
|
+
const err = reason instanceof Error ? { message: reason.message, stack: reason.stack } : { value: String(reason) };
|
|
7765
8413
|
console.error(JSON.stringify({
|
|
7766
8414
|
level: "error",
|
|
7767
8415
|
event: "process_guardian.unhandled_rejection",
|
|
7768
|
-
|
|
7769
|
-
instanceId: this.instanceId
|
|
8416
|
+
...err,
|
|
8417
|
+
instanceId: this.instanceId,
|
|
8418
|
+
fatal: true
|
|
7770
8419
|
}));
|
|
8420
|
+
this.stop();
|
|
8421
|
+
process.exit(1);
|
|
7771
8422
|
});
|
|
7772
8423
|
process.on("SIGTERM", (origin) => {
|
|
7773
8424
|
console.log(JSON.stringify({
|
|
@@ -8308,8 +8959,8 @@ var Bm25Index = class {
|
|
|
8308
8959
|
df;
|
|
8309
8960
|
N;
|
|
8310
8961
|
safeAvgLen;
|
|
8311
|
-
score(
|
|
8312
|
-
const qTokens = tokenise(
|
|
8962
|
+
score(query, filter) {
|
|
8963
|
+
const qTokens = tokenise(query);
|
|
8313
8964
|
if (qTokens.length === 0) return [];
|
|
8314
8965
|
const results = [];
|
|
8315
8966
|
for (const doc of this.documents) {
|
|
@@ -8632,7 +9283,7 @@ var IndexStore = class {
|
|
|
8632
9283
|
);
|
|
8633
9284
|
}
|
|
8634
9285
|
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
8635
|
-
search(
|
|
9286
|
+
search(query, filter) {
|
|
8636
9287
|
const conditions = [];
|
|
8637
9288
|
const values = [];
|
|
8638
9289
|
let effectiveKind = filter?.kind;
|
|
@@ -8656,8 +9307,8 @@ var IndexStore = class {
|
|
|
8656
9307
|
conditions.push("file LIKE ?");
|
|
8657
9308
|
values.push(`%${filter.file}%`);
|
|
8658
9309
|
}
|
|
8659
|
-
if (
|
|
8660
|
-
const tokens =
|
|
9310
|
+
if (query.trim()) {
|
|
9311
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
8661
9312
|
const tokenConds = tokens.map(() => "text LIKE ?");
|
|
8662
9313
|
conditions.push(`(${tokenConds.join(" OR ")})`);
|
|
8663
9314
|
for (const t of tokens) values.push(`%${t}%`);
|
|
@@ -8691,10 +9342,10 @@ var IndexStore = class {
|
|
|
8691
9342
|
* `LIKE '%tok%'` recall for the common symbol-search shapes ("user" finds
|
|
8692
9343
|
* "users", camelCase-split text makes "complex" find "complexOperation").
|
|
8693
9344
|
*/
|
|
8694
|
-
searchRanked(
|
|
8695
|
-
const tokens = tokenise(
|
|
9345
|
+
searchRanked(query, filter, limit) {
|
|
9346
|
+
const tokens = tokenise(query);
|
|
8696
9347
|
if (tokens.length === 0 || !this.ftsAvailable) {
|
|
8697
|
-
return this.searchRankedFallback(
|
|
9348
|
+
return this.searchRankedFallback(query, filter, limit);
|
|
8698
9349
|
}
|
|
8699
9350
|
let effectiveKind = filter?.kind;
|
|
8700
9351
|
if (filter?.lspKind !== void 0) {
|
|
@@ -8751,19 +9402,19 @@ var IndexStore = class {
|
|
|
8751
9402
|
};
|
|
8752
9403
|
}
|
|
8753
9404
|
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
8754
|
-
searchRankedFallback(
|
|
8755
|
-
const candidates = this.search(
|
|
9405
|
+
searchRankedFallback(query, filter, limit) {
|
|
9406
|
+
const candidates = this.search(query, filter);
|
|
8756
9407
|
if (candidates.length === 0) return { results: [], total: 0 };
|
|
8757
|
-
if (!
|
|
9408
|
+
if (!query.trim()) {
|
|
8758
9409
|
return { results: candidates.slice(0, limit), total: candidates.length };
|
|
8759
9410
|
}
|
|
8760
9411
|
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
8761
9412
|
const bm25 = buildBm25Index(
|
|
8762
9413
|
candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
|
|
8763
9414
|
);
|
|
8764
|
-
const scored = bm25.score(
|
|
9415
|
+
const scored = bm25.score(query, (id) => candidateById.has(id));
|
|
8765
9416
|
scored.sort((a, b) => b.score - a.score);
|
|
8766
|
-
const qTokens = tokenise(
|
|
9417
|
+
const qTokens = tokenise(query);
|
|
8767
9418
|
const results = scored.slice(0, limit).map(({ id, score }) => {
|
|
8768
9419
|
const c = expectDefined(candidateById.get(id));
|
|
8769
9420
|
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
@@ -8874,6 +9525,104 @@ var IndexStore = class {
|
|
|
8874
9525
|
}
|
|
8875
9526
|
});
|
|
8876
9527
|
}
|
|
9528
|
+
/**
|
|
9529
|
+
* Commit a batch of file-level symbol/refs/upserts in a single transaction.
|
|
9530
|
+
*
|
|
9531
|
+
* Used by the indexer to amortize SQLite commit overhead across many files.
|
|
9532
|
+
* Before this, the indexer issued one transaction per file (BEGIN IMMEDIATE
|
|
9533
|
+
* for symbols, plus per-file deletes and an upsertFile call), so a 20-file
|
|
9534
|
+
* parallel batch cost ~5+ transactions × 20 files = 100+ commits. With
|
|
9535
|
+
* this entry point we do exactly one BEGIN/COMMIT per parallel batch.
|
|
9536
|
+
*
|
|
9537
|
+
* Each entry must already be a fully-parsed FileSymbols (symbols + refs).
|
|
9538
|
+
* The caller is responsible for the per-file prefix accounting
|
|
9539
|
+
* (refsByLine → flat list with `fromId` populated). `deleteForFiles` lets
|
|
9540
|
+
* the caller clear stale symbols/refs for any files being re-indexed before
|
|
9541
|
+
* the inserts run (required to keep refs → symbols FK invariants).
|
|
9542
|
+
*
|
|
9543
|
+
* Returns the symbols back with their assigned `id` (same shape as
|
|
9544
|
+
* {@link insertSymbols}) so callers can build final per-file results.
|
|
9545
|
+
*/
|
|
9546
|
+
commitBatch(entries, options = {}) {
|
|
9547
|
+
if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
|
|
9548
|
+
return [];
|
|
9549
|
+
}
|
|
9550
|
+
return this.runWithRetry(() => {
|
|
9551
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
9552
|
+
try {
|
|
9553
|
+
if (options.deleteForFiles && options.deleteForFiles.length > 0) {
|
|
9554
|
+
const placeholders = options.deleteForFiles.map(() => "?").join(",");
|
|
9555
|
+
if (this.ftsAvailable) {
|
|
9556
|
+
this.db.prepare(
|
|
9557
|
+
`DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
9558
|
+
).run(...options.deleteForFiles);
|
|
9559
|
+
}
|
|
9560
|
+
this.db.prepare(
|
|
9561
|
+
`DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
|
|
9562
|
+
).run(...options.deleteForFiles);
|
|
9563
|
+
this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
|
|
9564
|
+
}
|
|
9565
|
+
const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
|
|
9566
|
+
let nextId = (maxRows[0]?.m ?? 0) + 1;
|
|
9567
|
+
const symStmt = this.db.prepare(
|
|
9568
|
+
`INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
|
|
9569
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
9570
|
+
);
|
|
9571
|
+
const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
|
|
9572
|
+
const allInserted = [];
|
|
9573
|
+
const refsToInsert = [];
|
|
9574
|
+
for (const entry of entries) {
|
|
9575
|
+
for (const s of entry.symbols) {
|
|
9576
|
+
const id = nextId++;
|
|
9577
|
+
symStmt.run(
|
|
9578
|
+
id,
|
|
9579
|
+
s.lang,
|
|
9580
|
+
s.kind,
|
|
9581
|
+
s.name,
|
|
9582
|
+
s.file,
|
|
9583
|
+
s.line,
|
|
9584
|
+
s.col,
|
|
9585
|
+
s.signature,
|
|
9586
|
+
s.docComment,
|
|
9587
|
+
s.scope,
|
|
9588
|
+
s.text,
|
|
9589
|
+
s.file
|
|
9590
|
+
);
|
|
9591
|
+
ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
|
|
9592
|
+
allInserted.push({ ...s, id });
|
|
9593
|
+
}
|
|
9594
|
+
for (const r of entry.refs) refsToInsert.push(r);
|
|
9595
|
+
}
|
|
9596
|
+
if (refsToInsert.length > 0) {
|
|
9597
|
+
const refStmt = this.db.prepare(
|
|
9598
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line)
|
|
9599
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
9600
|
+
);
|
|
9601
|
+
for (const ref of refsToInsert) {
|
|
9602
|
+
refStmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
|
|
9603
|
+
}
|
|
9604
|
+
}
|
|
9605
|
+
const upsertStmt = this.db.prepare(
|
|
9606
|
+
`INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
|
|
9607
|
+
VALUES (?, ?, ?, ?, ?)
|
|
9608
|
+
ON CONFLICT(file) DO UPDATE SET
|
|
9609
|
+
lang = excluded.lang,
|
|
9610
|
+
mtime_ms = excluded.mtime_ms,
|
|
9611
|
+
symbol_count = excluded.symbol_count,
|
|
9612
|
+
last_indexed = excluded.last_indexed`
|
|
9613
|
+
);
|
|
9614
|
+
const now2 = Date.now();
|
|
9615
|
+
for (const entry of entries) {
|
|
9616
|
+
upsertStmt.run(entry.file, entry.lang, entry.mtimeMs, entry.symbolCount, now2);
|
|
9617
|
+
}
|
|
9618
|
+
this.db.exec("COMMIT");
|
|
9619
|
+
return allInserted;
|
|
9620
|
+
} catch (err) {
|
|
9621
|
+
this.db.exec("ROLLBACK");
|
|
9622
|
+
throw err;
|
|
9623
|
+
}
|
|
9624
|
+
});
|
|
9625
|
+
}
|
|
8877
9626
|
/**
|
|
8878
9627
|
* Delete all refs whose source symbols are in a given file.
|
|
8879
9628
|
* Used when re-indexing a file to clear stale refs.
|
|
@@ -10405,6 +11154,8 @@ async function runIndexerWithStore(store, opts) {
|
|
|
10405
11154
|
return { file, stat: stat11, lang, parsed, content };
|
|
10406
11155
|
})
|
|
10407
11156
|
);
|
|
11157
|
+
const batchEntries = [];
|
|
11158
|
+
const deleteForFiles = [];
|
|
10408
11159
|
for (let fi = 0; fi < statReadParse.length; fi++) {
|
|
10409
11160
|
const settled = statReadParse[fi];
|
|
10410
11161
|
const file = expectDefined(batchFiles[fi]);
|
|
@@ -10429,43 +11180,116 @@ async function runIndexerWithStore(store, opts) {
|
|
|
10429
11180
|
}
|
|
10430
11181
|
if (!lang || !parsed) {
|
|
10431
11182
|
if (lang) {
|
|
10432
|
-
store.upsertFile({
|
|
11183
|
+
store.upsertFile({
|
|
11184
|
+
file,
|
|
11185
|
+
lang,
|
|
11186
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
11187
|
+
symbolCount: 0,
|
|
11188
|
+
lastIndexed: Date.now()
|
|
11189
|
+
});
|
|
10433
11190
|
filesIndexed++;
|
|
10434
11191
|
}
|
|
10435
11192
|
continue;
|
|
10436
11193
|
}
|
|
10437
|
-
store.deleteRefsForFile(file);
|
|
10438
|
-
store.deleteSymbolsForFile(file);
|
|
10439
11194
|
if (parsed.symbols.length === 0) {
|
|
10440
|
-
store.upsertFile({
|
|
11195
|
+
store.upsertFile({
|
|
11196
|
+
file,
|
|
11197
|
+
lang,
|
|
11198
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
11199
|
+
symbolCount: 0,
|
|
11200
|
+
lastIndexed: Date.now()
|
|
11201
|
+
});
|
|
10441
11202
|
filesIndexed++;
|
|
10442
11203
|
continue;
|
|
10443
11204
|
}
|
|
10444
|
-
const
|
|
10445
|
-
const count = symbolsWithIds.length;
|
|
10446
|
-
symbolsIndexed += count;
|
|
10447
|
-
langStats[lang] = (langStats[lang] ?? 0) + count;
|
|
11205
|
+
const refs = [];
|
|
10448
11206
|
if (parsed.refs && parsed.refs.length > 0) {
|
|
10449
|
-
const
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
11207
|
+
for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
|
|
11208
|
+
}
|
|
11209
|
+
batchEntries.push({
|
|
11210
|
+
file,
|
|
11211
|
+
lang,
|
|
11212
|
+
symbols: parsed.symbols,
|
|
11213
|
+
refs,
|
|
11214
|
+
mtimeMs: Math.floor(stat11.mtimeMs),
|
|
11215
|
+
symbolCount: parsed.symbols.length
|
|
11216
|
+
});
|
|
11217
|
+
deleteForFiles.push(file);
|
|
11218
|
+
}
|
|
11219
|
+
if (batchEntries.length > 0) {
|
|
11220
|
+
try {
|
|
11221
|
+
const inserted = store.commitBatch(batchEntries, { deleteForFiles });
|
|
11222
|
+
let cursor = 0;
|
|
11223
|
+
for (const entry of batchEntries) {
|
|
11224
|
+
const count = entry.symbols.length;
|
|
11225
|
+
const symbolsWithIds = inserted.slice(cursor, cursor + count);
|
|
11226
|
+
cursor += count;
|
|
11227
|
+
symbolsIndexed += count;
|
|
11228
|
+
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
|
|
11229
|
+
filesIndexed++;
|
|
11230
|
+
if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
|
|
11231
|
+
const refsByLine = /* @__PURE__ */ new Map();
|
|
11232
|
+
for (let i = 0; i < symbolsWithIds.length; i++) {
|
|
11233
|
+
const sym = symbolsWithIds[i];
|
|
11234
|
+
let arr = refsByLine.get(sym.line);
|
|
11235
|
+
if (!arr) {
|
|
11236
|
+
arr = [];
|
|
11237
|
+
refsByLine.set(sym.line, arr);
|
|
11238
|
+
}
|
|
11239
|
+
arr.push(i);
|
|
11240
|
+
}
|
|
11241
|
+
for (const ref of entry.refs) {
|
|
11242
|
+
const indices = refsByLine.get(ref.line);
|
|
11243
|
+
if (indices && indices.length > 0) {
|
|
11244
|
+
const idx = indices.shift();
|
|
11245
|
+
ref.fromId = symbolsWithIds[idx].id;
|
|
11246
|
+
}
|
|
11247
|
+
}
|
|
10455
11248
|
}
|
|
10456
|
-
arr.push(r);
|
|
10457
11249
|
}
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
11250
|
+
} catch (err) {
|
|
11251
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
11252
|
+
errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
|
|
11253
|
+
for (const entry of batchEntries) {
|
|
11254
|
+
try {
|
|
11255
|
+
store.deleteRefsForFile(entry.file);
|
|
11256
|
+
store.deleteSymbolsForFile(entry.file);
|
|
11257
|
+
const symbolsWithIds = store.insertSymbols(entry.symbols);
|
|
11258
|
+
symbolsIndexed += symbolsWithIds.length;
|
|
11259
|
+
langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
|
|
11260
|
+
filesIndexed++;
|
|
11261
|
+
if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
|
|
11262
|
+
const refsByLine = /* @__PURE__ */ new Map();
|
|
11263
|
+
for (const sym of symbolsWithIds) {
|
|
11264
|
+
let arr = refsByLine.get(sym.line);
|
|
11265
|
+
if (!arr) {
|
|
11266
|
+
arr = [];
|
|
11267
|
+
refsByLine.set(sym.line, arr);
|
|
11268
|
+
}
|
|
11269
|
+
arr.push(sym);
|
|
11270
|
+
}
|
|
11271
|
+
const fallbackBatch = [];
|
|
11272
|
+
for (const ref of entry.refs) {
|
|
11273
|
+
const syms = refsByLine.get(ref.line);
|
|
11274
|
+
if (syms && syms.length > 0) {
|
|
11275
|
+
const sym = syms.shift();
|
|
11276
|
+
fallbackBatch.push({ ...ref, fromId: sym.id });
|
|
11277
|
+
}
|
|
11278
|
+
}
|
|
11279
|
+
if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
|
|
11280
|
+
}
|
|
11281
|
+
store.upsertFile({
|
|
11282
|
+
file: entry.file,
|
|
11283
|
+
lang: entry.lang,
|
|
11284
|
+
mtimeMs: entry.mtimeMs,
|
|
11285
|
+
symbolCount: entry.symbolCount,
|
|
11286
|
+
lastIndexed: Date.now()
|
|
11287
|
+
});
|
|
11288
|
+
} catch (innerErr) {
|
|
11289
|
+
errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
|
|
10463
11290
|
}
|
|
10464
11291
|
}
|
|
10465
|
-
if (batch.length > 0) store.insertRefsBatch(batch);
|
|
10466
11292
|
}
|
|
10467
|
-
store.upsertFile({ file, lang, mtimeMs: Math.floor(stat11.mtimeMs), symbolCount: count, lastIndexed: Date.now() });
|
|
10468
|
-
filesIndexed++;
|
|
10469
11293
|
}
|
|
10470
11294
|
}
|
|
10471
11295
|
if (discoveredFiles) {
|
|
@@ -10907,7 +11731,7 @@ var codebaseSearchTool = {
|
|
|
10907
11731
|
name: "codebase-search",
|
|
10908
11732
|
category: "Project",
|
|
10909
11733
|
icon: "index",
|
|
10910
|
-
description: "
|
|
11734
|
+
description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Much more powerful and structured than raw `grep` for finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
|
|
10911
11735
|
usageHint: "PREFERRED FOR CODE UNDERSTANDING:\n\n- Use when you need to find where something is defined or used by name.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\nThis is generally better than `grep` when you are looking for symbols rather than arbitrary text patterns.",
|
|
10912
11736
|
permission: "auto",
|
|
10913
11737
|
mutating: false,
|
|
@@ -10941,6 +11765,10 @@ var codebaseSearchTool = {
|
|
|
10941
11765
|
description: "Maximum results to return (default 20, max 100)",
|
|
10942
11766
|
minimum: 1,
|
|
10943
11767
|
maximum: 100
|
|
11768
|
+
},
|
|
11769
|
+
preferLsp: {
|
|
11770
|
+
type: "boolean",
|
|
11771
|
+
description: "Prefer live LSP results over the index. Index-only when the LSP plugin is not active. When the LSP plugin is active and this is true, results come from live workspaceSymbol queries."
|
|
10944
11772
|
}
|
|
10945
11773
|
},
|
|
10946
11774
|
required: ["query"]
|
|
@@ -11103,15 +11931,15 @@ var setWorkingDirTool = {
|
|
|
11103
11931
|
};
|
|
11104
11932
|
}
|
|
11105
11933
|
};
|
|
11106
|
-
function findTaskIndex(tasks,
|
|
11107
|
-
const asNum = Number.parseInt(
|
|
11934
|
+
function findTaskIndex(tasks, query) {
|
|
11935
|
+
const asNum = Number.parseInt(query, 10);
|
|
11108
11936
|
if (!Number.isNaN(asNum)) {
|
|
11109
11937
|
const idx = asNum - 1;
|
|
11110
11938
|
if (tasks[idx]) return idx;
|
|
11111
11939
|
}
|
|
11112
|
-
const byId = tasks.findIndex((t) => t.id ===
|
|
11940
|
+
const byId = tasks.findIndex((t) => t.id === query);
|
|
11113
11941
|
if (byId >= 0) return byId;
|
|
11114
|
-
const lower =
|
|
11942
|
+
const lower = query.toLowerCase();
|
|
11115
11943
|
return tasks.findIndex((t) => t.title.toLowerCase().includes(lower));
|
|
11116
11944
|
}
|
|
11117
11945
|
var taskTool = {
|