@wrongstack/tools 0.305.0 → 0.306.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/_shell-pick.d.ts +4 -5
- package/dist/_util.d.ts +22 -5
- package/dist/audit.d.ts +0 -1
- package/dist/audit.js +135 -46
- package/dist/bash.js +61 -37
- package/dist/browser/index.js +29 -9
- package/dist/browser/types.d.ts +7 -1
- package/dist/builtin.js +1280 -728
- package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
- package/dist/codebase-index/index.d.ts +1 -0
- package/dist/codebase-index/index.js +225 -153
- package/dist/codebase-index/project-server-endpoint.d.ts +1 -2
- package/dist/codebase-index/project-server.js +19 -20
- package/dist/diff.d.ts +5 -0
- package/dist/diff.js +78 -12
- package/dist/document.js +18 -6
- package/dist/edit.js +69 -16
- package/dist/exec.js +44 -22
- package/dist/fetch.js +13 -1
- package/dist/format.d.ts +4 -2
- package/dist/format.js +81 -31
- package/dist/glob.js +12 -4
- package/dist/grep.d.ts +2 -0
- package/dist/grep.js +15 -4
- package/dist/index.js +1357 -765
- package/dist/install.js +96 -37
- package/dist/kanban-tool-types.d.ts +6 -1
- package/dist/kanban.js +60 -0
- package/dist/languages/index.js +28 -13
- package/dist/lint.js +28 -13
- package/dist/logs.d.ts +0 -1
- package/dist/logs.js +44 -13
- package/dist/memory.d.ts +8 -0
- package/dist/memory.js +23 -3
- package/dist/mode.d.ts +1 -1
- package/dist/mode.js +3 -0
- package/dist/next-steps.d.ts +2 -3
- package/dist/next-steps.js +3 -3
- package/dist/outdated.d.ts +0 -3
- package/dist/outdated.js +89 -48
- package/dist/pack.js +1280 -728
- package/dist/plan.js +76 -3
- package/dist/process-registry.d.ts +8 -2
- package/dist/process-registry.js +28 -13
- package/dist/ps-slash.js +22 -12
- package/dist/read.js +10 -4
- package/dist/replace.d.ts +4 -0
- package/dist/replace.js +104 -7
- package/dist/search.d.ts +6 -0
- package/dist/search.js +47 -26
- package/dist/session-kanban.d.ts +25 -0
- package/dist/session-kanban.js +16 -0
- package/dist/skill.d.ts +6 -0
- package/dist/skill.js +9 -10
- package/dist/task.js +66 -2
- package/dist/test.js +28 -13
- package/dist/todo.js +64 -2
- package/dist/tool-icons.js +4 -2
- package/dist/tool-summary.d.ts +1 -1
- package/dist/tool-summary.js +76 -1
- package/dist/tool-tier.js +1280 -728
- package/dist/tree.js +9 -10
- package/dist/typecheck.d.ts +0 -2
- package/dist/typecheck.js +98 -31
- package/dist/write.js +58 -10
- package/package.json +4 -3
package/dist/builtin.js
CHANGED
|
@@ -422,8 +422,11 @@ var init_redact_command = __esm({
|
|
|
422
422
|
/--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
|
|
423
423
|
// -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
|
|
424
424
|
// (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
|
|
425
|
+
// The value must be token-like (>= 8 chars) so ordinary combined flags such
|
|
426
|
+
// as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
|
|
427
|
+
// redacted, not just the first.
|
|
425
428
|
// NOTE: synced with @wrongstack/core observability/redact-command.ts.
|
|
426
|
-
/(?<![-\w])-t(?:[=\s]+)?[^\s,-]
|
|
429
|
+
/(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
|
|
427
430
|
// -p|-password|-a (redis auth) short flags: attached + separated + =value.
|
|
428
431
|
// Same token-start anchor; over-redaction is an accepted tradeoff for a
|
|
429
432
|
// redaction function. Synced with core copy.
|
|
@@ -431,8 +434,9 @@ var init_redact_command = __esm({
|
|
|
431
434
|
// env var–style secrets: TOKEN=x, API_KEY=y, etc.
|
|
432
435
|
/(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
|
|
433
436
|
// Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
|
|
434
|
-
// when preceded by a flag name (e.g. --github-token=EyJ...).
|
|
435
|
-
|
|
437
|
+
// when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
|
|
438
|
+
// every such flag in the command line is redacted, not just the first.
|
|
439
|
+
/--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
|
|
436
440
|
];
|
|
437
441
|
}
|
|
438
442
|
});
|
|
@@ -518,11 +522,15 @@ var init_process_registry = __esm({
|
|
|
518
522
|
return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
|
|
519
523
|
}
|
|
520
524
|
_canSignalProcessGroup(p) {
|
|
521
|
-
return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
|
|
525
|
+
return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
|
|
522
526
|
}
|
|
523
527
|
_killChildDirect(p, signal) {
|
|
524
528
|
try {
|
|
525
|
-
p.child
|
|
529
|
+
if (p.child) {
|
|
530
|
+
p.child.kill(signal);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
|
|
526
534
|
} catch {
|
|
527
535
|
}
|
|
528
536
|
}
|
|
@@ -720,15 +728,15 @@ var init_process_registry = __esm({
|
|
|
720
728
|
this._pruneStale(pid);
|
|
721
729
|
const p = this.processes.get(pid);
|
|
722
730
|
if (!p) return false;
|
|
723
|
-
if (p.killed) return true;
|
|
731
|
+
if (p.killed && opts.force !== true) return true;
|
|
724
732
|
if (p.protected && opts.includeProtected !== true) return false;
|
|
725
733
|
if (opts.preserveBackground && p.background) return false;
|
|
726
734
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
727
735
|
const isWin5 = os.platform() === "win32";
|
|
728
736
|
if (isWin5) {
|
|
729
|
-
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
737
|
+
const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
|
|
730
738
|
const directFallback = () => {
|
|
731
|
-
if (p.child.exitCode === null) {
|
|
739
|
+
if (p.child && p.child.exitCode === null) {
|
|
732
740
|
try {
|
|
733
741
|
p.child.kill("SIGKILL");
|
|
734
742
|
} catch {
|
|
@@ -740,10 +748,7 @@ var init_process_registry = __esm({
|
|
|
740
748
|
onSettled: directFallback
|
|
741
749
|
})) {
|
|
742
750
|
} else {
|
|
743
|
-
|
|
744
|
-
p.child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
745
|
-
} catch {
|
|
746
|
-
}
|
|
751
|
+
this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
|
|
747
752
|
}
|
|
748
753
|
p.killed = true;
|
|
749
754
|
return true;
|
|
@@ -754,7 +759,7 @@ var init_process_registry = __esm({
|
|
|
754
759
|
} else {
|
|
755
760
|
this._killPosix(p, "SIGTERM");
|
|
756
761
|
const timer = setTimeout(() => {
|
|
757
|
-
if (this.processes.has(pid) && !p.child
|
|
762
|
+
if (this.processes.has(pid) && !p.child?.killed) {
|
|
758
763
|
this._killPosix(p, "SIGKILL");
|
|
759
764
|
}
|
|
760
765
|
}, graceMs);
|
|
@@ -809,6 +814,16 @@ var init_process_registry = __esm({
|
|
|
809
814
|
* before reusing a PID, but we want to clean up before that becomes a risk.
|
|
810
815
|
*/
|
|
811
816
|
_isStaleEntry(entry) {
|
|
817
|
+
if (entry.child === null) {
|
|
818
|
+
if (Date.now() - entry.startedAt <= 6e4) return false;
|
|
819
|
+
if (os.platform() === "win32") return false;
|
|
820
|
+
try {
|
|
821
|
+
process.kill(entry.pid, 0);
|
|
822
|
+
return false;
|
|
823
|
+
} catch (err) {
|
|
824
|
+
return err.code !== "EPERM";
|
|
825
|
+
}
|
|
826
|
+
}
|
|
812
827
|
return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
|
|
813
828
|
}
|
|
814
829
|
/**
|
|
@@ -1010,8 +1025,8 @@ async function* spawnStream(opts) {
|
|
|
1010
1025
|
try {
|
|
1011
1026
|
for (; ; ) {
|
|
1012
1027
|
while (queue.length === 0) {
|
|
1013
|
-
await new Promise((
|
|
1014
|
-
waiter =
|
|
1028
|
+
await new Promise((resolve17) => {
|
|
1029
|
+
waiter = resolve17;
|
|
1015
1030
|
});
|
|
1016
1031
|
}
|
|
1017
1032
|
const chunk = queue.shift();
|
|
@@ -1099,19 +1114,48 @@ import * as Core from "@wrongstack/core/utils";
|
|
|
1099
1114
|
function sha256hex(content) {
|
|
1100
1115
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1101
1116
|
}
|
|
1102
|
-
async function detectPackageManager(cwd) {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1117
|
+
async function detectPackageManager(cwd, stopAt) {
|
|
1118
|
+
let dir = path3.resolve(cwd);
|
|
1119
|
+
const stop = stopAt ? path3.resolve(stopAt) : dir;
|
|
1120
|
+
for (; ; ) {
|
|
1121
|
+
const found = await detectPackageManagerInDir(dir);
|
|
1122
|
+
if (found) return found;
|
|
1123
|
+
if (dir === stop) break;
|
|
1124
|
+
const parent = path3.dirname(dir);
|
|
1125
|
+
const relParent = path3.relative(stop, parent);
|
|
1126
|
+
if (parent === dir || relParent.startsWith("..") || path3.isAbsolute(relParent)) break;
|
|
1127
|
+
dir = parent;
|
|
1108
1128
|
}
|
|
1129
|
+
return "npm";
|
|
1130
|
+
}
|
|
1131
|
+
async function detectPackageManagerInDir(dir) {
|
|
1132
|
+
const fs35 = await import("node:fs/promises");
|
|
1109
1133
|
try {
|
|
1110
|
-
await
|
|
1111
|
-
|
|
1134
|
+
const raw = await fs35.readFile(path3.join(dir, "package.json"), "utf8");
|
|
1135
|
+
const declared = JSON.parse(raw).packageManager;
|
|
1136
|
+
if (typeof declared === "string") {
|
|
1137
|
+
const name = declared.split("@")[0] ?? "";
|
|
1138
|
+
if (name === "pnpm" || name === "yarn") return name;
|
|
1139
|
+
if (name === "npm" || name === "bun") return "npm";
|
|
1140
|
+
}
|
|
1112
1141
|
} catch {
|
|
1113
1142
|
}
|
|
1114
|
-
|
|
1143
|
+
const lockfiles = [
|
|
1144
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
1145
|
+
["yarn.lock", "yarn"],
|
|
1146
|
+
["bun.lockb", "npm"],
|
|
1147
|
+
["bun.lock", "npm"],
|
|
1148
|
+
["package-lock.json", "npm"],
|
|
1149
|
+
["npm-shrinkwrap.json", "npm"]
|
|
1150
|
+
];
|
|
1151
|
+
for (const [file, manager] of lockfiles) {
|
|
1152
|
+
try {
|
|
1153
|
+
await fs35.stat(`${dir}/${file}`);
|
|
1154
|
+
return manager;
|
|
1155
|
+
} catch {
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return null;
|
|
1115
1159
|
}
|
|
1116
1160
|
function resolvePath(input, ctx) {
|
|
1117
1161
|
return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
@@ -1170,6 +1214,20 @@ async function safeResolveReal(input, ctx) {
|
|
|
1170
1214
|
const abs = safeResolve(input, ctx);
|
|
1171
1215
|
return await resolveRealInsideRoot(abs, ctx);
|
|
1172
1216
|
}
|
|
1217
|
+
function truncateDiffPayload(diff, maxBytes) {
|
|
1218
|
+
const total = Buffer.byteLength(diff, "utf8");
|
|
1219
|
+
if (total <= maxBytes) return { text: diff, truncated: false };
|
|
1220
|
+
const MARKER_RESERVE = 96;
|
|
1221
|
+
let head = takeHeadBytes(diff, Math.max(0, maxBytes - MARKER_RESERVE));
|
|
1222
|
+
const nl = head.lastIndexOf("\n");
|
|
1223
|
+
if (nl > 0) head = head.slice(0, nl);
|
|
1224
|
+
const kept = Buffer.byteLength(head, "utf8");
|
|
1225
|
+
return {
|
|
1226
|
+
text: `${head}
|
|
1227
|
+
\u2026[diff truncated: ${total - kept} of ${total} bytes omitted]`,
|
|
1228
|
+
truncated: true
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1173
1231
|
function truncateMiddle(s, max) {
|
|
1174
1232
|
if (Buffer.byteLength(s, "utf8") <= max) return s;
|
|
1175
1233
|
const half = Math.floor(max / 2);
|
|
@@ -2732,8 +2790,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
|
|
|
2732
2790
|
collectFileEvidence(directory, fullPath, entry.name, profiles, state);
|
|
2733
2791
|
}
|
|
2734
2792
|
}
|
|
2735
|
-
function collectFileEvidence(directory, fullPath,
|
|
2736
|
-
const lower =
|
|
2793
|
+
function collectFileEvidence(directory, fullPath, basename14, profiles, state) {
|
|
2794
|
+
const lower = basename14.toLowerCase();
|
|
2737
2795
|
const extension = path4.extname(lower);
|
|
2738
2796
|
for (const profile of profiles) {
|
|
2739
2797
|
const detector = profile.detectors.find(
|
|
@@ -2744,7 +2802,7 @@ function collectFileEvidence(directory, fullPath, basename13, profiles, state) {
|
|
|
2744
2802
|
candidate.evidence.push({
|
|
2745
2803
|
kind: detector.kind,
|
|
2746
2804
|
path: fullPath,
|
|
2747
|
-
value:
|
|
2805
|
+
value: basename14,
|
|
2748
2806
|
weight: detector.weight
|
|
2749
2807
|
});
|
|
2750
2808
|
if (detector.kind === "manifest" || detector.kind === "config") {
|
|
@@ -2872,8 +2930,8 @@ function normalizeLimits(input) {
|
|
|
2872
2930
|
async function canonicalDirectory(input) {
|
|
2873
2931
|
const resolved = path4.resolve(input);
|
|
2874
2932
|
const real = await fs2.realpath(resolved);
|
|
2875
|
-
const
|
|
2876
|
-
if (!
|
|
2933
|
+
const stat19 = await fs2.stat(real);
|
|
2934
|
+
if (!stat19.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
|
|
2877
2935
|
return real;
|
|
2878
2936
|
}
|
|
2879
2937
|
async function canonicalInside(input, root, label) {
|
|
@@ -3772,8 +3830,8 @@ async function executeInternal(options, startedAt) {
|
|
|
3772
3830
|
const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
|
|
3773
3831
|
if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
|
|
3774
3832
|
const safeTarget = await assertContainedFile(target, options.projectRoot);
|
|
3775
|
-
const
|
|
3776
|
-
if (
|
|
3833
|
+
const stat19 = await fs4.stat(safeTarget);
|
|
3834
|
+
if (stat19.size > MAX_INTERNAL_SOURCE_BYTES) {
|
|
3777
3835
|
return unavailableResult(
|
|
3778
3836
|
options,
|
|
3779
3837
|
`Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
|
|
@@ -4060,8 +4118,8 @@ async function snapshotPaths(paths) {
|
|
|
4060
4118
|
const existing = [];
|
|
4061
4119
|
for (const candidate of paths) {
|
|
4062
4120
|
try {
|
|
4063
|
-
const
|
|
4064
|
-
if (!
|
|
4121
|
+
const stat19 = await fs4.stat(candidate);
|
|
4122
|
+
if (!stat19.isFile()) continue;
|
|
4065
4123
|
existing.push(candidate);
|
|
4066
4124
|
} catch {
|
|
4067
4125
|
}
|
|
@@ -4072,15 +4130,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
|
|
|
4072
4130
|
const beforeSet = new Set(before);
|
|
4073
4131
|
const afterSet = new Set(after);
|
|
4074
4132
|
const changed = /* @__PURE__ */ new Set();
|
|
4075
|
-
for (const
|
|
4076
|
-
if (!beforeSet.has(
|
|
4133
|
+
for (const path40 of after) {
|
|
4134
|
+
if (!beforeSet.has(path40)) changed.add(path40);
|
|
4077
4135
|
}
|
|
4078
|
-
for (const
|
|
4079
|
-
if (!afterSet.has(
|
|
4136
|
+
for (const path40 of before) {
|
|
4137
|
+
if (!afterSet.has(path40)) changed.add(path40);
|
|
4080
4138
|
}
|
|
4081
4139
|
if (beforeSizes && afterSizes) {
|
|
4082
|
-
for (const
|
|
4083
|
-
if (beforeSizes.get(
|
|
4140
|
+
for (const path40 of after) {
|
|
4141
|
+
if (beforeSizes.get(path40) !== afterSizes.get(path40)) changed.add(path40);
|
|
4084
4142
|
}
|
|
4085
4143
|
}
|
|
4086
4144
|
return [...changed].sort();
|
|
@@ -4089,8 +4147,8 @@ async function snapshotSizes(paths) {
|
|
|
4089
4147
|
const sizes = /* @__PURE__ */ new Map();
|
|
4090
4148
|
for (const candidate of paths) {
|
|
4091
4149
|
try {
|
|
4092
|
-
const
|
|
4093
|
-
if (
|
|
4150
|
+
const stat19 = await fs4.stat(candidate);
|
|
4151
|
+
if (stat19.isFile()) sizes.set(candidate, stat19.size);
|
|
4094
4152
|
} catch {
|
|
4095
4153
|
}
|
|
4096
4154
|
}
|
|
@@ -5545,7 +5603,7 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
5545
5603
|
}
|
|
5546
5604
|
const goBinary = resolveWin32Command("go");
|
|
5547
5605
|
const goResult = await new Promise(
|
|
5548
|
-
(
|
|
5606
|
+
(resolve17, reject) => {
|
|
5549
5607
|
let settled = false;
|
|
5550
5608
|
const proc = spawn5(goBinary, ["run", scriptPath], {
|
|
5551
5609
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -5574,7 +5632,7 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
5574
5632
|
if (settled) return;
|
|
5575
5633
|
settled = true;
|
|
5576
5634
|
clearTimeout(timer);
|
|
5577
|
-
|
|
5635
|
+
resolve17({ code: code2, stdout: stdout2 });
|
|
5578
5636
|
});
|
|
5579
5637
|
}
|
|
5580
5638
|
);
|
|
@@ -6239,7 +6297,7 @@ async function resolvePython() {
|
|
|
6239
6297
|
return null;
|
|
6240
6298
|
}
|
|
6241
6299
|
function commandIsAvailable(command) {
|
|
6242
|
-
return new Promise((
|
|
6300
|
+
return new Promise((resolve17) => {
|
|
6243
6301
|
let settled = false;
|
|
6244
6302
|
const proc = spawn6(command, ["--version"], {
|
|
6245
6303
|
stdio: "ignore",
|
|
@@ -6249,7 +6307,7 @@ function commandIsAvailable(command) {
|
|
|
6249
6307
|
if (settled) return;
|
|
6250
6308
|
settled = true;
|
|
6251
6309
|
clearTimeout(timer);
|
|
6252
|
-
|
|
6310
|
+
resolve17(available);
|
|
6253
6311
|
};
|
|
6254
6312
|
const timer = setTimeout(() => {
|
|
6255
6313
|
proc.kill("SIGKILL");
|
|
@@ -6261,7 +6319,7 @@ function commandIsAvailable(command) {
|
|
|
6261
6319
|
});
|
|
6262
6320
|
}
|
|
6263
6321
|
function spawnPyParser(pyBinary, scriptPath, filePath, content) {
|
|
6264
|
-
return new Promise((
|
|
6322
|
+
return new Promise((resolve17, reject) => {
|
|
6265
6323
|
let settled = false;
|
|
6266
6324
|
const proc = spawn6(pyBinary, [scriptPath, filePath], {
|
|
6267
6325
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -6290,7 +6348,7 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
|
|
|
6290
6348
|
if (settled) return;
|
|
6291
6349
|
settled = true;
|
|
6292
6350
|
clearTimeout(timer);
|
|
6293
|
-
|
|
6351
|
+
resolve17({ code, stdout });
|
|
6294
6352
|
});
|
|
6295
6353
|
});
|
|
6296
6354
|
}
|
|
@@ -6699,9 +6757,9 @@ function parseSymbols6(opts) {
|
|
|
6699
6757
|
function regexParse2(opts) {
|
|
6700
6758
|
const { file, content, lang } = opts;
|
|
6701
6759
|
const symbols = [];
|
|
6702
|
-
const
|
|
6703
|
-
const isPackageJson =
|
|
6704
|
-
const isTsconfig =
|
|
6760
|
+
const basename14 = path22.basename(file).toLowerCase();
|
|
6761
|
+
const isPackageJson = basename14 === "package.json";
|
|
6762
|
+
const isTsconfig = basename14 === "tsconfig.json" || basename14 === "tsconfig.build.json";
|
|
6705
6763
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
6706
6764
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
6707
6765
|
const lines = content.split("\n");
|
|
@@ -7631,11 +7689,18 @@ var init_tree_sitter_parser = __esm({
|
|
|
7631
7689
|
init_spawn_stream();
|
|
7632
7690
|
init_util();
|
|
7633
7691
|
init_legacy_bridge();
|
|
7692
|
+
var SEVERITY_RANK = {
|
|
7693
|
+
info: 0,
|
|
7694
|
+
low: 1,
|
|
7695
|
+
moderate: 2,
|
|
7696
|
+
high: 3,
|
|
7697
|
+
critical: 4
|
|
7698
|
+
};
|
|
7634
7699
|
var auditTool = {
|
|
7635
7700
|
name: "audit",
|
|
7636
7701
|
category: "Package Management",
|
|
7637
7702
|
description: "Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.",
|
|
7638
|
-
usageHint: "CRITICAL SECURITY TOOL:\n\n- Run regularly and especially before any release.\n- Use `level` to focus on high/critical issues.\n- `
|
|
7703
|
+
usageHint: "CRITICAL SECURITY TOOL:\n\n- Run regularly and especially before any release.\n- Use `level` to focus on high/critical issues.\n- This tool is read-only: to remediate, use `install` (or `language_package`) to upgrade the affected packages.\nThis is one of the most important tools for supply chain security.",
|
|
7639
7704
|
permission: "confirm",
|
|
7640
7705
|
mutating: false,
|
|
7641
7706
|
capabilities: ["shell.restricted"],
|
|
@@ -7650,8 +7715,10 @@ var auditTool = {
|
|
|
7650
7715
|
enum: ["low", "moderate", "high", "critical"],
|
|
7651
7716
|
description: "Minimum severity level to report"
|
|
7652
7717
|
},
|
|
7653
|
-
fix: {
|
|
7654
|
-
|
|
7718
|
+
fix: {
|
|
7719
|
+
type: "boolean",
|
|
7720
|
+
description: "Deprecated and rejected \u2014 this tool is read-only and never modifies dependencies. Use `install` (or `language_package`) to remediate vulnerabilities."
|
|
7721
|
+
}
|
|
7655
7722
|
}
|
|
7656
7723
|
},
|
|
7657
7724
|
async execute(input, ctx, opts) {
|
|
@@ -7665,6 +7732,11 @@ var auditTool = {
|
|
|
7665
7732
|
return final;
|
|
7666
7733
|
},
|
|
7667
7734
|
async *executeStream(input, ctx, opts) {
|
|
7735
|
+
if (input.fix === true) {
|
|
7736
|
+
throw new Error(
|
|
7737
|
+
"audit: `fix: true` is not supported \u2014 this tool is read-only (mutating: false). To remediate vulnerabilities, upgrade the affected packages with the `install` tool (or `language_package` for non-JS ecosystems)."
|
|
7738
|
+
);
|
|
7739
|
+
}
|
|
7668
7740
|
const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
|
|
7669
7741
|
const bridge = await tryLegacyPackageOperation("package-audit", {
|
|
7670
7742
|
cwd,
|
|
@@ -7694,13 +7766,11 @@ var auditTool = {
|
|
|
7694
7766
|
};
|
|
7695
7767
|
return;
|
|
7696
7768
|
}
|
|
7697
|
-
const manager = await detectPackageManager(cwd);
|
|
7769
|
+
const manager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
7698
7770
|
yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
|
|
7699
7771
|
const args = ["audit", "--json"];
|
|
7700
|
-
if (input.
|
|
7701
|
-
|
|
7702
|
-
const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(",");
|
|
7703
|
-
args.push(...pkgs.map((p) => p.trim()));
|
|
7772
|
+
if (input.level && (manager === "npm" || manager === "pnpm")) {
|
|
7773
|
+
args.push(`--audit-level=${input.level}`);
|
|
7704
7774
|
}
|
|
7705
7775
|
const result = yield* spawnStream({
|
|
7706
7776
|
cmd: manager,
|
|
@@ -7709,10 +7779,16 @@ var auditTool = {
|
|
|
7709
7779
|
signal: opts.signal,
|
|
7710
7780
|
maxBytes: 1e5
|
|
7711
7781
|
});
|
|
7712
|
-
yield {
|
|
7782
|
+
yield {
|
|
7783
|
+
type: "final",
|
|
7784
|
+
output: parseAuditOutput(result.stdout, result.exitCode, {
|
|
7785
|
+
level: input.level,
|
|
7786
|
+
spawnTruncated: result.truncated
|
|
7787
|
+
})
|
|
7788
|
+
};
|
|
7713
7789
|
}
|
|
7714
7790
|
};
|
|
7715
|
-
function parseAuditOutput(json2, exitCode) {
|
|
7791
|
+
function parseAuditOutput(json2, exitCode, opts = {}) {
|
|
7716
7792
|
if (!json2) {
|
|
7717
7793
|
return {
|
|
7718
7794
|
exit_code: exitCode,
|
|
@@ -7723,18 +7799,14 @@ function parseAuditOutput(json2, exitCode) {
|
|
|
7723
7799
|
truncated: false
|
|
7724
7800
|
};
|
|
7725
7801
|
}
|
|
7802
|
+
const cappedOutput = normalizeCommandOutput(json2);
|
|
7803
|
+
const truncated = opts.spawnTruncated === true || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
|
|
7726
7804
|
try {
|
|
7727
7805
|
const data = JSON.parse(json2);
|
|
7728
|
-
|
|
7729
|
-
const
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
advisories.push({
|
|
7733
|
-
severity: adv.severity ?? "unknown",
|
|
7734
|
-
package: adv.module_name ?? id,
|
|
7735
|
-
title: adv.title ?? "Unknown vulnerability",
|
|
7736
|
-
url: adv.url ?? ""
|
|
7737
|
-
});
|
|
7806
|
+
let advisories = extractAdvisories(data);
|
|
7807
|
+
const minRank = opts.level ? SEVERITY_RANK[opts.level] ?? 0 : 0;
|
|
7808
|
+
if (minRank > 0) {
|
|
7809
|
+
advisories = advisories.filter((a) => (SEVERITY_RANK[a.severity] ?? 0) >= minRank);
|
|
7738
7810
|
}
|
|
7739
7811
|
const total = advisories.length;
|
|
7740
7812
|
const summary = total === 0 ? "No vulnerabilities found" : `Found ${total} vulnerabilities: ${advisories.filter((a) => a.severity === "critical").length} critical, ${advisories.filter((a) => a.severity === "high").length} high`;
|
|
@@ -7743,8 +7815,8 @@ function parseAuditOutput(json2, exitCode) {
|
|
|
7743
7815
|
vulnerabilities: advisories,
|
|
7744
7816
|
total,
|
|
7745
7817
|
summary,
|
|
7746
|
-
output:
|
|
7747
|
-
truncated
|
|
7818
|
+
output: cappedOutput,
|
|
7819
|
+
truncated
|
|
7748
7820
|
};
|
|
7749
7821
|
} catch {
|
|
7750
7822
|
return {
|
|
@@ -7752,15 +7824,47 @@ function parseAuditOutput(json2, exitCode) {
|
|
|
7752
7824
|
vulnerabilities: [],
|
|
7753
7825
|
total: 0,
|
|
7754
7826
|
summary: "Could not parse audit output",
|
|
7755
|
-
output:
|
|
7756
|
-
truncated
|
|
7827
|
+
output: cappedOutput,
|
|
7828
|
+
truncated
|
|
7757
7829
|
};
|
|
7758
7830
|
}
|
|
7759
7831
|
}
|
|
7832
|
+
function extractAdvisories(data) {
|
|
7833
|
+
const advisories = [];
|
|
7834
|
+
const ads = data["advisories"];
|
|
7835
|
+
if (ads && typeof ads === "object") {
|
|
7836
|
+
for (const [id, value] of Object.entries(ads)) {
|
|
7837
|
+
const adv = value ?? {};
|
|
7838
|
+
advisories.push({
|
|
7839
|
+
severity: typeof adv["severity"] === "string" ? adv["severity"] : "unknown",
|
|
7840
|
+
package: typeof adv["module_name"] === "string" ? adv["module_name"] : id,
|
|
7841
|
+
title: typeof adv["title"] === "string" ? adv["title"] : "Unknown vulnerability",
|
|
7842
|
+
url: typeof adv["url"] === "string" ? adv["url"] : ""
|
|
7843
|
+
});
|
|
7844
|
+
}
|
|
7845
|
+
return advisories;
|
|
7846
|
+
}
|
|
7847
|
+
const vulns = data["vulnerabilities"];
|
|
7848
|
+
if (vulns && typeof vulns === "object") {
|
|
7849
|
+
for (const [pkg, value] of Object.entries(vulns)) {
|
|
7850
|
+
const vuln = value ?? {};
|
|
7851
|
+
const via = Array.isArray(vuln["via"]) ? vuln["via"] : [];
|
|
7852
|
+
const detail = via.find((v) => !!v && typeof v === "object");
|
|
7853
|
+
advisories.push({
|
|
7854
|
+
severity: typeof vuln["severity"] === "string" ? vuln["severity"] : "unknown",
|
|
7855
|
+
package: pkg,
|
|
7856
|
+
title: detail && typeof detail["title"] === "string" ? detail["title"] : "Unknown vulnerability",
|
|
7857
|
+
url: detail && typeof detail["url"] === "string" ? detail["url"] : ""
|
|
7858
|
+
});
|
|
7859
|
+
}
|
|
7860
|
+
}
|
|
7861
|
+
return advisories;
|
|
7862
|
+
}
|
|
7760
7863
|
|
|
7761
7864
|
// src/bash.ts
|
|
7762
7865
|
import { spawn as spawn3 } from "node:child_process";
|
|
7763
7866
|
import * as os4 from "node:os";
|
|
7867
|
+
import { StringDecoder } from "node:string_decoder";
|
|
7764
7868
|
import {
|
|
7765
7869
|
emitProcessCompleted as emitProcessCompleted2,
|
|
7766
7870
|
emitProcessOutput as emitProcessOutput2,
|
|
@@ -8041,7 +8145,6 @@ var PersistentProcessRegistry = class {
|
|
|
8041
8145
|
try {
|
|
8042
8146
|
const data = await readRegistryFile(this.registryPath);
|
|
8043
8147
|
data.instances.set(String(entry.pid), entry);
|
|
8044
|
-
const child = null;
|
|
8045
8148
|
this.baseRegistry.register({
|
|
8046
8149
|
pid: entry.pid,
|
|
8047
8150
|
name: entry.name,
|
|
@@ -8049,7 +8152,7 @@ var PersistentProcessRegistry = class {
|
|
|
8049
8152
|
startedAt: entry.startedAt,
|
|
8050
8153
|
sessionId: entry.sessionId,
|
|
8051
8154
|
protected: entry.protected,
|
|
8052
|
-
child
|
|
8155
|
+
child: null
|
|
8053
8156
|
});
|
|
8054
8157
|
await writeRegistryFile(this.registryPath, data);
|
|
8055
8158
|
} finally {
|
|
@@ -8609,7 +8712,7 @@ function looksLikePowerShell(command) {
|
|
|
8609
8712
|
return true;
|
|
8610
8713
|
}
|
|
8611
8714
|
if (PS_VERB_RE.test(trimmed)) return true;
|
|
8612
|
-
if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps
|
|
8715
|
+
if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps)\b/i.test(trimmed)) {
|
|
8613
8716
|
return true;
|
|
8614
8717
|
}
|
|
8615
8718
|
if (looksLikePowerShellExtended(command)) return true;
|
|
@@ -8701,7 +8804,7 @@ var bashTool = {
|
|
|
8701
8804
|
name: "bash",
|
|
8702
8805
|
category: "Shell",
|
|
8703
8806
|
description: "Execute an arbitrary command in the user's default shell (bash/zsh/pwsh/cmd). stdout and stderr are merged into one stream. This is the most powerful and dangerous tool \u2014 it gives the model full access to the developer's machine. Prefer specialized tools whenever possible.",
|
|
8704
|
-
usageHint: "SECURITY WARNING: This tool runs with the full privileges of the current user.\n\nBest practices for the model:\n- Strongly prefer `exec` for known safe commands (node, npm, pnpm, tsc, git, etc.).\n- Use bash only when you genuinely need shell features (pipes, redirection, complex one-liners).\n- Prefer single focused commands over huge `&&` chains.\n- Use `background: true` only for long-running processes (dev servers, watchers).\n- The working directory is the project root.\n- Output may be truncated in the middle for very large results.",
|
|
8807
|
+
usageHint: "SECURITY WARNING: This tool runs with the full privileges of the current user.\n\nBest practices for the model:\n- Strongly prefer `exec` for known safe commands (node, npm, pnpm, tsc, git, etc.).\n- Use bash only when you genuinely need shell features (pipes, redirection, complex one-liners).\n- Prefer single focused commands over huge `&&` chains.\n- Use `background: true` only for long-running processes (dev servers, watchers).\n- The working directory is the session working dir (changed via `set_working_dir`), defaulting to the project root.\n- Output may be truncated in the middle for very large results.",
|
|
8705
8808
|
selection: {
|
|
8706
8809
|
doNotUseWhen: "the command is allowlisted and does not require pipes, redirection, or shell expansion.",
|
|
8707
8810
|
useInstead: ["exec"]
|
|
@@ -8715,7 +8818,14 @@ var bashTool = {
|
|
|
8715
8818
|
// explicitly removes the implicit cross-tool aliasing.
|
|
8716
8819
|
subjectKey: "command",
|
|
8717
8820
|
capabilities: ["shell.arbitrary"],
|
|
8718
|
-
|
|
8821
|
+
// Executor-level abort ceiling. Must sit ABOVE the per-call `timeout_ms`
|
|
8822
|
+
// ceiling (600_000): the tool's own timer tree-kills and returns a
|
|
8823
|
+
// structured `timed_out: true` result, while the executor's
|
|
8824
|
+
// AbortSignal.timeout is a blunt abort. The old value (300_000) meant any
|
|
8825
|
+
// timeout_ms > 5min was silently cut short by the executor. The 10s margin
|
|
8826
|
+
// covers the kill/teardown window. (The executor additionally clamps to
|
|
8827
|
+
// config `tools.maxToolTimeoutMs`.)
|
|
8828
|
+
timeoutMs: 61e4,
|
|
8719
8829
|
maxOutputBytes: MAX_OUTPUT,
|
|
8720
8830
|
estimatedDurationMs: 3e4,
|
|
8721
8831
|
inputSchema: {
|
|
@@ -8727,7 +8837,7 @@ var bashTool = {
|
|
|
8727
8837
|
},
|
|
8728
8838
|
timeout_ms: {
|
|
8729
8839
|
type: "integer",
|
|
8730
|
-
description: "Optional timeout for this specific command in milliseconds."
|
|
8840
|
+
description: "Optional timeout for this specific command in milliseconds (default 300000, max 600000)."
|
|
8731
8841
|
},
|
|
8732
8842
|
background: {
|
|
8733
8843
|
type: "boolean",
|
|
@@ -8778,16 +8888,7 @@ var bashTool = {
|
|
|
8778
8888
|
return;
|
|
8779
8889
|
}
|
|
8780
8890
|
const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
|
|
8781
|
-
|
|
8782
|
-
console.warn(JSON.stringify({
|
|
8783
|
-
level: "warn",
|
|
8784
|
-
event: "bash.pipe_to_shell_detected",
|
|
8785
|
-
message: "Detected pipe-to-shell pattern. Consider reviewing the full command before confirming.",
|
|
8786
|
-
command_prefix: input.command.slice(0, 100),
|
|
8787
|
-
// Log first 100 chars for review
|
|
8788
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
8789
|
-
}));
|
|
8790
|
-
}
|
|
8891
|
+
const pipeToShellNote = PIPE_TO_SHELL_PATTERN.test(input.command) ? "\n\n[wrongstack] Caution: this command pipes output into a shell interpreter (pipe-to-shell). Piped content executes as arbitrary code \u2014 review the source before trusting the result, and prefer downloading to a file and inspecting it first." : "";
|
|
8791
8892
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS2, 6e5));
|
|
8792
8893
|
const isWin5 = os4.platform() === "win32";
|
|
8793
8894
|
let plan;
|
|
@@ -8821,11 +8922,12 @@ var bashTool = {
|
|
|
8821
8922
|
const shell = plan.bin;
|
|
8822
8923
|
const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
|
|
8823
8924
|
const env = buildChildEnv2(ctx.session?.id);
|
|
8925
|
+
const spawnCwd = ctx.workingDir ?? ctx.projectRoot;
|
|
8824
8926
|
const detached = !isWin5;
|
|
8825
8927
|
const startedAt = Date.now();
|
|
8826
8928
|
if (input.background) {
|
|
8827
8929
|
const child2 = spawn3(shell, args, {
|
|
8828
|
-
cwd:
|
|
8930
|
+
cwd: spawnCwd,
|
|
8829
8931
|
env,
|
|
8830
8932
|
// PowerShell takes the script on stdin (no argv quoting); cmd.exe
|
|
8831
8933
|
// and POSIX shells ignore stdin when given the command inline.
|
|
@@ -8856,7 +8958,7 @@ var bashTool = {
|
|
|
8856
8958
|
parentPid: process.pid,
|
|
8857
8959
|
command: redactCommand(`${shell} ${args.join(" ")}`),
|
|
8858
8960
|
args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
|
|
8859
|
-
cwd:
|
|
8961
|
+
cwd: spawnCwd,
|
|
8860
8962
|
background: true,
|
|
8861
8963
|
startedAt: new Date(startedAt).toISOString()
|
|
8862
8964
|
});
|
|
@@ -8900,7 +9002,9 @@ var bashTool = {
|
|
|
8900
9002
|
yield {
|
|
8901
9003
|
type: "final",
|
|
8902
9004
|
output: {
|
|
8903
|
-
output
|
|
9005
|
+
// Background runs have no captured output; the pipe-to-shell caution
|
|
9006
|
+
// (when present) is the only thing worth surfacing.
|
|
9007
|
+
output: pipeToShellNote.trim(),
|
|
8904
9008
|
exit_code: null,
|
|
8905
9009
|
timed_out: false,
|
|
8906
9010
|
pid: pid2
|
|
@@ -8917,7 +9021,7 @@ var bashTool = {
|
|
|
8917
9021
|
return;
|
|
8918
9022
|
}
|
|
8919
9023
|
const child = spawn3(shell, args, {
|
|
8920
|
-
cwd:
|
|
9024
|
+
cwd: spawnCwd,
|
|
8921
9025
|
env,
|
|
8922
9026
|
// PowerShell takes the script on stdin (no argv quoting); cmd.exe
|
|
8923
9027
|
// and POSIX shells ignore stdin when given the command inline.
|
|
@@ -8942,7 +9046,7 @@ var bashTool = {
|
|
|
8942
9046
|
parentPid: process.pid,
|
|
8943
9047
|
command: redactCommand(`${shell} ${args.join(" ")}`),
|
|
8944
9048
|
args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
|
|
8945
|
-
cwd:
|
|
9049
|
+
cwd: spawnCwd,
|
|
8946
9050
|
background: false,
|
|
8947
9051
|
startedAt: new Date(startedAt).toISOString()
|
|
8948
9052
|
});
|
|
@@ -9033,10 +9137,10 @@ var bashTool = {
|
|
|
9033
9137
|
queue.push(c);
|
|
9034
9138
|
}
|
|
9035
9139
|
};
|
|
9036
|
-
const next = () => new Promise((
|
|
9140
|
+
const next = () => new Promise((resolve17) => {
|
|
9037
9141
|
const c = queue.shift();
|
|
9038
|
-
if (c)
|
|
9039
|
-
else resolveNext =
|
|
9142
|
+
if (c) resolve17(c);
|
|
9143
|
+
else resolveNext = resolve17;
|
|
9040
9144
|
});
|
|
9041
9145
|
let lastFlush = Date.now();
|
|
9042
9146
|
const flush = () => {
|
|
@@ -9061,8 +9165,10 @@ var bashTool = {
|
|
|
9061
9165
|
child.stderr?.resume();
|
|
9062
9166
|
}
|
|
9063
9167
|
};
|
|
9168
|
+
const stdoutDecoder = new StringDecoder("utf8");
|
|
9169
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
9064
9170
|
const onData = (chunk, stream) => {
|
|
9065
|
-
const text =
|
|
9171
|
+
const text = (stream === "stdout" ? stdoutDecoder : stderrDecoder).write(chunk);
|
|
9066
9172
|
if (stream === "stdout") stdoutBytes += chunk.byteLength;
|
|
9067
9173
|
else stderrBytes += chunk.byteLength;
|
|
9068
9174
|
emitProcessOutput2({ pid, stream, chunk });
|
|
@@ -9089,6 +9195,12 @@ var bashTool = {
|
|
|
9089
9195
|
if (typeof pid === "number") registry.unregister(pid);
|
|
9090
9196
|
registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
|
|
9091
9197
|
completeForeground(timedOut ? 124 : code ?? (signal ? 1 : 0), signal ?? void 0);
|
|
9198
|
+
const tail = stdoutDecoder.end() + stderrDecoder.end();
|
|
9199
|
+
if (tail) {
|
|
9200
|
+
if (buf.length < MAX_OUTPUT) buf += tail.slice(0, MAX_OUTPUT - buf.length);
|
|
9201
|
+
spool.write(tail);
|
|
9202
|
+
pending2 += tail;
|
|
9203
|
+
}
|
|
9092
9204
|
push({ kind: "end", code });
|
|
9093
9205
|
});
|
|
9094
9206
|
try {
|
|
@@ -9108,7 +9220,7 @@ var bashTool = {
|
|
|
9108
9220
|
output: {
|
|
9109
9221
|
output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
|
|
9110
9222
|
|
|
9111
|
-
${hint}` : ""),
|
|
9223
|
+
${hint}` : "") + pipeToShellNote,
|
|
9112
9224
|
exit_code: c.code,
|
|
9113
9225
|
timed_out: timedOut
|
|
9114
9226
|
}
|
|
@@ -9170,7 +9282,7 @@ ${hint}` : ""),
|
|
|
9170
9282
|
if (!sessionId) return;
|
|
9171
9283
|
for (const entry of registry.bySession(sessionId)) {
|
|
9172
9284
|
if (entry.name !== "bash") continue;
|
|
9173
|
-
if (entry.child.exitCode !== null) continue;
|
|
9285
|
+
if (entry.child && entry.child.exitCode !== null) continue;
|
|
9174
9286
|
if (entry.background) continue;
|
|
9175
9287
|
if (entry.protected) continue;
|
|
9176
9288
|
registry.kill(entry.pid, { force: true });
|
|
@@ -9415,8 +9527,8 @@ function sweepOldArtifacts(root) {
|
|
|
9415
9527
|
for (const name of names) {
|
|
9416
9528
|
const target = path10.join(dir, name);
|
|
9417
9529
|
try {
|
|
9418
|
-
const
|
|
9419
|
-
if (
|
|
9530
|
+
const stat19 = await fs7.stat(target);
|
|
9531
|
+
if (stat19.isFile() && stat19.mtimeMs < cutoff) {
|
|
9420
9532
|
await fs7.rm(target, { force: true });
|
|
9421
9533
|
removed++;
|
|
9422
9534
|
}
|
|
@@ -9444,14 +9556,14 @@ var BrowserArtifactStore = class {
|
|
|
9444
9556
|
}
|
|
9445
9557
|
async record(id, sessionId, kind, target, mimeType) {
|
|
9446
9558
|
await fs7.chmod(target, 384).catch(() => void 0);
|
|
9447
|
-
const [
|
|
9559
|
+
const [stat19, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
|
|
9448
9560
|
const artifact = {
|
|
9449
9561
|
id,
|
|
9450
9562
|
kind,
|
|
9451
9563
|
sensitivity: "sensitive",
|
|
9452
9564
|
path: target,
|
|
9453
9565
|
mimeType,
|
|
9454
|
-
sizeBytes:
|
|
9566
|
+
sizeBytes: stat19.size,
|
|
9455
9567
|
sha256,
|
|
9456
9568
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9457
9569
|
};
|
|
@@ -9491,11 +9603,11 @@ var BrowserArtifactStore = class {
|
|
|
9491
9603
|
};
|
|
9492
9604
|
async function hashFile(target) {
|
|
9493
9605
|
const hash = createHash3("sha256");
|
|
9494
|
-
await new Promise((
|
|
9606
|
+
await new Promise((resolve17, reject) => {
|
|
9495
9607
|
const stream = createReadStream(target);
|
|
9496
9608
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
9497
9609
|
stream.once("error", reject);
|
|
9498
|
-
stream.once("end",
|
|
9610
|
+
stream.once("end", resolve17);
|
|
9499
9611
|
});
|
|
9500
9612
|
return hash.digest("hex");
|
|
9501
9613
|
}
|
|
@@ -9645,7 +9757,7 @@ var BrowserNetworkGuardProxy = class {
|
|
|
9645
9757
|
async start() {
|
|
9646
9758
|
if (this.url) return this.url;
|
|
9647
9759
|
if (this.startPromise) return this.startPromise;
|
|
9648
|
-
this.startPromise = new Promise((
|
|
9760
|
+
this.startPromise = new Promise((resolve17, reject) => {
|
|
9649
9761
|
const onError = (error) => {
|
|
9650
9762
|
this.server.off("listening", onListening);
|
|
9651
9763
|
reject(error);
|
|
@@ -9658,7 +9770,7 @@ var BrowserNetworkGuardProxy = class {
|
|
|
9658
9770
|
return;
|
|
9659
9771
|
}
|
|
9660
9772
|
this.url = `http://127.0.0.1:${address.port}`;
|
|
9661
|
-
|
|
9773
|
+
resolve17(this.url);
|
|
9662
9774
|
};
|
|
9663
9775
|
this.server.once("error", onError);
|
|
9664
9776
|
this.server.once("listening", onListening);
|
|
@@ -9673,7 +9785,7 @@ var BrowserNetworkGuardProxy = class {
|
|
|
9673
9785
|
for (const socket of this.sockets) socket.destroy();
|
|
9674
9786
|
this.sockets.clear();
|
|
9675
9787
|
if (!this.server.listening) return;
|
|
9676
|
-
await new Promise((
|
|
9788
|
+
await new Promise((resolve17) => this.server.close(() => resolve17()));
|
|
9677
9789
|
}
|
|
9678
9790
|
async forwardHttp(request2, response) {
|
|
9679
9791
|
try {
|
|
@@ -9703,8 +9815,8 @@ var BrowserNetworkGuardProxy = class {
|
|
|
9703
9815
|
upstream.on("error", () => writeProxyError(response, 502, "Bad Gateway"));
|
|
9704
9816
|
request2.on("aborted", () => upstream.destroy());
|
|
9705
9817
|
request2.pipe(upstream);
|
|
9706
|
-
} catch {
|
|
9707
|
-
writeProxyError(response, 403,
|
|
9818
|
+
} catch (error) {
|
|
9819
|
+
writeProxyError(response, 403, policyBlockMessage(error));
|
|
9708
9820
|
}
|
|
9709
9821
|
}
|
|
9710
9822
|
async forwardConnect(request2, client, head) {
|
|
@@ -9728,8 +9840,8 @@ var BrowserNetworkGuardProxy = class {
|
|
|
9728
9840
|
pipeDuplexPair(upstream, client);
|
|
9729
9841
|
});
|
|
9730
9842
|
client.once("close", () => upstream.destroy());
|
|
9731
|
-
} catch {
|
|
9732
|
-
client.end(
|
|
9843
|
+
} catch (error) {
|
|
9844
|
+
client.end(rawForbiddenResponse(policyBlockMessage(error)));
|
|
9733
9845
|
}
|
|
9734
9846
|
}
|
|
9735
9847
|
async forwardUpgrade(request2, client, head) {
|
|
@@ -9770,9 +9882,9 @@ var BrowserNetworkGuardProxy = class {
|
|
|
9770
9882
|
});
|
|
9771
9883
|
client.once("close", () => upstream?.destroy());
|
|
9772
9884
|
upstream.end();
|
|
9773
|
-
} catch {
|
|
9885
|
+
} catch (error) {
|
|
9774
9886
|
upstream?.destroy();
|
|
9775
|
-
client.end(
|
|
9887
|
+
client.end(rawForbiddenResponse(policyBlockMessage(error)));
|
|
9776
9888
|
}
|
|
9777
9889
|
}
|
|
9778
9890
|
resolve(rawUrl) {
|
|
@@ -9812,6 +9924,18 @@ function writeProxyError(response, status, message) {
|
|
|
9812
9924
|
response.writeHead(status, { "content-type": "text/plain", connection: "close" });
|
|
9813
9925
|
response.end(message);
|
|
9814
9926
|
}
|
|
9927
|
+
function policyBlockMessage(error) {
|
|
9928
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
9929
|
+
return reason ? `Blocked by browser network policy: ${reason}` : "Blocked by browser network policy";
|
|
9930
|
+
}
|
|
9931
|
+
function rawForbiddenResponse(message) {
|
|
9932
|
+
return `HTTP/1.1 403 Forbidden\r
|
|
9933
|
+
Content-Type: text/plain\r
|
|
9934
|
+
Content-Length: ${Buffer.byteLength(message)}\r
|
|
9935
|
+
Connection: close\r
|
|
9936
|
+
\r
|
|
9937
|
+
` + message;
|
|
9938
|
+
}
|
|
9815
9939
|
|
|
9816
9940
|
// src/browser/manager.ts
|
|
9817
9941
|
var DEFAULT_OPERATION_TIMEOUT_MS = 3e4;
|
|
@@ -10039,13 +10163,21 @@ var BrowserSessionManager = class {
|
|
|
10039
10163
|
if (relative12.startsWith("..") || path11.isAbsolute(relative12)) {
|
|
10040
10164
|
throw new Error("browser: upload files must stay inside the project root");
|
|
10041
10165
|
}
|
|
10042
|
-
|
|
10166
|
+
let realFile;
|
|
10167
|
+
try {
|
|
10168
|
+
realFile = await fs8.realpath(absolute);
|
|
10169
|
+
} catch (error) {
|
|
10170
|
+
if (error?.code === "ENOENT") {
|
|
10171
|
+
throw new Error(`browser: upload file not found: ${file}`);
|
|
10172
|
+
}
|
|
10173
|
+
throw error;
|
|
10174
|
+
}
|
|
10043
10175
|
const realRelative = path11.relative(realRoot, realFile);
|
|
10044
10176
|
if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
|
|
10045
10177
|
throw new Error("browser: upload files must not escape the project root through a symlink");
|
|
10046
10178
|
}
|
|
10047
|
-
const
|
|
10048
|
-
if (!
|
|
10179
|
+
const stat19 = await fs8.stat(realFile);
|
|
10180
|
+
if (!stat19.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
|
|
10049
10181
|
resolved.push(realFile);
|
|
10050
10182
|
}
|
|
10051
10183
|
await this.runPageOperation(
|
|
@@ -10226,7 +10358,7 @@ function pushBounded(target, value, limit) {
|
|
|
10226
10358
|
}
|
|
10227
10359
|
async function abortable(signal, operation, onAbort) {
|
|
10228
10360
|
signal.throwIfAborted();
|
|
10229
|
-
return new Promise((
|
|
10361
|
+
return new Promise((resolve17, reject) => {
|
|
10230
10362
|
let settled = false;
|
|
10231
10363
|
let aborting = false;
|
|
10232
10364
|
const finish = (fn) => {
|
|
@@ -10244,7 +10376,7 @@ async function abortable(signal, operation, onAbort) {
|
|
|
10244
10376
|
signal.addEventListener("abort", abort, { once: true });
|
|
10245
10377
|
operation().then(
|
|
10246
10378
|
(value) => {
|
|
10247
|
-
if (!aborting) finish(() =>
|
|
10379
|
+
if (!aborting) finish(() => resolve17(value));
|
|
10248
10380
|
},
|
|
10249
10381
|
(err) => {
|
|
10250
10382
|
if (!aborting) finish(() => reject(err));
|
|
@@ -10301,7 +10433,7 @@ var sessionIdSchema = {
|
|
|
10301
10433
|
};
|
|
10302
10434
|
var browserOpenTool = {
|
|
10303
10435
|
name: "browser_open",
|
|
10304
|
-
description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL.",
|
|
10436
|
+
description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL. Private/localhost origins are blocked by default; enable specific origins via the WRONGSTACK_BROWSER_PRIVATE_ORIGINS env allowlist.",
|
|
10305
10437
|
usageHint: "browser_open({ url?, width?, height?, trace? })",
|
|
10306
10438
|
permission: "confirm",
|
|
10307
10439
|
mutating: true,
|
|
@@ -10354,7 +10486,7 @@ var browserStatusTool = {
|
|
|
10354
10486
|
};
|
|
10355
10487
|
var browserNavigateTool = {
|
|
10356
10488
|
name: "browser_navigate",
|
|
10357
|
-
description: "Navigate an owned browser session to an approved http(s) URL.",
|
|
10489
|
+
description: "Navigate an owned browser session to an approved http(s) URL. Private/localhost origins are blocked by default; enable specific origins via the WRONGSTACK_BROWSER_PRIVATE_ORIGINS env allowlist.",
|
|
10358
10490
|
usageHint: "browser_navigate({ sessionId, url })",
|
|
10359
10491
|
permission: "confirm",
|
|
10360
10492
|
mutating: true,
|
|
@@ -10693,7 +10825,7 @@ for (const tool of browserTools) tool.timeoutMs ??= 6e4;
|
|
|
10693
10825
|
import { spawn as spawn4 } from "node:child_process";
|
|
10694
10826
|
import * as fs13 from "node:fs";
|
|
10695
10827
|
import * as net3 from "node:net";
|
|
10696
|
-
import { StringDecoder } from "node:string_decoder";
|
|
10828
|
+
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
10697
10829
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10698
10830
|
import { checkUnixSocketPath } from "@wrongstack/core/utils";
|
|
10699
10831
|
|
|
@@ -10800,7 +10932,6 @@ import * as fs12 from "node:fs";
|
|
|
10800
10932
|
import * as os5 from "node:os";
|
|
10801
10933
|
import * as path17 from "node:path";
|
|
10802
10934
|
import { fileURLToPath } from "node:url";
|
|
10803
|
-
import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
|
|
10804
10935
|
|
|
10805
10936
|
// src/codebase-index/writer.ts
|
|
10806
10937
|
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
@@ -13735,12 +13866,12 @@ function projectIndexServerBuildId(entrypoint) {
|
|
|
13735
13866
|
const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
|
|
13736
13867
|
const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path17.resolve(cleanHref);
|
|
13737
13868
|
try {
|
|
13738
|
-
const
|
|
13739
|
-
if (buildIdCache?.file === file && buildIdCache.mtimeMs ===
|
|
13869
|
+
const stat19 = fs12.statSync(file);
|
|
13870
|
+
if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat19.mtimeMs && buildIdCache.size === stat19.size) {
|
|
13740
13871
|
return buildIdCache.buildId;
|
|
13741
13872
|
}
|
|
13742
13873
|
const buildId = createHash4("sha256").update(fs12.readFileSync(file)).digest("hex").slice(0, 24);
|
|
13743
|
-
buildIdCache = { file, mtimeMs:
|
|
13874
|
+
buildIdCache = { file, mtimeMs: stat19.mtimeMs, size: stat19.size, buildId };
|
|
13744
13875
|
return buildId;
|
|
13745
13876
|
} catch {
|
|
13746
13877
|
return `unreadable:${path17.basename(file)}`;
|
|
@@ -13882,8 +14013,8 @@ function isProjectIndexServerHealth(value) {
|
|
|
13882
14013
|
return typeof health.checkedAt === "number" && typeof health.uptimeMs === "number" && typeof memory?.rss === "number" && typeof memory.heapUsed === "number" && typeof memory.heapTotal === "number" && typeof memory.external === "number" && typeof health.clients === "number" && typeof health.activeRequests === "number" && typeof health.activeWrites === "number" && typeof health.queuedWrites === "number" && typeof health.pendingExternalFiles === "number" && typeof health.watchingExternal === "boolean" && typeof activity?.indexing === "boolean" && typeof activity.currentFile === "number" && typeof activity.totalFiles === "number" && typeof activity.generation === "number";
|
|
13883
14014
|
}
|
|
13884
14015
|
function delay(ms) {
|
|
13885
|
-
return new Promise((
|
|
13886
|
-
const timer = setTimeout(
|
|
14016
|
+
return new Promise((resolve17) => {
|
|
14017
|
+
const timer = setTimeout(resolve17, ms);
|
|
13887
14018
|
timer.unref?.();
|
|
13888
14019
|
});
|
|
13889
14020
|
}
|
|
@@ -14087,7 +14218,7 @@ var ProjectServerConnection = class {
|
|
|
14087
14218
|
return Promise.reject(new Error("codebase-index server connection is not available"));
|
|
14088
14219
|
}
|
|
14089
14220
|
const id = this.nextId++;
|
|
14090
|
-
return new Promise((
|
|
14221
|
+
return new Promise((resolve17, reject) => {
|
|
14091
14222
|
const timer = setTimeout(() => {
|
|
14092
14223
|
const entry = this.pending.get(id);
|
|
14093
14224
|
if (!entry) return;
|
|
@@ -14110,7 +14241,7 @@ var ProjectServerConnection = class {
|
|
|
14110
14241
|
entry.reject(cancellationError(signal));
|
|
14111
14242
|
} : void 0;
|
|
14112
14243
|
this.pending.set(id, {
|
|
14113
|
-
resolve:
|
|
14244
|
+
resolve: resolve17,
|
|
14114
14245
|
reject,
|
|
14115
14246
|
timer,
|
|
14116
14247
|
signal,
|
|
@@ -14179,7 +14310,7 @@ var ProjectServerConnection = class {
|
|
|
14179
14310
|
this.binaryBuffer = [];
|
|
14180
14311
|
this.useBinary = false;
|
|
14181
14312
|
this.textDecoder = null;
|
|
14182
|
-
return new Promise((
|
|
14313
|
+
return new Promise((resolve17, reject) => {
|
|
14183
14314
|
const socket = net3.createConnection(this.endpoint);
|
|
14184
14315
|
this.socket = socket;
|
|
14185
14316
|
const timer = setTimeout(() => {
|
|
@@ -14191,7 +14322,7 @@ var ProjectServerConnection = class {
|
|
|
14191
14322
|
clearTimeout(timer);
|
|
14192
14323
|
this.connectResolve = null;
|
|
14193
14324
|
this.connectReject = null;
|
|
14194
|
-
|
|
14325
|
+
resolve17();
|
|
14195
14326
|
};
|
|
14196
14327
|
const finishReject = (error) => {
|
|
14197
14328
|
clearTimeout(timer);
|
|
@@ -14214,7 +14345,7 @@ var ProjectServerConnection = class {
|
|
|
14214
14345
|
this.onBinaryData(socket, chunk);
|
|
14215
14346
|
return;
|
|
14216
14347
|
}
|
|
14217
|
-
if (!this.textDecoder) this.textDecoder = new
|
|
14348
|
+
if (!this.textDecoder) this.textDecoder = new StringDecoder2("utf8");
|
|
14218
14349
|
this.buffer += this.textDecoder.write(chunk);
|
|
14219
14350
|
while (true) {
|
|
14220
14351
|
const newline = this.buffer.indexOf("\n");
|
|
@@ -15205,9 +15336,9 @@ var ParserWorkerPool = class {
|
|
|
15205
15336
|
for (let i = 0; i < files.length; i++) {
|
|
15206
15337
|
chunks[i % workerCount].push(files[i]);
|
|
15207
15338
|
}
|
|
15208
|
-
return new Promise((
|
|
15339
|
+
return new Promise((resolve17, reject) => {
|
|
15209
15340
|
this.pending.set(batchId, {
|
|
15210
|
-
resolve:
|
|
15341
|
+
resolve: resolve17,
|
|
15211
15342
|
reject,
|
|
15212
15343
|
accumulated: [],
|
|
15213
15344
|
expectedWorkers: workerCount,
|
|
@@ -15238,10 +15369,10 @@ var ParserWorkerPool = class {
|
|
|
15238
15369
|
await Promise.allSettled(
|
|
15239
15370
|
workers.map(
|
|
15240
15371
|
(w) => Promise.race([
|
|
15241
|
-
new Promise((
|
|
15242
|
-
w.once("exit", () =>
|
|
15372
|
+
new Promise((resolve17) => {
|
|
15373
|
+
w.once("exit", () => resolve17());
|
|
15243
15374
|
}),
|
|
15244
|
-
new Promise((
|
|
15375
|
+
new Promise((resolve17) => setTimeout(() => resolve17(), 2e3))
|
|
15245
15376
|
]).then(() => {
|
|
15246
15377
|
if (!w.threadId) return;
|
|
15247
15378
|
return w.terminate().catch(() => {
|
|
@@ -15305,7 +15436,7 @@ function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
|
|
|
15305
15436
|
return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
|
|
15306
15437
|
}
|
|
15307
15438
|
function yieldEventLoop() {
|
|
15308
|
-
return new Promise((
|
|
15439
|
+
return new Promise((resolve17) => setImmediate(resolve17));
|
|
15309
15440
|
}
|
|
15310
15441
|
function throwIfAborted(signal) {
|
|
15311
15442
|
if (!signal?.aborted) return;
|
|
@@ -15333,7 +15464,7 @@ function normalizeComparablePath(value) {
|
|
|
15333
15464
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
15334
15465
|
}
|
|
15335
15466
|
function gitOutput(projectRoot, args) {
|
|
15336
|
-
return new Promise((
|
|
15467
|
+
return new Promise((resolve17, reject) => {
|
|
15337
15468
|
execFile(
|
|
15338
15469
|
"git",
|
|
15339
15470
|
["-C", projectRoot, ...args],
|
|
@@ -15344,7 +15475,7 @@ function gitOutput(projectRoot, args) {
|
|
|
15344
15475
|
},
|
|
15345
15476
|
(error, stdout) => {
|
|
15346
15477
|
if (error) reject(error);
|
|
15347
|
-
else
|
|
15478
|
+
else resolve17(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
|
|
15348
15479
|
}
|
|
15349
15480
|
);
|
|
15350
15481
|
});
|
|
@@ -15576,9 +15707,9 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15576
15707
|
const statReadParse = await Promise.allSettled(
|
|
15577
15708
|
batchFiles.map(
|
|
15578
15709
|
async (file) => {
|
|
15579
|
-
let
|
|
15710
|
+
let stat19;
|
|
15580
15711
|
try {
|
|
15581
|
-
|
|
15712
|
+
stat19 = await fs18.stat(file, statOpts);
|
|
15582
15713
|
} catch (e) {
|
|
15583
15714
|
if (isAbortError(e)) throw e;
|
|
15584
15715
|
return {
|
|
@@ -15590,21 +15721,21 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15590
15721
|
missing: isMissingPathError(e)
|
|
15591
15722
|
};
|
|
15592
15723
|
}
|
|
15593
|
-
if (!
|
|
15724
|
+
if (!stat19.isFile()) return { file, stat: stat19, lang: "", parsed: null };
|
|
15594
15725
|
const lang = detectLang(file);
|
|
15595
|
-
if (!lang) return { file, stat:
|
|
15596
|
-
if (
|
|
15726
|
+
if (!lang) return { file, stat: stat19, lang: "", parsed: null };
|
|
15727
|
+
if (stat19.size > MAX_INDEX_FILE_BYTES) {
|
|
15597
15728
|
return {
|
|
15598
15729
|
file,
|
|
15599
|
-
stat:
|
|
15730
|
+
stat: stat19,
|
|
15600
15731
|
lang,
|
|
15601
15732
|
parsed: null,
|
|
15602
|
-
error: `file too large (${
|
|
15733
|
+
error: `file too large (${stat19.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
15603
15734
|
};
|
|
15604
15735
|
}
|
|
15605
15736
|
const meta = existingMeta.get(file);
|
|
15606
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
15607
|
-
return { file, stat:
|
|
15737
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat19.mtimeMs)) {
|
|
15738
|
+
return { file, stat: stat19, lang, parsed: null, skippedMeta: meta };
|
|
15608
15739
|
}
|
|
15609
15740
|
let content;
|
|
15610
15741
|
try {
|
|
@@ -15613,7 +15744,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15613
15744
|
if (isAbortError(e)) throw e;
|
|
15614
15745
|
return {
|
|
15615
15746
|
file,
|
|
15616
|
-
stat:
|
|
15747
|
+
stat: stat19,
|
|
15617
15748
|
lang,
|
|
15618
15749
|
parsed: null,
|
|
15619
15750
|
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
@@ -15623,15 +15754,15 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15623
15754
|
if (!force && meta && meta.contentHash && contentHash === meta.contentHash) {
|
|
15624
15755
|
return {
|
|
15625
15756
|
file,
|
|
15626
|
-
stat:
|
|
15757
|
+
stat: stat19,
|
|
15627
15758
|
lang,
|
|
15628
15759
|
parsed: null,
|
|
15629
15760
|
content,
|
|
15630
15761
|
contentHash,
|
|
15631
|
-
skippedMeta: { ...meta, mtimeMs: Math.floor(
|
|
15762
|
+
skippedMeta: { ...meta, mtimeMs: Math.floor(stat19.mtimeMs) }
|
|
15632
15763
|
};
|
|
15633
15764
|
}
|
|
15634
|
-
return { file, stat:
|
|
15765
|
+
return { file, stat: stat19, lang, parsed: null, content, contentHash };
|
|
15635
15766
|
}
|
|
15636
15767
|
)
|
|
15637
15768
|
);
|
|
@@ -15710,7 +15841,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15710
15841
|
filesFailed++;
|
|
15711
15842
|
continue;
|
|
15712
15843
|
}
|
|
15713
|
-
const { stat:
|
|
15844
|
+
const { stat: stat19, lang, parsed } = result;
|
|
15714
15845
|
if (result.skippedMeta) {
|
|
15715
15846
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
15716
15847
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -15734,7 +15865,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15734
15865
|
store.upsertFile({
|
|
15735
15866
|
file,
|
|
15736
15867
|
lang,
|
|
15737
|
-
mtimeMs: Math.floor(
|
|
15868
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
15738
15869
|
symbolCount: 0,
|
|
15739
15870
|
lastIndexed: Date.now(),
|
|
15740
15871
|
contentHash: result.contentHash ?? ""
|
|
@@ -15748,7 +15879,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15748
15879
|
store.replaceEmptyFile({
|
|
15749
15880
|
file,
|
|
15750
15881
|
lang,
|
|
15751
|
-
mtimeMs: Math.floor(
|
|
15882
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
15752
15883
|
symbolCount: 0,
|
|
15753
15884
|
lastIndexed: Date.now(),
|
|
15754
15885
|
contentHash: result.contentHash ?? ""
|
|
@@ -15762,7 +15893,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15762
15893
|
lang,
|
|
15763
15894
|
symbols: parsed.symbols,
|
|
15764
15895
|
refs: parsed.refs ?? [],
|
|
15765
|
-
mtimeMs: Math.floor(
|
|
15896
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
15766
15897
|
symbolCount: parsed.symbols.length,
|
|
15767
15898
|
contentHash: result.contentHash ?? ""
|
|
15768
15899
|
});
|
|
@@ -16087,7 +16218,7 @@ function callIndexOp(op, args, opts) {
|
|
|
16087
16218
|
opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
|
|
16088
16219
|
);
|
|
16089
16220
|
}
|
|
16090
|
-
return new Promise((
|
|
16221
|
+
return new Promise((resolve17, reject) => {
|
|
16091
16222
|
const id = nextRpcId++;
|
|
16092
16223
|
const timer = setTimeout(() => {
|
|
16093
16224
|
pending.delete(id);
|
|
@@ -16109,7 +16240,7 @@ function callIndexOp(op, args, opts) {
|
|
|
16109
16240
|
pending.set(id, {
|
|
16110
16241
|
resolve: (v) => {
|
|
16111
16242
|
cleanup();
|
|
16112
|
-
|
|
16243
|
+
resolve17(v);
|
|
16113
16244
|
},
|
|
16114
16245
|
reject: (e) => {
|
|
16115
16246
|
cleanup();
|
|
@@ -16260,6 +16391,160 @@ async function outgoingCallsService2(args) {
|
|
|
16260
16391
|
}
|
|
16261
16392
|
|
|
16262
16393
|
// src/codebase-index/codebase-index-tool.ts
|
|
16394
|
+
import { ToolValidationError } from "@wrongstack/core/types";
|
|
16395
|
+
|
|
16396
|
+
// src/codebase-index/codebase-search-tool.ts
|
|
16397
|
+
import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
|
|
16398
|
+
var INDEXABLE_LANG_IDS = [
|
|
16399
|
+
"ts",
|
|
16400
|
+
"tsx",
|
|
16401
|
+
"js",
|
|
16402
|
+
"jsx",
|
|
16403
|
+
"go",
|
|
16404
|
+
"py",
|
|
16405
|
+
"rs",
|
|
16406
|
+
"json",
|
|
16407
|
+
"yaml"
|
|
16408
|
+
];
|
|
16409
|
+
var codebaseSearchTool = {
|
|
16410
|
+
name: "codebase-search",
|
|
16411
|
+
category: "Project",
|
|
16412
|
+
icon: "index",
|
|
16413
|
+
description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Use `grep` instead for exact text, regexes, unsupported content, or concrete usage sites. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
|
|
16414
|
+
usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\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.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.",
|
|
16415
|
+
permission: "auto",
|
|
16416
|
+
mutating: false,
|
|
16417
|
+
capabilities: ["fs.read"],
|
|
16418
|
+
// The index host has its own 30s read watchdog. Leave enough headroom for
|
|
16419
|
+
// worker teardown and structured timeout reporting.
|
|
16420
|
+
timeoutMs: 35e3,
|
|
16421
|
+
inputSchema: {
|
|
16422
|
+
type: "object",
|
|
16423
|
+
properties: {
|
|
16424
|
+
query: {
|
|
16425
|
+
type: "string",
|
|
16426
|
+
description: "Search query \u2014 searches symbol names, signatures, and doc comments"
|
|
16427
|
+
},
|
|
16428
|
+
kind: {
|
|
16429
|
+
type: "string",
|
|
16430
|
+
enum: [
|
|
16431
|
+
"class",
|
|
16432
|
+
"interface",
|
|
16433
|
+
"enum",
|
|
16434
|
+
"type",
|
|
16435
|
+
"function",
|
|
16436
|
+
"method",
|
|
16437
|
+
"var",
|
|
16438
|
+
"const",
|
|
16439
|
+
"let",
|
|
16440
|
+
"property",
|
|
16441
|
+
"parameter",
|
|
16442
|
+
"namespace",
|
|
16443
|
+
"object",
|
|
16444
|
+
"literal",
|
|
16445
|
+
"schema",
|
|
16446
|
+
"struct",
|
|
16447
|
+
"trait",
|
|
16448
|
+
"impl",
|
|
16449
|
+
"static",
|
|
16450
|
+
"mod"
|
|
16451
|
+
],
|
|
16452
|
+
description: "Filter by indexed symbol kind"
|
|
16453
|
+
},
|
|
16454
|
+
lang: {
|
|
16455
|
+
type: "string",
|
|
16456
|
+
enum: [...INDEXABLE_LANG_IDS],
|
|
16457
|
+
description: "Filter by indexed language"
|
|
16458
|
+
},
|
|
16459
|
+
lspKind: {
|
|
16460
|
+
type: "integer",
|
|
16461
|
+
description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
|
|
16462
|
+
},
|
|
16463
|
+
file: {
|
|
16464
|
+
type: "string",
|
|
16465
|
+
description: "Filter to files matching this path substring"
|
|
16466
|
+
},
|
|
16467
|
+
limit: {
|
|
16468
|
+
type: "integer",
|
|
16469
|
+
description: "Maximum results to return (default 20, max 100)",
|
|
16470
|
+
minimum: 1,
|
|
16471
|
+
maximum: 100
|
|
16472
|
+
},
|
|
16473
|
+
preferLsp: {
|
|
16474
|
+
type: "boolean",
|
|
16475
|
+
description: "Prefer live LSP results over the index. Ignored unless the LSP plugin is active; when it is active and this is true, results come from live workspaceSymbol queries."
|
|
16476
|
+
}
|
|
16477
|
+
},
|
|
16478
|
+
required: ["query"]
|
|
16479
|
+
},
|
|
16480
|
+
async execute(input, ctx, execOpts) {
|
|
16481
|
+
const state = getIndexState();
|
|
16482
|
+
if (state.indexing && !state.ready) {
|
|
16483
|
+
return {
|
|
16484
|
+
results: [],
|
|
16485
|
+
total: 0,
|
|
16486
|
+
query: input.query,
|
|
16487
|
+
indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
|
|
16488
|
+
};
|
|
16489
|
+
}
|
|
16490
|
+
if (state.lastError) {
|
|
16491
|
+
const circuit = state.circuit;
|
|
16492
|
+
const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
|
|
16493
|
+
return {
|
|
16494
|
+
results: [],
|
|
16495
|
+
total: 0,
|
|
16496
|
+
query: input.query,
|
|
16497
|
+
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
16498
|
+
};
|
|
16499
|
+
}
|
|
16500
|
+
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
|
|
16501
|
+
let searched;
|
|
16502
|
+
try {
|
|
16503
|
+
searched = await searchCodebaseIndex(
|
|
16504
|
+
{
|
|
16505
|
+
projectRoot: ctx.projectRoot,
|
|
16506
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
16507
|
+
query: input.query,
|
|
16508
|
+
kind: input.kind?.toLowerCase(),
|
|
16509
|
+
lang: input.lang?.toLowerCase(),
|
|
16510
|
+
file: input.file,
|
|
16511
|
+
lspKind: input.lspKind,
|
|
16512
|
+
limit
|
|
16513
|
+
},
|
|
16514
|
+
{ signal: execOpts?.signal }
|
|
16515
|
+
);
|
|
16516
|
+
} catch (err) {
|
|
16517
|
+
if (execOpts?.signal?.aborted) throw err;
|
|
16518
|
+
return {
|
|
16519
|
+
results: [],
|
|
16520
|
+
total: 0,
|
|
16521
|
+
query: input.query,
|
|
16522
|
+
indexStatus: `Index query failed: ${toErrorMessage3(err)}. Fall back to grep/glob for this lookup.`
|
|
16523
|
+
};
|
|
16524
|
+
}
|
|
16525
|
+
const { results, total } = searched;
|
|
16526
|
+
let hasPersistedIndex = state.ready || total > 0;
|
|
16527
|
+
if (!hasPersistedIndex) {
|
|
16528
|
+
try {
|
|
16529
|
+
const stats = await codebaseIndexStats(
|
|
16530
|
+
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
16531
|
+
{ signal: execOpts?.signal }
|
|
16532
|
+
);
|
|
16533
|
+
hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
|
|
16534
|
+
} catch {
|
|
16535
|
+
}
|
|
16536
|
+
}
|
|
16537
|
+
return {
|
|
16538
|
+
results,
|
|
16539
|
+
total,
|
|
16540
|
+
query: input.query,
|
|
16541
|
+
...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
|
|
16542
|
+
};
|
|
16543
|
+
}
|
|
16544
|
+
};
|
|
16545
|
+
|
|
16546
|
+
// src/codebase-index/codebase-index-tool.ts
|
|
16547
|
+
var MAX_REPORTED_ERRORS = 20;
|
|
16263
16548
|
var codebaseIndexTool = {
|
|
16264
16549
|
name: "codebase-index",
|
|
16265
16550
|
category: "Project",
|
|
@@ -16285,12 +16570,23 @@ var codebaseIndexTool = {
|
|
|
16285
16570
|
},
|
|
16286
16571
|
langs: {
|
|
16287
16572
|
type: "array",
|
|
16288
|
-
items: { type: "string" },
|
|
16289
|
-
description:
|
|
16573
|
+
items: { type: "string", enum: [...INDEXABLE_LANG_IDS] },
|
|
16574
|
+
description: `Limit reindex to specific languages: ${INDEXABLE_LANG_IDS.join(", ")}`
|
|
16290
16575
|
}
|
|
16291
16576
|
}
|
|
16292
16577
|
},
|
|
16293
16578
|
async execute(input, ctx, execOpts) {
|
|
16579
|
+
if (input.langs) {
|
|
16580
|
+
const unknown = input.langs.filter(
|
|
16581
|
+
(lang) => !INDEXABLE_LANG_IDS.includes(lang)
|
|
16582
|
+
);
|
|
16583
|
+
if (unknown.length > 0) {
|
|
16584
|
+
throw new ToolValidationError({
|
|
16585
|
+
message: `codebase-index: unknown lang(s) ${unknown.map((l) => `"${l}"`).join(", ")}. Valid ids: ${INDEXABLE_LANG_IDS.join(", ")}.`,
|
|
16586
|
+
field: "langs"
|
|
16587
|
+
});
|
|
16588
|
+
}
|
|
16589
|
+
}
|
|
16294
16590
|
if (isIndexing()) {
|
|
16295
16591
|
return {
|
|
16296
16592
|
filesIndexed: 0,
|
|
@@ -16312,23 +16608,32 @@ var codebaseIndexTool = {
|
|
|
16312
16608
|
note: `Codebase indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}). Auto-retry possible in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s; the user can run /codebase-reindex to retry immediately.`
|
|
16313
16609
|
};
|
|
16314
16610
|
}
|
|
16315
|
-
|
|
16611
|
+
const result = await runStartupIndex({
|
|
16316
16612
|
projectRoot: ctx.projectRoot,
|
|
16317
16613
|
force: input.force ?? false,
|
|
16318
16614
|
langs: input.langs,
|
|
16319
16615
|
indexDir: codebaseIndexDirOverride(ctx),
|
|
16320
16616
|
signal: execOpts?.signal
|
|
16321
16617
|
});
|
|
16618
|
+
if (result.errors.length > MAX_REPORTED_ERRORS) {
|
|
16619
|
+
const hidden = result.errors.length - MAX_REPORTED_ERRORS;
|
|
16620
|
+
return {
|
|
16621
|
+
...result,
|
|
16622
|
+
errors: [...result.errors.slice(0, MAX_REPORTED_ERRORS), `+${hidden} more`]
|
|
16623
|
+
};
|
|
16624
|
+
}
|
|
16625
|
+
return result;
|
|
16322
16626
|
}
|
|
16323
16627
|
};
|
|
16324
16628
|
|
|
16325
16629
|
// src/codebase-index/codebase-incoming-calls-tool.ts
|
|
16630
|
+
import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
|
|
16326
16631
|
var codebaseIncomingCallsTool = {
|
|
16327
16632
|
name: "codebase-incoming-calls",
|
|
16328
16633
|
category: "Project",
|
|
16329
16634
|
icon: "index",
|
|
16330
|
-
description: "Find all callers of a function, method, or symbol \u2014 who invokes or references it. Uses the codebase index ref graph for instant, exact results.
|
|
16331
|
-
usageHint: 'CALL THIS BEFORE REFACTORING OR CHANGING ANY FUNCTION:\n\n-
|
|
16635
|
+
description: "Find all callers of a function, method, or symbol \u2014 who invokes or references it. Uses the codebase index ref graph for instant, exact results. Prefer this over grep for change-impact checks when the index is available.",
|
|
16636
|
+
usageHint: 'CALL THIS BEFORE REFACTORING OR CHANGING ANY FUNCTION:\n\n- Prefer this over grep when the index is available; fall back to grep when the index is cold/unavailable or for dynamic dispatch the ref graph cannot see.\n- Call codebase-incoming-calls({ symbol: "funcName" }) before editing the symbol.\n- Returns exact files, line numbers, caller signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Combine with codebase-outgoing-calls to see what the symbol itself calls.\nIf the index is not built, run codebase-index first.',
|
|
16332
16637
|
permission: "auto",
|
|
16333
16638
|
mutating: false,
|
|
16334
16639
|
capabilities: ["fs.read"],
|
|
@@ -16380,16 +16685,27 @@ var codebaseIncomingCallsTool = {
|
|
|
16380
16685
|
}
|
|
16381
16686
|
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
|
|
16382
16687
|
const transitive = input.transitive === true;
|
|
16383
|
-
|
|
16384
|
-
|
|
16385
|
-
|
|
16386
|
-
|
|
16688
|
+
let serviced;
|
|
16689
|
+
try {
|
|
16690
|
+
serviced = await incomingCallsService2(
|
|
16691
|
+
{
|
|
16692
|
+
projectRoot: ctx.projectRoot,
|
|
16693
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
16694
|
+
symbol: input.symbol,
|
|
16695
|
+
file: input.file,
|
|
16696
|
+
limit,
|
|
16697
|
+
transitive
|
|
16698
|
+
}
|
|
16699
|
+
);
|
|
16700
|
+
} catch (err) {
|
|
16701
|
+
return {
|
|
16387
16702
|
symbol: input.symbol,
|
|
16388
|
-
|
|
16389
|
-
|
|
16390
|
-
|
|
16391
|
-
}
|
|
16392
|
-
|
|
16703
|
+
calls: [],
|
|
16704
|
+
total: 0,
|
|
16705
|
+
indexStatus: `Index query failed: ${toErrorMessage4(err)}. Fall back to grep for this lookup.`
|
|
16706
|
+
};
|
|
16707
|
+
}
|
|
16708
|
+
const { calls, symbolFound, ambiguous, totalMatches } = serviced;
|
|
16393
16709
|
if (!symbolFound) {
|
|
16394
16710
|
let hasPersistedIndex = state.ready;
|
|
16395
16711
|
if (!hasPersistedIndex) {
|
|
@@ -16434,12 +16750,13 @@ var codebaseIncomingCallsTool = {
|
|
|
16434
16750
|
};
|
|
16435
16751
|
|
|
16436
16752
|
// src/codebase-index/codebase-outgoing-calls-tool.ts
|
|
16753
|
+
import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
|
|
16437
16754
|
var codebaseOutgoingCallsTool = {
|
|
16438
16755
|
name: "codebase-outgoing-calls",
|
|
16439
16756
|
category: "Project",
|
|
16440
16757
|
icon: "index",
|
|
16441
16758
|
description: "Find all functions/methods/symbols that a given symbol calls or depends on \u2014 its callees. Uses the codebase index ref graph for instant, exact results. Use this to understand a function's dependencies before modifying it.",
|
|
16442
|
-
usageHint: 'USE THIS TO UNDERSTAND A FUNCTION\'S DEPENDENCIES:\n\n- Call codebase-outgoing-calls({ symbol: "funcName" }) to see everything it calls.\n- Returns exact files, line numbers, callee signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Pair with codebase-incoming-calls for a complete impact picture: incoming = who calls you, outgoing = what you call.\nIf the index is not built, run codebase-index first.',
|
|
16759
|
+
usageHint: 'USE THIS TO UNDERSTAND A FUNCTION\'S DEPENDENCIES:\n\n- Prefer this over grep when the index is available; fall back to grep when the index is cold/unavailable or for dynamic dispatch the ref graph cannot see.\n- Call codebase-outgoing-calls({ symbol: "funcName" }) to see everything it calls.\n- Returns exact files, line numbers, callee signatures, and call types in milliseconds.\n- Use `file` to disambiguate when multiple symbols share a name.\n- Pair with codebase-incoming-calls for a complete impact picture: incoming = who calls you, outgoing = what you call.\nIf the index is not built, run codebase-index first.',
|
|
16443
16760
|
permission: "auto",
|
|
16444
16761
|
mutating: false,
|
|
16445
16762
|
capabilities: ["fs.read"],
|
|
@@ -16491,16 +16808,27 @@ var codebaseOutgoingCallsTool = {
|
|
|
16491
16808
|
}
|
|
16492
16809
|
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
|
|
16493
16810
|
const transitive = input.transitive === true;
|
|
16494
|
-
|
|
16495
|
-
|
|
16496
|
-
|
|
16497
|
-
|
|
16811
|
+
let serviced;
|
|
16812
|
+
try {
|
|
16813
|
+
serviced = await outgoingCallsService2(
|
|
16814
|
+
{
|
|
16815
|
+
projectRoot: ctx.projectRoot,
|
|
16816
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
16817
|
+
symbol: input.symbol,
|
|
16818
|
+
file: input.file,
|
|
16819
|
+
limit,
|
|
16820
|
+
transitive
|
|
16821
|
+
}
|
|
16822
|
+
);
|
|
16823
|
+
} catch (err) {
|
|
16824
|
+
return {
|
|
16498
16825
|
symbol: input.symbol,
|
|
16499
|
-
|
|
16500
|
-
|
|
16501
|
-
|
|
16502
|
-
}
|
|
16503
|
-
|
|
16826
|
+
calls: [],
|
|
16827
|
+
total: 0,
|
|
16828
|
+
indexStatus: `Index query failed: ${toErrorMessage5(err)}. Fall back to grep for this lookup.`
|
|
16829
|
+
};
|
|
16830
|
+
}
|
|
16831
|
+
const { calls, symbolFound, unresolvedCount, totalMatches } = serviced;
|
|
16504
16832
|
if (!symbolFound) {
|
|
16505
16833
|
let hasPersistedIndex = state.ready;
|
|
16506
16834
|
if (!hasPersistedIndex) {
|
|
@@ -16544,135 +16872,9 @@ var codebaseOutgoingCallsTool = {
|
|
|
16544
16872
|
}
|
|
16545
16873
|
};
|
|
16546
16874
|
|
|
16547
|
-
// src/codebase-index/codebase-
|
|
16548
|
-
var
|
|
16549
|
-
name: "codebase-
|
|
16550
|
-
category: "Project",
|
|
16551
|
-
icon: "index",
|
|
16552
|
-
description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
|
|
16553
|
-
usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\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.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.\nUse `grep` afterwards for exact text, regexes, unsupported content, or concrete usage sites.",
|
|
16554
|
-
permission: "auto",
|
|
16555
|
-
mutating: false,
|
|
16556
|
-
capabilities: ["fs.read"],
|
|
16557
|
-
// The index host has its own 30s read watchdog. Leave enough headroom for
|
|
16558
|
-
// worker teardown and structured timeout reporting.
|
|
16559
|
-
timeoutMs: 35e3,
|
|
16560
|
-
inputSchema: {
|
|
16561
|
-
type: "object",
|
|
16562
|
-
properties: {
|
|
16563
|
-
query: {
|
|
16564
|
-
type: "string",
|
|
16565
|
-
description: "Search query \u2014 searches symbol names, signatures, and doc comments"
|
|
16566
|
-
},
|
|
16567
|
-
kind: {
|
|
16568
|
-
type: "string",
|
|
16569
|
-
enum: [
|
|
16570
|
-
"class",
|
|
16571
|
-
"interface",
|
|
16572
|
-
"enum",
|
|
16573
|
-
"type",
|
|
16574
|
-
"function",
|
|
16575
|
-
"method",
|
|
16576
|
-
"var",
|
|
16577
|
-
"const",
|
|
16578
|
-
"let",
|
|
16579
|
-
"property",
|
|
16580
|
-
"parameter",
|
|
16581
|
-
"namespace",
|
|
16582
|
-
"object",
|
|
16583
|
-
"literal",
|
|
16584
|
-
"schema",
|
|
16585
|
-
"struct",
|
|
16586
|
-
"trait",
|
|
16587
|
-
"impl",
|
|
16588
|
-
"static",
|
|
16589
|
-
"mod"
|
|
16590
|
-
],
|
|
16591
|
-
description: "Filter by indexed symbol kind"
|
|
16592
|
-
},
|
|
16593
|
-
lang: {
|
|
16594
|
-
type: "string",
|
|
16595
|
-
enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
|
|
16596
|
-
description: "Filter by indexed language"
|
|
16597
|
-
},
|
|
16598
|
-
lspKind: {
|
|
16599
|
-
type: "integer",
|
|
16600
|
-
description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
|
|
16601
|
-
},
|
|
16602
|
-
file: {
|
|
16603
|
-
type: "string",
|
|
16604
|
-
description: "Filter to files matching this path substring"
|
|
16605
|
-
},
|
|
16606
|
-
limit: {
|
|
16607
|
-
type: "integer",
|
|
16608
|
-
description: "Maximum results to return (default 20, max 100)",
|
|
16609
|
-
minimum: 1,
|
|
16610
|
-
maximum: 100
|
|
16611
|
-
},
|
|
16612
|
-
preferLsp: {
|
|
16613
|
-
type: "boolean",
|
|
16614
|
-
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."
|
|
16615
|
-
}
|
|
16616
|
-
},
|
|
16617
|
-
required: ["query"]
|
|
16618
|
-
},
|
|
16619
|
-
async execute(input, ctx, execOpts) {
|
|
16620
|
-
const state = getIndexState();
|
|
16621
|
-
if (state.indexing && !state.ready) {
|
|
16622
|
-
return {
|
|
16623
|
-
results: [],
|
|
16624
|
-
total: 0,
|
|
16625
|
-
query: input.query,
|
|
16626
|
-
indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
|
|
16627
|
-
};
|
|
16628
|
-
}
|
|
16629
|
-
if (state.lastError) {
|
|
16630
|
-
const circuit = state.circuit;
|
|
16631
|
-
const retryHint = circuit.state === "open" ? `Indexing is paused (circuit open, retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s); the user can run /codebase-reindex to retry now.` : "Try /codebase-reindex.";
|
|
16632
|
-
return {
|
|
16633
|
-
results: [],
|
|
16634
|
-
total: 0,
|
|
16635
|
-
query: input.query,
|
|
16636
|
-
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
16637
|
-
};
|
|
16638
|
-
}
|
|
16639
|
-
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
|
|
16640
|
-
const { results, total } = await searchCodebaseIndex(
|
|
16641
|
-
{
|
|
16642
|
-
projectRoot: ctx.projectRoot,
|
|
16643
|
-
indexDir: codebaseIndexDirOverride(ctx),
|
|
16644
|
-
query: input.query,
|
|
16645
|
-
kind: input.kind?.toLowerCase(),
|
|
16646
|
-
lang: input.lang?.toLowerCase(),
|
|
16647
|
-
file: input.file,
|
|
16648
|
-
lspKind: input.lspKind,
|
|
16649
|
-
limit
|
|
16650
|
-
},
|
|
16651
|
-
{ signal: execOpts?.signal }
|
|
16652
|
-
);
|
|
16653
|
-
let hasPersistedIndex = state.ready || total > 0;
|
|
16654
|
-
if (!hasPersistedIndex) {
|
|
16655
|
-
try {
|
|
16656
|
-
const stats = await codebaseIndexStats(
|
|
16657
|
-
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
16658
|
-
{ signal: execOpts?.signal }
|
|
16659
|
-
);
|
|
16660
|
-
hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
|
|
16661
|
-
} catch {
|
|
16662
|
-
}
|
|
16663
|
-
}
|
|
16664
|
-
return {
|
|
16665
|
-
results,
|
|
16666
|
-
total,
|
|
16667
|
-
query: input.query,
|
|
16668
|
-
...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
|
|
16669
|
-
};
|
|
16670
|
-
}
|
|
16671
|
-
};
|
|
16672
|
-
|
|
16673
|
-
// src/codebase-index/codebase-stats-tool.ts
|
|
16674
|
-
var codebaseStatsTool = {
|
|
16675
|
-
name: "codebase-stats",
|
|
16875
|
+
// src/codebase-index/codebase-stats-tool.ts
|
|
16876
|
+
var codebaseStatsTool = {
|
|
16877
|
+
name: "codebase-stats",
|
|
16676
16878
|
category: "Project",
|
|
16677
16879
|
icon: "index",
|
|
16678
16880
|
description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
|
|
@@ -16763,9 +16965,9 @@ import * as path25 from "node:path";
|
|
|
16763
16965
|
var deadCodeScanTool = {
|
|
16764
16966
|
name: "dead-code-scan",
|
|
16765
16967
|
category: "Project",
|
|
16766
|
-
icon: "
|
|
16968
|
+
icon: "index",
|
|
16767
16969
|
description: "Scan TypeScript/JavaScript source files for exported symbols that appear unused anywhere in the project. Uses the codebase-index reference graph (import/call/type-ref edges) to compute transitive reachability from package.json entry points. Requires a built codebase-index (run `codebase-index` first if you get no results).",
|
|
16768
|
-
usageHint:
|
|
16970
|
+
usageHint: "SCANS ALL INDEXED FILES UNDER THE PROJECT ROOT:\n\n- `projectRoot` defaults to the current project root; `indexDir` overrides the resolved index location.\n- `entryPoints` is an array of file paths that AUGMENTS the auto-discovered entry points (package.json bin/main/exports/types plus conventional src/index.ts-style files) \u2014 it does not replace them.\n\nThe scan runs against the existing index; results are best-effort (dynamic imports, external consumers, and config-driven registration are invisible).",
|
|
16769
16971
|
permission: "auto",
|
|
16770
16972
|
mutating: false,
|
|
16771
16973
|
capabilities: ["fs.read"],
|
|
@@ -17528,12 +17730,14 @@ import { statSync as statSync3 } from "node:fs";
|
|
|
17528
17730
|
import * as fs22 from "node:fs/promises";
|
|
17529
17731
|
import * as path27 from "node:path";
|
|
17530
17732
|
import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
|
|
17733
|
+
import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
|
|
17531
17734
|
var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
|
|
17735
|
+
var MAX_GIT_DIFF_CHARS = 1e5;
|
|
17532
17736
|
var diffTool = {
|
|
17533
17737
|
name: "diff",
|
|
17534
17738
|
category: "Filesystem",
|
|
17535
17739
|
description: "Show file content with line numbers, staged/working-tree diffs via git, or commit/branch diffs. A safer and more structured alternative to raw `git diff` via shell.",
|
|
17536
|
-
usageHint: 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\n\n- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes).\n- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\n- `staged: true` \u2192 only show staged changes.\n- `mode`
|
|
17740
|
+
usageHint: 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\n\n- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes). Result `mode` is "dump".\n- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\n- `staged: true` \u2192 only show staged changes.\n- `mode` only affects the git-diff path: "stat" runs `git diff --stat`; "side-by-side" is not supported and falls back to unified (result `mode` reports what was produced).\n- `context` sets the unified-diff context line count on the git path (`-U<n>`); the dump path has no context notion.\n\nNOTE: For a true file-vs-file unified diff, supply `a` and `b` so the tool delegates to `git diff`. The `files`-only path is a line-numbered dump, not a diff.\n\nThis tool has important safety guards against flag injection (see previous security findings).',
|
|
17537
17741
|
permission: "auto",
|
|
17538
17742
|
mutating: false,
|
|
17539
17743
|
maxOutputBytes: 262144,
|
|
@@ -17566,11 +17770,12 @@ var diffTool = {
|
|
|
17566
17770
|
mode: {
|
|
17567
17771
|
type: "string",
|
|
17568
17772
|
enum: ["unified", "side-by-side", "stat"],
|
|
17569
|
-
description: 'Output format. "unified" is default
|
|
17773
|
+
description: 'Output format for the git-diff path. "unified" is default; "stat" shows a summary only; "side-by-side" is not supported and falls back to unified. The `files`-only dump path ignores this.'
|
|
17570
17774
|
},
|
|
17571
17775
|
context: {
|
|
17572
17776
|
type: "integer",
|
|
17573
|
-
|
|
17777
|
+
minimum: 0,
|
|
17778
|
+
description: "Number of context lines for git unified diffs (default: 3, passed as -U<n>). Ignored by the `files`-only dump path."
|
|
17574
17779
|
}
|
|
17575
17780
|
}
|
|
17576
17781
|
},
|
|
@@ -17583,16 +17788,31 @@ var diffTool = {
|
|
|
17583
17788
|
};
|
|
17584
17789
|
async function gitDiff(input, ctx, signal) {
|
|
17585
17790
|
if (input.a?.startsWith("-")) {
|
|
17586
|
-
throw new
|
|
17791
|
+
throw new ToolValidationError2({
|
|
17792
|
+
message: `diff: unsafe ref "${input.a}" \u2014 refs may not begin with '-' (flag injection)`,
|
|
17793
|
+
field: "a"
|
|
17794
|
+
});
|
|
17587
17795
|
}
|
|
17588
17796
|
if (input.b?.startsWith("-")) {
|
|
17589
|
-
throw new
|
|
17797
|
+
throw new ToolValidationError2({
|
|
17798
|
+
message: `diff: unsafe ref "${input.b}" \u2014 refs may not begin with '-' (flag injection)`,
|
|
17799
|
+
field: "b"
|
|
17800
|
+
});
|
|
17590
17801
|
}
|
|
17802
|
+
const requestedMode = input.mode ?? "unified";
|
|
17803
|
+
const statMode = requestedMode === "stat";
|
|
17804
|
+
const effectiveMode = statMode ? "stat" : "unified";
|
|
17805
|
+
const sideBySideNote = requestedMode === "side-by-side" ? "side-by-side output is not supported; a unified diff was produced instead." : void 0;
|
|
17591
17806
|
const gitDir = findGitDir(ctx.cwd);
|
|
17592
17807
|
if (!gitDir) {
|
|
17593
|
-
return { diff: "", files: [], truncated: false, mode:
|
|
17808
|
+
return { diff: "", files: [], truncated: false, mode: effectiveMode };
|
|
17594
17809
|
}
|
|
17595
17810
|
const args = ["diff", "--no-color"];
|
|
17811
|
+
if (statMode) args.push("--stat");
|
|
17812
|
+
if (!statMode && input.context !== void 0) {
|
|
17813
|
+
const contextLines = Math.max(0, Math.floor(input.context));
|
|
17814
|
+
if (Number.isFinite(contextLines)) args.push(`-U${contextLines}`);
|
|
17815
|
+
}
|
|
17596
17816
|
if (input.staged) args.push("--staged");
|
|
17597
17817
|
if (input.a) args.push(input.a);
|
|
17598
17818
|
if (input.b) args.push(input.b);
|
|
@@ -17601,19 +17821,30 @@ async function gitDiff(input, ctx, signal) {
|
|
|
17601
17821
|
args.push("--", ...files.map((f) => f.trim()));
|
|
17602
17822
|
}
|
|
17603
17823
|
const result = await runGit(args, gitDir, signal);
|
|
17824
|
+
let diff = result.stdout;
|
|
17825
|
+
let truncated = false;
|
|
17826
|
+
if (diff.length > MAX_GIT_DIFF_CHARS) {
|
|
17827
|
+
let clipped = diff.slice(0, MAX_GIT_DIFF_CHARS);
|
|
17828
|
+
const nl = clipped.lastIndexOf("\n");
|
|
17829
|
+
if (nl > 0) clipped = clipped.slice(0, nl);
|
|
17830
|
+
diff = `${clipped}
|
|
17831
|
+
\u2026[git diff truncated: ${result.stdout.length - clipped.length} of ${result.stdout.length} characters omitted]`;
|
|
17832
|
+
truncated = true;
|
|
17833
|
+
}
|
|
17604
17834
|
return {
|
|
17605
|
-
diff
|
|
17835
|
+
diff,
|
|
17606
17836
|
files: [],
|
|
17607
|
-
truncated
|
|
17608
|
-
mode:
|
|
17837
|
+
truncated,
|
|
17838
|
+
mode: effectiveMode,
|
|
17839
|
+
note: sideBySideNote
|
|
17609
17840
|
};
|
|
17610
17841
|
}
|
|
17611
17842
|
function findGitDir(cwd) {
|
|
17612
17843
|
let dir = cwd;
|
|
17613
17844
|
for (let i = 0; i < 20; i++) {
|
|
17614
17845
|
try {
|
|
17615
|
-
const
|
|
17616
|
-
if (
|
|
17846
|
+
const stat19 = statSync3(path27.join(dir, ".git"));
|
|
17847
|
+
if (stat19.isDirectory()) return dir;
|
|
17617
17848
|
} catch {
|
|
17618
17849
|
}
|
|
17619
17850
|
const parent = path27.dirname(dir);
|
|
@@ -17623,7 +17854,7 @@ function findGitDir(cwd) {
|
|
|
17623
17854
|
return null;
|
|
17624
17855
|
}
|
|
17625
17856
|
function runGit(args, cwd, signal) {
|
|
17626
|
-
return new Promise((
|
|
17857
|
+
return new Promise((resolve17) => {
|
|
17627
17858
|
let stdout = "";
|
|
17628
17859
|
let stderr = "";
|
|
17629
17860
|
const child = spawn7("git", args, {
|
|
@@ -17639,8 +17870,8 @@ function runGit(args, cwd, signal) {
|
|
|
17639
17870
|
child.stderr?.on("data", (c) => {
|
|
17640
17871
|
stderr += c.toString();
|
|
17641
17872
|
});
|
|
17642
|
-
child.on("close", (code) =>
|
|
17643
|
-
child.on("error", (e) =>
|
|
17873
|
+
child.on("close", (code) => resolve17({ stdout, stderr, exitCode: code ?? 0 }));
|
|
17874
|
+
child.on("error", (e) => resolve17({ stdout: "", stderr: e.message, exitCode: 1 }));
|
|
17644
17875
|
});
|
|
17645
17876
|
}
|
|
17646
17877
|
async function fileDiff(input, ctx, _signal) {
|
|
@@ -17651,19 +17882,19 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
17651
17882
|
diff: "No files specified",
|
|
17652
17883
|
files: [],
|
|
17653
17884
|
truncated: false,
|
|
17654
|
-
mode:
|
|
17885
|
+
mode: "dump"
|
|
17655
17886
|
};
|
|
17656
17887
|
}
|
|
17657
17888
|
const results = [];
|
|
17658
17889
|
let truncated = false;
|
|
17659
17890
|
for (const file of files) {
|
|
17660
|
-
const absPath =
|
|
17661
|
-
const
|
|
17662
|
-
if (!
|
|
17663
|
-
if (
|
|
17891
|
+
const absPath = await safeResolveReal(file, ctx);
|
|
17892
|
+
const stat19 = await fs22.stat(absPath).catch(() => null);
|
|
17893
|
+
if (!stat19?.isFile()) continue;
|
|
17894
|
+
if (stat19.size > MAX_FILE_DUMP_BYTES) {
|
|
17664
17895
|
truncated = true;
|
|
17665
17896
|
results.push(
|
|
17666
|
-
`--- ${file} (skipped: ${
|
|
17897
|
+
`--- ${file} (skipped: ${stat19.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
|
|
17667
17898
|
);
|
|
17668
17899
|
continue;
|
|
17669
17900
|
}
|
|
@@ -17675,7 +17906,10 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
17675
17906
|
diff: results.join("\n\n"),
|
|
17676
17907
|
files,
|
|
17677
17908
|
truncated,
|
|
17678
|
-
mode:
|
|
17909
|
+
// Honest mode: this path always produces a line-numbered dump — it never
|
|
17910
|
+
// honors `mode`, so it must not echo the requested value back.
|
|
17911
|
+
mode: "dump",
|
|
17912
|
+
note: input.mode !== void 0 ? "The `files`-only path is a line-numbered dump; `mode` only affects the git-diff path (`a`/`b`)." : void 0
|
|
17679
17913
|
};
|
|
17680
17914
|
}
|
|
17681
17915
|
function formatWithLineNumbers(file, lines) {
|
|
@@ -17688,11 +17922,12 @@ ${numbered}`;
|
|
|
17688
17922
|
// src/document.ts
|
|
17689
17923
|
init_util();
|
|
17690
17924
|
import * as fs23 from "node:fs/promises";
|
|
17925
|
+
import * as path28 from "node:path";
|
|
17691
17926
|
var documentTool = {
|
|
17692
17927
|
name: "document",
|
|
17693
17928
|
category: "Project",
|
|
17694
|
-
description: "DEPRECATED \u2014
|
|
17695
|
-
usageHint: "Deprecated:
|
|
17929
|
+
description: "DEPRECATED \u2014 read-only preview stub that lists undocumented symbols as `skipped` candidates. It never writes files and does not generate real docstrings. If the auto-doc plugin is enabled, use its `auto_doc` tool (with `dry_run: true` to preview) instead.",
|
|
17930
|
+
usageHint: "Deprecated: this tool only lists undocumented symbols with placeholder comments \u2014 it does not generate real JSDoc/TSDoc and writes nothing. When the auto-doc plugin is enabled, prefer its `auto_doc` tool (`dry_run: true` for previewing, without it for writing).",
|
|
17696
17931
|
permission: "auto",
|
|
17697
17932
|
mutating: false,
|
|
17698
17933
|
timeoutMs: 3e4,
|
|
@@ -17728,7 +17963,11 @@ var documentTool = {
|
|
|
17728
17963
|
const results = [];
|
|
17729
17964
|
let filesProcessed = 0;
|
|
17730
17965
|
let itemsDocumented = 0;
|
|
17731
|
-
const fileList = input.files ? await resolveFiles(
|
|
17966
|
+
const fileList = input.files ? await resolveFiles(
|
|
17967
|
+
Array.isArray(input.files) ? input.files.join(",") : input.files,
|
|
17968
|
+
cwd,
|
|
17969
|
+
ctx
|
|
17970
|
+
) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
17732
17971
|
for (const absPath of fileList) {
|
|
17733
17972
|
try {
|
|
17734
17973
|
const content = await fs23.readFile(absPath, "utf8");
|
|
@@ -17761,14 +18000,21 @@ var documentTool = {
|
|
|
17761
18000
|
};
|
|
17762
18001
|
}
|
|
17763
18002
|
};
|
|
17764
|
-
async function resolveFiles(filesInput, cwd) {
|
|
17765
|
-
const files =
|
|
18003
|
+
async function resolveFiles(filesInput, cwd, ctx) {
|
|
18004
|
+
const files = filesInput.split(",");
|
|
17766
18005
|
const resolved = [];
|
|
17767
18006
|
for (const f of files) {
|
|
17768
|
-
const
|
|
18007
|
+
const entry = f.trim();
|
|
18008
|
+
if (!entry) continue;
|
|
18009
|
+
let absPath;
|
|
17769
18010
|
try {
|
|
17770
|
-
|
|
17771
|
-
|
|
18011
|
+
absPath = ensureInsideRoot(path28.resolve(cwd, entry), ctx);
|
|
18012
|
+
} catch {
|
|
18013
|
+
continue;
|
|
18014
|
+
}
|
|
18015
|
+
try {
|
|
18016
|
+
const stat19 = await fs23.stat(absPath);
|
|
18017
|
+
if (stat19.isFile()) resolved.push(absPath);
|
|
17772
18018
|
} catch {
|
|
17773
18019
|
}
|
|
17774
18020
|
}
|
|
@@ -17838,7 +18084,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
|
|
|
17838
18084
|
// src/e2e.ts
|
|
17839
18085
|
init_util();
|
|
17840
18086
|
import { open, readdir as readdir7 } from "node:fs/promises";
|
|
17841
|
-
import * as
|
|
18087
|
+
import * as path29 from "node:path";
|
|
17842
18088
|
async function readBoundedText(filePath, maxBytes) {
|
|
17843
18089
|
let handle;
|
|
17844
18090
|
try {
|
|
@@ -17888,8 +18134,8 @@ var MAX_PACKAGE_BYTES = 512 * 1024;
|
|
|
17888
18134
|
var MAX_CONFIG_BYTES = 512 * 1024;
|
|
17889
18135
|
var MAX_SPEC_SAMPLES = 20;
|
|
17890
18136
|
function relativePath(root, target) {
|
|
17891
|
-
const value =
|
|
17892
|
-
return value.split(
|
|
18137
|
+
const value = path29.relative(root, target) || ".";
|
|
18138
|
+
return value.split(path29.sep).join("/");
|
|
17893
18139
|
}
|
|
17894
18140
|
function escapeRegExp(value) {
|
|
17895
18141
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -17961,7 +18207,7 @@ async function scanWorkspace(root, maxDepth, signal) {
|
|
|
17961
18207
|
}
|
|
17962
18208
|
for (const entry of entries) {
|
|
17963
18209
|
signal.throwIfAborted();
|
|
17964
|
-
const absolutePath =
|
|
18210
|
+
const absolutePath = path29.join(current.directory, entry.name);
|
|
17965
18211
|
if (entry.isFile()) {
|
|
17966
18212
|
if (entry.name === "package.json") result.packageFiles.push(absolutePath);
|
|
17967
18213
|
const framework = CONFIG_NAMES.get(entry.name);
|
|
@@ -18022,9 +18268,9 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
|
|
|
18022
18268
|
if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
|
|
18023
18269
|
if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
|
|
18024
18270
|
if (directory === scanRoot) break;
|
|
18025
|
-
const parent =
|
|
18026
|
-
const relativeParent =
|
|
18027
|
-
if (parent === directory || relativeParent.startsWith("..") ||
|
|
18271
|
+
const parent = path29.dirname(directory);
|
|
18272
|
+
const relativeParent = path29.relative(scanRoot, parent);
|
|
18273
|
+
if (parent === directory || relativeParent.startsWith("..") || path29.isAbsolute(relativeParent)) {
|
|
18028
18274
|
break;
|
|
18029
18275
|
}
|
|
18030
18276
|
directory = parent;
|
|
@@ -18069,7 +18315,7 @@ function isSpec(framework, filename) {
|
|
|
18069
18315
|
return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
|
|
18070
18316
|
}
|
|
18071
18317
|
async function collectSpecs(root, framework, testDirectory, signal) {
|
|
18072
|
-
const roots = testDirectory ? [
|
|
18318
|
+
const roots = testDirectory ? [path29.resolve(root, testDirectory)] : framework === "cypress" ? [path29.join(root, "cypress", "e2e"), path29.join(root, "cypress", "integration")] : [path29.join(root, "tests")];
|
|
18073
18319
|
const samples = [];
|
|
18074
18320
|
let count = 0;
|
|
18075
18321
|
let scanned = 0;
|
|
@@ -18088,7 +18334,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
|
|
|
18088
18334
|
}
|
|
18089
18335
|
for (const entry of entries) {
|
|
18090
18336
|
signal.throwIfAborted();
|
|
18091
|
-
const target =
|
|
18337
|
+
const target = path29.join(directory, entry.name);
|
|
18092
18338
|
if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
|
|
18093
18339
|
else if (entry.isFile() && isSpec(framework, entry.name)) {
|
|
18094
18340
|
count += 1;
|
|
@@ -18108,7 +18354,7 @@ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
|
|
|
18108
18354
|
const found = packagesByDirectory.get(directory);
|
|
18109
18355
|
if (found) return found;
|
|
18110
18356
|
if (directory === scanRoot) return void 0;
|
|
18111
|
-
const parent =
|
|
18357
|
+
const parent = path29.dirname(directory);
|
|
18112
18358
|
if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
|
|
18113
18359
|
directory = parent;
|
|
18114
18360
|
}
|
|
@@ -18119,13 +18365,13 @@ async function discoverE2EProjects(root, options) {
|
|
|
18119
18365
|
const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
|
|
18120
18366
|
(info) => Boolean(info)
|
|
18121
18367
|
);
|
|
18122
|
-
const packagesByDirectory = new Map(packages.map((info) => [
|
|
18368
|
+
const packagesByDirectory = new Map(packages.map((info) => [path29.dirname(info.path), info]));
|
|
18123
18369
|
const candidates = /* @__PURE__ */ new Map();
|
|
18124
18370
|
for (const config of scan.configs) {
|
|
18125
18371
|
if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
|
|
18126
18372
|
continue;
|
|
18127
18373
|
}
|
|
18128
|
-
const projectRoot =
|
|
18374
|
+
const projectRoot = path29.dirname(config.absolutePath);
|
|
18129
18375
|
candidates.set(`${config.framework}:${projectRoot}`, {
|
|
18130
18376
|
framework: config.framework,
|
|
18131
18377
|
root: projectRoot,
|
|
@@ -18133,7 +18379,7 @@ async function discoverE2EProjects(root, options) {
|
|
|
18133
18379
|
});
|
|
18134
18380
|
}
|
|
18135
18381
|
for (const info of packages) {
|
|
18136
|
-
const projectRoot =
|
|
18382
|
+
const projectRoot = path29.dirname(info.path);
|
|
18137
18383
|
for (const framework of frameworkFromPackage(info)) {
|
|
18138
18384
|
if (options.framework && options.framework !== "all" && framework !== options.framework)
|
|
18139
18385
|
continue;
|
|
@@ -18149,10 +18395,10 @@ async function discoverE2EProjects(root, options) {
|
|
|
18149
18395
|
const scripts = matchingScripts(info, candidate.framework);
|
|
18150
18396
|
const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
|
|
18151
18397
|
const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
|
|
18152
|
-
const resolvedTestDirectory = testDirectory ?
|
|
18153
|
-
const relativeTestDirectory = resolvedTestDirectory ?
|
|
18398
|
+
const resolvedTestDirectory = testDirectory ? path29.resolve(candidate.root, testDirectory) : void 0;
|
|
18399
|
+
const relativeTestDirectory = resolvedTestDirectory ? path29.relative(root, resolvedTestDirectory) : void 0;
|
|
18154
18400
|
const unsafeTestDirectory = Boolean(
|
|
18155
|
-
relativeTestDirectory && (relativeTestDirectory.startsWith("..") ||
|
|
18401
|
+
relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path29.isAbsolute(relativeTestDirectory))
|
|
18156
18402
|
);
|
|
18157
18403
|
const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
|
|
18158
18404
|
const warnings = [];
|
|
@@ -18260,7 +18506,7 @@ import {
|
|
|
18260
18506
|
toStyle,
|
|
18261
18507
|
unifiedDiff
|
|
18262
18508
|
} from "@wrongstack/core/utils";
|
|
18263
|
-
import { ToolValidationError } from "@wrongstack/core/types";
|
|
18509
|
+
import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
|
|
18264
18510
|
|
|
18265
18511
|
// src/_edit-match.ts
|
|
18266
18512
|
var TIER_LABEL = {
|
|
@@ -18488,7 +18734,7 @@ function prefixSimilarity(a, b) {
|
|
|
18488
18734
|
}
|
|
18489
18735
|
|
|
18490
18736
|
// src/_syntax-check.ts
|
|
18491
|
-
import * as
|
|
18737
|
+
import * as path30 from "node:path";
|
|
18492
18738
|
var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
18493
18739
|
var MAX_CHECK_CHARS = 15e5;
|
|
18494
18740
|
var MAX_ERRORS = 5;
|
|
@@ -18512,15 +18758,15 @@ async function checkSyntax(filePath, content, previousContent) {
|
|
|
18512
18758
|
return { errors, preExisting };
|
|
18513
18759
|
}
|
|
18514
18760
|
function isJsoncFile(filePath) {
|
|
18515
|
-
const base =
|
|
18761
|
+
const base = path30.basename(filePath).toLowerCase();
|
|
18516
18762
|
if (base.endsWith(".jsonc")) return true;
|
|
18517
18763
|
if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
|
|
18518
|
-
const dir =
|
|
18764
|
+
const dir = path30.basename(path30.dirname(filePath)).toLowerCase();
|
|
18519
18765
|
return dir === ".vscode";
|
|
18520
18766
|
}
|
|
18521
18767
|
async function parseErrors(filePath, content) {
|
|
18522
18768
|
if (content.length > MAX_CHECK_CHARS) return void 0;
|
|
18523
|
-
const ext =
|
|
18769
|
+
const ext = path30.extname(filePath).toLowerCase();
|
|
18524
18770
|
if (ext === ".json" || ext === ".jsonc") {
|
|
18525
18771
|
try {
|
|
18526
18772
|
JSON.parse(content);
|
|
@@ -18547,7 +18793,7 @@ async function parseErrors(filePath, content) {
|
|
|
18547
18793
|
ts2.ScriptKind.JSX
|
|
18548
18794
|
);
|
|
18549
18795
|
const sourceFile = ts2.createSourceFile(
|
|
18550
|
-
|
|
18796
|
+
path30.basename(filePath),
|
|
18551
18797
|
content,
|
|
18552
18798
|
ts2.ScriptTarget.Latest,
|
|
18553
18799
|
/* setParentNodes */
|
|
@@ -18574,6 +18820,7 @@ function formatDiag(ts2, diag, content, sourceFile) {
|
|
|
18574
18820
|
|
|
18575
18821
|
// src/edit.ts
|
|
18576
18822
|
init_util();
|
|
18823
|
+
var MAX_DIFF_BYTES = 262144;
|
|
18577
18824
|
var editTool = {
|
|
18578
18825
|
name: "edit",
|
|
18579
18826
|
category: "Filesystem",
|
|
@@ -18584,46 +18831,62 @@ var editTool = {
|
|
|
18584
18831
|
useInstead: ["write", "patch"]
|
|
18585
18832
|
},
|
|
18586
18833
|
permission: "confirm",
|
|
18834
|
+
// WS-046: gives permission decisions something to key on — the file being
|
|
18835
|
+
// edited, so trust rules can scope by path.
|
|
18836
|
+
subjectKey: "path",
|
|
18587
18837
|
mutating: true,
|
|
18588
18838
|
capabilities: ["fs.write"],
|
|
18589
18839
|
icon: "edit",
|
|
18590
18840
|
timeoutMs: 5e3,
|
|
18841
|
+
maxOutputBytes: 262144,
|
|
18591
18842
|
inputSchema: {
|
|
18592
18843
|
type: "object",
|
|
18593
18844
|
properties: {
|
|
18594
|
-
path: {
|
|
18595
|
-
|
|
18596
|
-
|
|
18597
|
-
|
|
18845
|
+
path: {
|
|
18846
|
+
type: "string",
|
|
18847
|
+
description: "Path to the file to edit \u2014 relative to the project root, or absolute inside it."
|
|
18848
|
+
},
|
|
18849
|
+
old_string: {
|
|
18850
|
+
type: "string",
|
|
18851
|
+
description: "The exact text to replace, including whitespace and indentation. Must be unique in the file unless `replace_all` is set \u2014 add surrounding lines to disambiguate."
|
|
18852
|
+
},
|
|
18853
|
+
new_string: {
|
|
18854
|
+
type: "string",
|
|
18855
|
+
description: "The exact replacement text (may be empty to delete `old_string`)."
|
|
18856
|
+
},
|
|
18857
|
+
replace_all: {
|
|
18858
|
+
type: "boolean",
|
|
18859
|
+
description: "Replace every occurrence instead of requiring a unique match. Only allowed when `old_string` matches exactly (or up to trailing whitespace) \u2014 fuzzy matches stay single-target."
|
|
18860
|
+
}
|
|
18598
18861
|
},
|
|
18599
18862
|
required: ["path", "old_string", "new_string"]
|
|
18600
18863
|
},
|
|
18601
18864
|
async execute(input, ctx, opts) {
|
|
18602
18865
|
if (!input?.path) {
|
|
18603
|
-
throw new
|
|
18866
|
+
throw new ToolValidationError3({ message: "edit: path is required", field: "path" });
|
|
18604
18867
|
}
|
|
18605
18868
|
if (input.old_string === void 0) {
|
|
18606
|
-
throw new
|
|
18869
|
+
throw new ToolValidationError3({
|
|
18607
18870
|
message: "edit: old_string is required",
|
|
18608
18871
|
field: "old_string"
|
|
18609
18872
|
});
|
|
18610
18873
|
}
|
|
18611
18874
|
if (input.new_string === void 0) {
|
|
18612
|
-
throw new
|
|
18875
|
+
throw new ToolValidationError3({
|
|
18613
18876
|
message: "edit: new_string is required",
|
|
18614
18877
|
field: "new_string"
|
|
18615
18878
|
});
|
|
18616
18879
|
}
|
|
18617
18880
|
if (input.old_string === "") {
|
|
18618
|
-
throw new
|
|
18881
|
+
throw new ToolValidationError3({
|
|
18619
18882
|
message: "edit: old_string cannot be empty",
|
|
18620
18883
|
field: "old_string"
|
|
18621
18884
|
});
|
|
18622
18885
|
}
|
|
18623
18886
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
18624
|
-
const
|
|
18887
|
+
const stat19 = await fs24.stat(absPath).catch((err) => {
|
|
18625
18888
|
if (err.code === "ENOENT") {
|
|
18626
|
-
throw new
|
|
18889
|
+
throw new ToolValidationError3({
|
|
18627
18890
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
18628
18891
|
field: "path",
|
|
18629
18892
|
context: { exists: false }
|
|
@@ -18631,8 +18894,8 @@ var editTool = {
|
|
|
18631
18894
|
}
|
|
18632
18895
|
throw err;
|
|
18633
18896
|
});
|
|
18634
|
-
if (!
|
|
18635
|
-
throw new
|
|
18897
|
+
if (!stat19.isFile()) {
|
|
18898
|
+
throw new ToolValidationError3({
|
|
18636
18899
|
message: `edit: "${input.path}" is not a regular file`,
|
|
18637
18900
|
field: "path"
|
|
18638
18901
|
});
|
|
@@ -18645,7 +18908,7 @@ var editTool = {
|
|
|
18645
18908
|
const lastReadHash = ctx.lastReadHash?.(absPath);
|
|
18646
18909
|
if (lastReadHash !== void 0) {
|
|
18647
18910
|
if (lastReadHash !== originalHash) {
|
|
18648
|
-
throw new
|
|
18911
|
+
throw new ToolValidationError3({
|
|
18649
18912
|
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
18650
18913
|
field: "path",
|
|
18651
18914
|
context: { reason: "external_modification" }
|
|
@@ -18654,15 +18917,15 @@ var editTool = {
|
|
|
18654
18917
|
} else {
|
|
18655
18918
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
18656
18919
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
18657
|
-
throw new
|
|
18920
|
+
throw new ToolValidationError3({
|
|
18658
18921
|
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
18659
18922
|
field: "path",
|
|
18660
18923
|
context: { reason: "external_modification" }
|
|
18661
18924
|
});
|
|
18662
18925
|
}
|
|
18663
18926
|
}
|
|
18664
|
-
if (autoRead && updated.mtimeMs >
|
|
18665
|
-
throw new
|
|
18927
|
+
if (autoRead && updated.mtimeMs > stat19.mtimeMs + mtimeTolerance) {
|
|
18928
|
+
throw new ToolValidationError3({
|
|
18666
18929
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
18667
18930
|
field: "path",
|
|
18668
18931
|
context: { reason: "auto_read_race" }
|
|
@@ -18674,6 +18937,9 @@ var editTool = {
|
|
|
18674
18937
|
const oldLf = normalizeToLf(input.old_string);
|
|
18675
18938
|
const newLf = normalizeToLf(input.new_string);
|
|
18676
18939
|
if (oldLf === newLf) {
|
|
18940
|
+
if (!fileLf.includes(oldLf)) {
|
|
18941
|
+
throw noMatchError(input.path, fileLf, oldLf);
|
|
18942
|
+
}
|
|
18677
18943
|
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
|
|
18678
18944
|
return {
|
|
18679
18945
|
path: absPath,
|
|
@@ -18687,26 +18953,20 @@ var editTool = {
|
|
|
18687
18953
|
const ladder = findLadderMatches(fileLf, oldLf);
|
|
18688
18954
|
if (!ladder) {
|
|
18689
18955
|
opts?.signal?.throwIfAborted();
|
|
18690
|
-
|
|
18691
|
-
throw new ToolValidationError({
|
|
18692
|
-
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
|
|
18693
|
-
${hint.snippet}
|
|
18694
|
-
Compare this against your old_string and retry with the file's actual text.` : ""}`,
|
|
18695
|
-
field: "old_string"
|
|
18696
|
-
});
|
|
18956
|
+
throw noMatchError(input.path, fileLf, oldLf);
|
|
18697
18957
|
}
|
|
18698
18958
|
const { tier, matches } = ladder;
|
|
18699
18959
|
const count = matches.length;
|
|
18700
18960
|
if (ladder.ambiguous) {
|
|
18701
18961
|
const lines = matches.map((m) => m.startLine);
|
|
18702
|
-
throw new
|
|
18962
|
+
throw new ToolValidationError3({
|
|
18703
18963
|
message: `edit: old_string only matched fuzzily and ${count} candidate blocks scored too close to distinguish (lines: ${lines.join(", ")}) in "${input.path}". Re-read the file and use the exact text of the intended block.`,
|
|
18704
18964
|
field: "old_string",
|
|
18705
18965
|
context: { occurrences: count, matchTier: tier }
|
|
18706
18966
|
});
|
|
18707
18967
|
}
|
|
18708
18968
|
if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
|
|
18709
|
-
throw new
|
|
18969
|
+
throw new ToolValidationError3({
|
|
18710
18970
|
message: `edit: old_string only matched via ${TIER_LABEL[tier]} in "${input.path}", but replace_all requires an exact (or trailing-whitespace) match. Re-read the file and use its exact text.`,
|
|
18711
18971
|
field: "old_string",
|
|
18712
18972
|
context: { matchTier: tier }
|
|
@@ -18714,7 +18974,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
18714
18974
|
}
|
|
18715
18975
|
if (count > 1 && !input.replace_all) {
|
|
18716
18976
|
const lines = matches.map((m) => m.startLine);
|
|
18717
|
-
throw new
|
|
18977
|
+
throw new ToolValidationError3({
|
|
18718
18978
|
message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")})${tier === "exact" ? "" : ` via ${TIER_LABEL[tier]}`}. Add more context to make it unique, or set replace_all: true.`,
|
|
18719
18979
|
field: "old_string",
|
|
18720
18980
|
context: { occurrences: count, matchTier: tier }
|
|
@@ -18755,16 +19015,20 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
18755
19015
|
after: newFile
|
|
18756
19016
|
});
|
|
18757
19017
|
opts?.signal?.throwIfAborted();
|
|
18758
|
-
const diff =
|
|
18759
|
-
|
|
18760
|
-
|
|
18761
|
-
|
|
19018
|
+
const { text: diff, truncated: diffTruncated } = truncateDiffPayload(
|
|
19019
|
+
unifiedDiff(original, newFile, {
|
|
19020
|
+
fromFile: input.path,
|
|
19021
|
+
toFile: input.path
|
|
19022
|
+
}),
|
|
19023
|
+
MAX_DIFF_BYTES
|
|
19024
|
+
);
|
|
19025
|
+
const diffNote = diffTruncated ? "Diff truncated to the 256 KiB output budget \u2014 the full edit is on disk." : void 0;
|
|
18762
19026
|
const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
|
|
18763
19027
|
let syntaxNote;
|
|
18764
19028
|
if (syntax && syntax.errors.length > 0) {
|
|
18765
19029
|
syntaxNote = syntax.preExisting ? `Syntax check: the file still has parse errors (they pre-date this edit) \u2014 see syntax_errors.` : `Syntax check: this edit introduced ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`;
|
|
18766
19030
|
}
|
|
18767
|
-
const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
|
|
19031
|
+
const notes = [autoReadNote, tierNote, diffNote, syntaxNote].filter(Boolean);
|
|
18768
19032
|
return {
|
|
18769
19033
|
path: absPath,
|
|
18770
19034
|
replacements: input.replace_all ? count : 1,
|
|
@@ -18775,6 +19039,15 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
18775
19039
|
};
|
|
18776
19040
|
}
|
|
18777
19041
|
};
|
|
19042
|
+
function noMatchError(inputPath, fileLf, oldLf) {
|
|
19043
|
+
const hint = nearestMatchHint(fileLf, oldLf);
|
|
19044
|
+
return new ToolValidationError3({
|
|
19045
|
+
message: `edit: no match for old_string in "${inputPath}".${hint ? ` Nearest match near line ${hint.line}:
|
|
19046
|
+
${hint.snippet}
|
|
19047
|
+
Compare this against your old_string and retry with the file's actual text.` : ""}`,
|
|
19048
|
+
field: "old_string"
|
|
19049
|
+
});
|
|
19050
|
+
}
|
|
18778
19051
|
|
|
18779
19052
|
// src/exec.ts
|
|
18780
19053
|
import { spawn as spawn8 } from "node:child_process";
|
|
@@ -18783,7 +19056,7 @@ import {
|
|
|
18783
19056
|
emitProcessOutput as emitProcessOutput3,
|
|
18784
19057
|
emitProcessStarted as emitProcessStarted3
|
|
18785
19058
|
} from "@wrongstack/core/observability";
|
|
18786
|
-
import { toErrorMessage as
|
|
19059
|
+
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils/error";
|
|
18787
19060
|
|
|
18788
19061
|
// src/_danger-detect.ts
|
|
18789
19062
|
var argHas = (args, value) => args.includes(value);
|
|
@@ -19115,7 +19388,7 @@ init_win32_resolve();
|
|
|
19115
19388
|
|
|
19116
19389
|
// src/exec-kill-guard.ts
|
|
19117
19390
|
import * as os8 from "node:os";
|
|
19118
|
-
import * as
|
|
19391
|
+
import * as path31 from "node:path";
|
|
19119
19392
|
var isWin3 = os8.platform() === "win32";
|
|
19120
19393
|
async function checkExecKillCommand(cmd, args) {
|
|
19121
19394
|
if (!cmd) return { blocked: false };
|
|
@@ -19328,7 +19601,7 @@ async function checkKillTarget(target) {
|
|
|
19328
19601
|
reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
|
|
19329
19602
|
};
|
|
19330
19603
|
}
|
|
19331
|
-
const currentImage =
|
|
19604
|
+
const currentImage = path31.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
|
|
19332
19605
|
const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
|
|
19333
19606
|
if (targetsNodeRuntime && currentImage === "node") {
|
|
19334
19607
|
return {
|
|
@@ -19950,6 +20223,7 @@ function isExecCommandAllowed(cmd) {
|
|
|
19950
20223
|
var MAX_ARGS = 20;
|
|
19951
20224
|
var MAX_OUTPUT2 = 2e5;
|
|
19952
20225
|
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
20226
|
+
var MAX_TIMEOUT_MS = 6e5;
|
|
19953
20227
|
var BLOCKED_ARG_PATTERNS = {
|
|
19954
20228
|
python: [],
|
|
19955
20229
|
// git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
|
|
@@ -20075,8 +20349,8 @@ var SAFE_DANGER = { level: "safe", reasons: [] };
|
|
|
20075
20349
|
var execTool = {
|
|
20076
20350
|
name: "exec",
|
|
20077
20351
|
category: "Shell",
|
|
20078
|
-
description: "Execute a
|
|
20079
|
-
usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be in the allowlist. Defaults cover JS (node/npm/pnpm/yarn/bun/deno/tsc/vitest/eslint/biome), Go (`go build`/`go test`), Rust (cargo), Python (python/pip), Ruby (gem/bundle), JVM (java/mvn/gradle), .NET (dotnet), native (make/cmake), and git. Users can extend it via `tools.exec.allow` in config.\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- If a command is not allowlisted, the error explains how to add it; for one-off arbitrary commands, fall back to `bash` (with strong justification).\
|
|
20352
|
+
description: "Execute a command from a **curated command roster** with argument validation and confirm gating. This is the **preferred** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It is NOT a sandbox \u2014 several rostered commands (node, python, powershell, \u2026) can run arbitrary code \u2014 so prefer least-privilege commands.",
|
|
20353
|
+
usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be in the allowlist. Defaults cover JS (node/npm/pnpm/yarn/bun/deno/tsc/vitest/eslint/biome), Go (`go build`/`go test`), Rust (cargo), Python (python/pip), Ruby (gem/bundle), JVM (java/mvn/gradle), .NET (dotnet), native (make/cmake), and git. Users can extend it via `tools.exec.allow` in config.\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- If a command is not allowlisted, the error explains how to add it; for one-off arbitrary commands, fall back to `bash` (with strong justification).\nThe curated roster + confirm gating narrows the surface compared to full shell access, but this is not a sandbox \u2014 prefer least-privilege commands.",
|
|
20080
20354
|
selection: {
|
|
20081
20355
|
doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
|
|
20082
20356
|
useInstead: ["bash"]
|
|
@@ -20092,7 +20366,13 @@ var execTool = {
|
|
|
20092
20366
|
subjectKey: "command",
|
|
20093
20367
|
mutating: true,
|
|
20094
20368
|
riskTier: "standard",
|
|
20095
|
-
|
|
20369
|
+
// Executor-level abort ceiling. Must sit ABOVE the per-call timeout ceiling
|
|
20370
|
+
// (MAX_TIMEOUT_MS): the tool's own timer resolves with exit 124 + registry
|
|
20371
|
+
// tree-kill; the executor's AbortSignal.timeout is a blunt abort that would
|
|
20372
|
+
// otherwise fire first and discard the structured timeout result. The 10s
|
|
20373
|
+
// margin covers the kill/teardown window. (The executor additionally clamps
|
|
20374
|
+
// to config `tools.maxToolTimeoutMs`.)
|
|
20375
|
+
timeoutMs: MAX_TIMEOUT_MS + 1e4,
|
|
20096
20376
|
capabilities: ["shell.restricted"],
|
|
20097
20377
|
icon: "terminal",
|
|
20098
20378
|
inputSchema: {
|
|
@@ -20113,7 +20393,7 @@ var execTool = {
|
|
|
20113
20393
|
},
|
|
20114
20394
|
timeout: {
|
|
20115
20395
|
type: "integer",
|
|
20116
|
-
description: "Per-command timeout in milliseconds."
|
|
20396
|
+
description: "Per-command timeout in milliseconds (default 30000, max 600000)."
|
|
20117
20397
|
}
|
|
20118
20398
|
},
|
|
20119
20399
|
required: ["command"]
|
|
@@ -20157,7 +20437,7 @@ var execTool = {
|
|
|
20157
20437
|
};
|
|
20158
20438
|
}
|
|
20159
20439
|
const args = (input.args ?? []).slice(0, MAX_ARGS);
|
|
20160
|
-
const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS3,
|
|
20440
|
+
const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS3, MAX_TIMEOUT_MS));
|
|
20161
20441
|
const danger = detectDanger(cmd, args, dangerBypass);
|
|
20162
20442
|
const killCheck = await checkExecKillCommand(cmd, args);
|
|
20163
20443
|
if (killCheck.blocked) {
|
|
@@ -20185,15 +20465,16 @@ var execTool = {
|
|
|
20185
20465
|
danger
|
|
20186
20466
|
};
|
|
20187
20467
|
}
|
|
20468
|
+
const defaultCwd = ctx.workingDir ?? ctx.cwd;
|
|
20188
20469
|
let cwd;
|
|
20189
20470
|
try {
|
|
20190
|
-
cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(
|
|
20471
|
+
cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(defaultCwd, ctx);
|
|
20191
20472
|
} catch {
|
|
20192
20473
|
return {
|
|
20193
20474
|
command: cmd,
|
|
20194
20475
|
args,
|
|
20195
20476
|
stdout: "",
|
|
20196
|
-
stderr: `cwd "${input.cwd ??
|
|
20477
|
+
stderr: `cwd "${input.cwd ?? defaultCwd}" resolves outside project root`,
|
|
20197
20478
|
exitCode: 1,
|
|
20198
20479
|
truncated: false,
|
|
20199
20480
|
allowed: false,
|
|
@@ -20205,7 +20486,7 @@ var execTool = {
|
|
|
20205
20486
|
}
|
|
20206
20487
|
};
|
|
20207
20488
|
function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
20208
|
-
return new Promise((
|
|
20489
|
+
return new Promise((resolve17) => {
|
|
20209
20490
|
let stdout = "";
|
|
20210
20491
|
let stderr = "";
|
|
20211
20492
|
let killed = false;
|
|
@@ -20213,7 +20494,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
20213
20494
|
const finish = (result) => {
|
|
20214
20495
|
if (resolvedOnce.value) return;
|
|
20215
20496
|
resolvedOnce.value = true;
|
|
20216
|
-
|
|
20497
|
+
resolve17(result);
|
|
20217
20498
|
};
|
|
20218
20499
|
const startedAt = Date.now();
|
|
20219
20500
|
let stdoutBytes = 0;
|
|
@@ -20265,7 +20546,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
20265
20546
|
command: cmd,
|
|
20266
20547
|
args,
|
|
20267
20548
|
stdout: "",
|
|
20268
|
-
stderr: `spawn failed: ${
|
|
20549
|
+
stderr: `spawn failed: ${toErrorMessage6(err)}`,
|
|
20269
20550
|
exitCode: 1,
|
|
20270
20551
|
truncated: false,
|
|
20271
20552
|
allowed: true,
|
|
@@ -20368,7 +20649,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
20368
20649
|
}
|
|
20369
20650
|
|
|
20370
20651
|
// src/fetch.ts
|
|
20371
|
-
import { FetchError as FetchError2, ToolError, ToolValidationError as
|
|
20652
|
+
import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
|
|
20372
20653
|
import TurndownService from "turndown";
|
|
20373
20654
|
|
|
20374
20655
|
// src/_fetch-guard.ts
|
|
@@ -20378,7 +20659,7 @@ import {
|
|
|
20378
20659
|
isPrivateIPv4 as isPrivateIPv42,
|
|
20379
20660
|
isPrivateIPv6 as isPrivateIPv62
|
|
20380
20661
|
} from "@wrongstack/core/utils";
|
|
20381
|
-
import { FetchError, ToolValidationError as
|
|
20662
|
+
import { FetchError, ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
20382
20663
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
20383
20664
|
var nativeGlobalFetch = globalThis.fetch;
|
|
20384
20665
|
var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
|
|
@@ -20449,13 +20730,13 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
20449
20730
|
for (; ; ) {
|
|
20450
20731
|
const parsed = new URL(currentUrl);
|
|
20451
20732
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
20452
|
-
throw new
|
|
20733
|
+
throw new ToolValidationError4({
|
|
20453
20734
|
message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
|
|
20454
20735
|
field: "url"
|
|
20455
20736
|
});
|
|
20456
20737
|
}
|
|
20457
20738
|
if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
20458
|
-
throw new
|
|
20739
|
+
throw new ToolValidationError4({
|
|
20459
20740
|
message: "fetch: redirect to http:// blocked (HTTPS required by default)",
|
|
20460
20741
|
field: "url"
|
|
20461
20742
|
});
|
|
@@ -20471,6 +20752,10 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
20471
20752
|
if (res.status < 300 || res.status > 399) {
|
|
20472
20753
|
return res;
|
|
20473
20754
|
}
|
|
20755
|
+
try {
|
|
20756
|
+
await res.body?.cancel();
|
|
20757
|
+
} catch {
|
|
20758
|
+
}
|
|
20474
20759
|
redirectCount++;
|
|
20475
20760
|
if (redirectCount > maxRedirects) {
|
|
20476
20761
|
throw new FetchError({
|
|
@@ -20494,7 +20779,7 @@ async function assertNotPrivate(hostname2) {
|
|
|
20494
20779
|
if (ALLOW_PRIVATE) return;
|
|
20495
20780
|
const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
20496
20781
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
20497
|
-
throw new
|
|
20782
|
+
throw new ToolValidationError4({
|
|
20498
20783
|
message: "fetch: blocked localhost target",
|
|
20499
20784
|
field: "url"
|
|
20500
20785
|
});
|
|
@@ -20502,14 +20787,14 @@ async function assertNotPrivate(hostname2) {
|
|
|
20502
20787
|
const ipVersion = net4.isIP(host);
|
|
20503
20788
|
if (ipVersion === 4) {
|
|
20504
20789
|
if (isPrivateIPv42(host)) {
|
|
20505
|
-
throw new
|
|
20790
|
+
throw new ToolValidationError4({
|
|
20506
20791
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
20507
20792
|
field: "url"
|
|
20508
20793
|
});
|
|
20509
20794
|
}
|
|
20510
20795
|
} else if (ipVersion === 6) {
|
|
20511
20796
|
if (isPrivateIPv62(host)) {
|
|
20512
|
-
throw new
|
|
20797
|
+
throw new ToolValidationError4({
|
|
20513
20798
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
20514
20799
|
field: "url"
|
|
20515
20800
|
});
|
|
@@ -20520,14 +20805,14 @@ async function assertNotPrivate(hostname2) {
|
|
|
20520
20805
|
for (const r of records) {
|
|
20521
20806
|
const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
|
|
20522
20807
|
if (bad) {
|
|
20523
|
-
throw new
|
|
20808
|
+
throw new ToolValidationError4({
|
|
20524
20809
|
message: `fetch: resolved to private address ${r.address}`,
|
|
20525
20810
|
field: "url"
|
|
20526
20811
|
});
|
|
20527
20812
|
}
|
|
20528
20813
|
}
|
|
20529
20814
|
} catch (err) {
|
|
20530
|
-
if (err instanceof
|
|
20815
|
+
if (err instanceof ToolValidationError4) throw err;
|
|
20531
20816
|
}
|
|
20532
20817
|
}
|
|
20533
20818
|
}
|
|
@@ -20544,6 +20829,8 @@ TD.addRule("stripDangerousElements", {
|
|
|
20544
20829
|
filter: ["script", "style", "noscript"],
|
|
20545
20830
|
replacement: () => ""
|
|
20546
20831
|
});
|
|
20832
|
+
var PRUNED_BOILERPLATE_TAGS = /* @__PURE__ */ new Set(["nav", "header", "footer", "aside", "svg", "iframe"]);
|
|
20833
|
+
TD.remove((node) => PRUNED_BOILERPLATE_TAGS.has(node.nodeName.toLowerCase()));
|
|
20547
20834
|
var MAX_BYTES = 131072;
|
|
20548
20835
|
var TIMEOUT_MS = 2e4;
|
|
20549
20836
|
var combineSignals = (signals) => AbortSignal.any(signals);
|
|
@@ -20573,7 +20860,7 @@ var fetchTool = {
|
|
|
20573
20860
|
format: {
|
|
20574
20861
|
type: "string",
|
|
20575
20862
|
enum: ["markdown", "text", "raw"],
|
|
20576
|
-
description: 'Output format. "markdown" is recommended for HTML pages.'
|
|
20863
|
+
description: 'Output format. "markdown" is recommended for HTML pages; for non-HTML content types it falls back to plain text (JSON is pretty-printed).'
|
|
20577
20864
|
}
|
|
20578
20865
|
},
|
|
20579
20866
|
required: ["url"]
|
|
@@ -20602,20 +20889,26 @@ var fetchTool = {
|
|
|
20602
20889
|
},
|
|
20603
20890
|
async *executeStream(input, ctx, opts) {
|
|
20604
20891
|
if (!input?.url) {
|
|
20605
|
-
throw new
|
|
20892
|
+
throw new ToolValidationError5({
|
|
20606
20893
|
message: "fetch: url is required",
|
|
20607
20894
|
field: "url"
|
|
20608
20895
|
});
|
|
20609
20896
|
}
|
|
20610
20897
|
const u = new URL(input.url);
|
|
20898
|
+
if (u.username || u.password) {
|
|
20899
|
+
throw new ToolValidationError5({
|
|
20900
|
+
message: "fetch: URLs with embedded credentials (user:pass@host) are not allowed",
|
|
20901
|
+
field: "url"
|
|
20902
|
+
});
|
|
20903
|
+
}
|
|
20611
20904
|
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
20612
|
-
throw new
|
|
20905
|
+
throw new ToolValidationError5({
|
|
20613
20906
|
message: `fetch: unsupported protocol "${u.protocol}"`,
|
|
20614
20907
|
field: "url"
|
|
20615
20908
|
});
|
|
20616
20909
|
}
|
|
20617
20910
|
if (u.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
20618
|
-
throw new
|
|
20911
|
+
throw new ToolValidationError5({
|
|
20619
20912
|
message: "fetch: http:// blocked (HTTPS required by default)",
|
|
20620
20913
|
field: "url"
|
|
20621
20914
|
});
|
|
@@ -20805,8 +21098,9 @@ var formatTool = {
|
|
|
20805
21098
|
type: "final",
|
|
20806
21099
|
output: {
|
|
20807
21100
|
fixer: bridge.language,
|
|
20808
|
-
|
|
20809
|
-
|
|
21101
|
+
// Language-bridge runs don't report per-file counts.
|
|
21102
|
+
files_checked: void 0,
|
|
21103
|
+
files_changed: void 0,
|
|
20810
21104
|
output: normalizeCommandOutput(run.output || run.error || ""),
|
|
20811
21105
|
truncated: run.truncated
|
|
20812
21106
|
}
|
|
@@ -20833,11 +21127,14 @@ var formatTool = {
|
|
|
20833
21127
|
text: `Running ${detected}\u2026`,
|
|
20834
21128
|
data: { fixer: detected, check: !!input.check }
|
|
20835
21129
|
};
|
|
20836
|
-
const
|
|
20837
|
-
|
|
20838
|
-
if (
|
|
20839
|
-
|
|
20840
|
-
args.push(
|
|
21130
|
+
const fileList = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()) : [];
|
|
21131
|
+
let args;
|
|
21132
|
+
if (detected === "prettier") {
|
|
21133
|
+
args = [input.check ? "--check" : "--write"];
|
|
21134
|
+
args.push(...fileList.length > 0 ? fileList : ["."]);
|
|
21135
|
+
} else {
|
|
21136
|
+
args = ["format", input.check ? "--check" : "--write"];
|
|
21137
|
+
if (fileList.length > 0) args.push("--", ...fileList);
|
|
20841
21138
|
}
|
|
20842
21139
|
const result = yield* spawnStream({
|
|
20843
21140
|
cmd: detected,
|
|
@@ -20846,39 +21143,70 @@ var formatTool = {
|
|
|
20846
21143
|
signal: opts.signal,
|
|
20847
21144
|
maxBytes: 1e5
|
|
20848
21145
|
});
|
|
20849
|
-
const
|
|
21146
|
+
const combinedOut = `${result.stdout}
|
|
21147
|
+
${result.stderr}`;
|
|
21148
|
+
const counts = parseFormatterCounts(detected, combinedOut);
|
|
20850
21149
|
yield {
|
|
20851
21150
|
type: "final",
|
|
20852
21151
|
output: {
|
|
20853
21152
|
fixer: detected,
|
|
20854
|
-
files_checked:
|
|
20855
|
-
files_changed: changed,
|
|
21153
|
+
files_checked: counts.checked,
|
|
21154
|
+
files_changed: counts.changed,
|
|
20856
21155
|
output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
|
|
20857
21156
|
truncated: result.truncated
|
|
20858
21157
|
}
|
|
20859
21158
|
};
|
|
20860
21159
|
}
|
|
20861
21160
|
};
|
|
21161
|
+
function parseFormatterCounts(fixer, output) {
|
|
21162
|
+
if (fixer !== "biome") return { checked: void 0, changed: void 0 };
|
|
21163
|
+
const checkedMatch = /\b(?:Checked|Formatted)\s+(\d+)\s+files?\b/i.exec(output);
|
|
21164
|
+
const changedMatch = /\bFixed\s+(\d+)\s+files?\b/i.exec(output);
|
|
21165
|
+
return {
|
|
21166
|
+
checked: checkedMatch?.[1] !== void 0 ? Number(checkedMatch[1]) : void 0,
|
|
21167
|
+
changed: changedMatch?.[1] !== void 0 ? Number(changedMatch[1]) : void 0
|
|
21168
|
+
};
|
|
21169
|
+
}
|
|
20862
21170
|
async function detectFixer(cwd) {
|
|
20863
|
-
const
|
|
20864
|
-
|
|
20865
|
-
await stat18(`${cwd}/biome.json`);
|
|
20866
|
-
return "biome";
|
|
20867
|
-
} catch {
|
|
21171
|
+
const fs35 = await import("node:fs/promises");
|
|
21172
|
+
const exists = async (file) => {
|
|
20868
21173
|
try {
|
|
20869
|
-
await
|
|
20870
|
-
return
|
|
21174
|
+
await fs35.stat(`${cwd}/${file}`);
|
|
21175
|
+
return true;
|
|
20871
21176
|
} catch {
|
|
20872
|
-
return
|
|
21177
|
+
return false;
|
|
20873
21178
|
}
|
|
21179
|
+
};
|
|
21180
|
+
if (await exists("biome.json") || await exists("biome.jsonc")) return "biome";
|
|
21181
|
+
const PRETTIER_CONFIGS = [
|
|
21182
|
+
".prettierrc",
|
|
21183
|
+
".prettierrc.json",
|
|
21184
|
+
".prettierrc.yml",
|
|
21185
|
+
".prettierrc.yaml",
|
|
21186
|
+
".prettierrc.js",
|
|
21187
|
+
".prettierrc.cjs",
|
|
21188
|
+
".prettierrc.mjs",
|
|
21189
|
+
"prettier.config.js",
|
|
21190
|
+
"prettier.config.cjs",
|
|
21191
|
+
"prettier.config.mjs"
|
|
21192
|
+
];
|
|
21193
|
+
for (const cfg of PRETTIER_CONFIGS) {
|
|
21194
|
+
if (await exists(cfg)) return "prettier";
|
|
21195
|
+
}
|
|
21196
|
+
try {
|
|
21197
|
+
const raw = await fs35.readFile(`${cwd}/package.json`, "utf8");
|
|
21198
|
+
const pkg = JSON.parse(raw);
|
|
21199
|
+
if (pkg["prettier"] !== void 0) return "prettier";
|
|
21200
|
+
} catch {
|
|
20874
21201
|
}
|
|
21202
|
+
return "biome";
|
|
20875
21203
|
}
|
|
20876
21204
|
|
|
20877
21205
|
// src/git.ts
|
|
20878
21206
|
init_util();
|
|
20879
21207
|
import { spawn as spawn9 } from "node:child_process";
|
|
20880
21208
|
import { statSync as statSync4 } from "node:fs";
|
|
20881
|
-
import { dirname as
|
|
21209
|
+
import { dirname as dirname13, resolve as resolve14, sep as sep6 } from "node:path";
|
|
20882
21210
|
import { assessCommitSafety } from "@wrongstack/core/coordination";
|
|
20883
21211
|
import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
|
|
20884
21212
|
var TIMEOUT_MS2 = 3e4;
|
|
@@ -21039,8 +21367,8 @@ function validateWorktreeInput(input, projectRoot) {
|
|
|
21039
21367
|
return reject(`unsafe worktree path: ${input.worktreePath}`);
|
|
21040
21368
|
}
|
|
21041
21369
|
if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
|
|
21042
|
-
const root =
|
|
21043
|
-
const abs =
|
|
21370
|
+
const root = resolve14(projectRoot);
|
|
21371
|
+
const abs = resolve14(root, input.worktreePath);
|
|
21044
21372
|
if (abs !== root && !abs.startsWith(root + sep6)) {
|
|
21045
21373
|
return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
|
|
21046
21374
|
}
|
|
@@ -21052,12 +21380,12 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
21052
21380
|
let dir = cwd;
|
|
21053
21381
|
for (let i = 0; i < 20; i++) {
|
|
21054
21382
|
try {
|
|
21055
|
-
const
|
|
21056
|
-
if (
|
|
21383
|
+
const stat19 = statSync4(`${dir}/.git`);
|
|
21384
|
+
if (stat19.isDirectory() || stat19.isFile()) return dir;
|
|
21057
21385
|
} catch {
|
|
21058
21386
|
}
|
|
21059
21387
|
if (dir === root) break;
|
|
21060
|
-
const parent =
|
|
21388
|
+
const parent = dirname13(dir);
|
|
21061
21389
|
if (parent === dir) break;
|
|
21062
21390
|
dir = parent;
|
|
21063
21391
|
}
|
|
@@ -21134,7 +21462,7 @@ function buildArgs(input) {
|
|
|
21134
21462
|
}
|
|
21135
21463
|
}
|
|
21136
21464
|
function runGit2(args, cwd, signal) {
|
|
21137
|
-
return new Promise((
|
|
21465
|
+
return new Promise((resolve17) => {
|
|
21138
21466
|
let stdout = "";
|
|
21139
21467
|
let stderr = "";
|
|
21140
21468
|
const child = spawn9("git", args, {
|
|
@@ -21155,7 +21483,7 @@ function runGit2(args, cwd, signal) {
|
|
|
21155
21483
|
}
|
|
21156
21484
|
});
|
|
21157
21485
|
child.on("error", (err) => {
|
|
21158
|
-
|
|
21486
|
+
resolve17({
|
|
21159
21487
|
command: args[0],
|
|
21160
21488
|
stdout: normalizeCommandOutput(stdout),
|
|
21161
21489
|
stderr: err.message,
|
|
@@ -21164,7 +21492,7 @@ function runGit2(args, cwd, signal) {
|
|
|
21164
21492
|
});
|
|
21165
21493
|
});
|
|
21166
21494
|
child.on("close", (code) => {
|
|
21167
|
-
|
|
21495
|
+
resolve17({
|
|
21168
21496
|
command: args[0],
|
|
21169
21497
|
stdout: normalizeCommandOutput(stdout),
|
|
21170
21498
|
stderr: normalizeCommandOutput(stderr),
|
|
@@ -21177,8 +21505,9 @@ function runGit2(args, cwd, signal) {
|
|
|
21177
21505
|
|
|
21178
21506
|
// src/glob.ts
|
|
21179
21507
|
import * as fs25 from "node:fs/promises";
|
|
21180
|
-
import * as
|
|
21508
|
+
import * as path32 from "node:path";
|
|
21181
21509
|
import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
|
|
21510
|
+
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
21182
21511
|
|
|
21183
21512
|
// src/_concurrency.ts
|
|
21184
21513
|
async function mapWithConcurrency2(items, limit, fn) {
|
|
@@ -21204,7 +21533,7 @@ var WALK_CONCURRENCY = 16;
|
|
|
21204
21533
|
var globTool = {
|
|
21205
21534
|
name: "glob",
|
|
21206
21535
|
category: "Filesystem",
|
|
21207
|
-
description: "Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
|
|
21536
|
+
description: "Find files by path pattern. Results are sorted by modification time, newest first, so when the list is truncated at `limit` it is the oldest files that are dropped (recency-biased). Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
|
|
21208
21537
|
usageHint: "PATH DISCOVERY AND SEARCH SCOPING:\n\n- When `codebase-search` is live, use it first for code concepts; use `glob` for filenames, path patterns, and non-indexed files.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
|
|
21209
21538
|
selection: {
|
|
21210
21539
|
doNotUseWhen: "you need to search inside file contents.",
|
|
@@ -21215,7 +21544,7 @@ var globTool = {
|
|
|
21215
21544
|
capabilities: ["fs.read"],
|
|
21216
21545
|
icon: "folder",
|
|
21217
21546
|
maxOutputBytes: 65536,
|
|
21218
|
-
timeoutMs:
|
|
21547
|
+
timeoutMs: 15e3,
|
|
21219
21548
|
inputSchema: {
|
|
21220
21549
|
type: "object",
|
|
21221
21550
|
properties: {
|
|
@@ -21229,13 +21558,20 @@ var globTool = {
|
|
|
21229
21558
|
},
|
|
21230
21559
|
limit: {
|
|
21231
21560
|
type: "integer",
|
|
21232
|
-
|
|
21561
|
+
minimum: 1,
|
|
21562
|
+
maximum: 5e3,
|
|
21563
|
+
description: "Maximum number of results to return (default 1000, max 5000). Results are sorted by mtime descending, so truncation keeps the most recently modified files."
|
|
21233
21564
|
}
|
|
21234
21565
|
},
|
|
21235
21566
|
required: ["pattern"]
|
|
21236
21567
|
},
|
|
21237
21568
|
async execute(input, ctx, opts) {
|
|
21238
|
-
if (!input?.pattern)
|
|
21569
|
+
if (!input?.pattern) {
|
|
21570
|
+
throw new ToolValidationError6({
|
|
21571
|
+
message: "glob: pattern is required",
|
|
21572
|
+
field: "pattern"
|
|
21573
|
+
});
|
|
21574
|
+
}
|
|
21239
21575
|
const signal = opts?.signal;
|
|
21240
21576
|
const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
|
|
21241
21577
|
const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
|
|
@@ -21280,7 +21616,7 @@ var globTool = {
|
|
|
21280
21616
|
const name = e.name;
|
|
21281
21617
|
if (DEFAULT_IGNORE2.has(name)) continue;
|
|
21282
21618
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
21283
|
-
const full =
|
|
21619
|
+
const full = path32.join(dir, name);
|
|
21284
21620
|
if (e.isDirectory()) {
|
|
21285
21621
|
if (isGitIgnored(rel, true)) continue;
|
|
21286
21622
|
subdirs.push({ full, rel });
|
|
@@ -21330,8 +21666,8 @@ var globTool = {
|
|
|
21330
21666
|
// src/grep.ts
|
|
21331
21667
|
import { spawn as spawn10 } from "node:child_process";
|
|
21332
21668
|
import * as fs26 from "node:fs/promises";
|
|
21333
|
-
import * as
|
|
21334
|
-
import { ToolValidationError as
|
|
21669
|
+
import * as path33 from "node:path";
|
|
21670
|
+
import { ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
|
|
21335
21671
|
import {
|
|
21336
21672
|
buildChildEnv as buildChildEnv5,
|
|
21337
21673
|
compileGlob as compileGlob3,
|
|
@@ -21535,7 +21871,7 @@ var grepTool = {
|
|
|
21535
21871
|
},
|
|
21536
21872
|
async *executeStream(input, ctx, opts) {
|
|
21537
21873
|
if (!input?.pattern) {
|
|
21538
|
-
throw new
|
|
21874
|
+
throw new ToolValidationError7({
|
|
21539
21875
|
message: "grep: pattern is required",
|
|
21540
21876
|
field: "pattern"
|
|
21541
21877
|
});
|
|
@@ -21545,12 +21881,12 @@ var grepTool = {
|
|
|
21545
21881
|
const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
|
|
21546
21882
|
const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
|
|
21547
21883
|
if (!validation.ok) {
|
|
21548
|
-
throw new
|
|
21884
|
+
throw new ToolValidationError7({
|
|
21549
21885
|
message: `grep: ${validation.reason}`,
|
|
21550
21886
|
field: "pattern"
|
|
21551
21887
|
});
|
|
21552
21888
|
}
|
|
21553
|
-
const rgAvailable = await detectRg(
|
|
21889
|
+
const rgAvailable = await detectRg();
|
|
21554
21890
|
if (rgAvailable) {
|
|
21555
21891
|
try {
|
|
21556
21892
|
yield* runRgStream(input, base, mode, limit, opts.signal);
|
|
@@ -21563,16 +21899,23 @@ var grepTool = {
|
|
|
21563
21899
|
yield { type: "final", output: out };
|
|
21564
21900
|
}
|
|
21565
21901
|
};
|
|
21566
|
-
|
|
21567
|
-
|
|
21902
|
+
var rgAvailabilityCache;
|
|
21903
|
+
function detectRg() {
|
|
21904
|
+
rgAvailabilityCache ??= new Promise((resolve17) => {
|
|
21568
21905
|
try {
|
|
21569
|
-
const p = spawn10("rg", ["--version"], {
|
|
21570
|
-
|
|
21571
|
-
|
|
21906
|
+
const p = spawn10("rg", ["--version"], {
|
|
21907
|
+
env: buildChildEnv5(),
|
|
21908
|
+
stdio: "ignore",
|
|
21909
|
+
signal: AbortSignal.timeout(1e4),
|
|
21910
|
+
windowsHide: true
|
|
21911
|
+
});
|
|
21912
|
+
p.on("error", () => resolve17(false));
|
|
21913
|
+
p.on("close", (code) => resolve17(code === 0));
|
|
21572
21914
|
} catch {
|
|
21573
|
-
|
|
21915
|
+
resolve17(false);
|
|
21574
21916
|
}
|
|
21575
21917
|
});
|
|
21918
|
+
return rgAvailabilityCache;
|
|
21576
21919
|
}
|
|
21577
21920
|
async function* runRgStream(input, base, mode, limit, signal) {
|
|
21578
21921
|
const args = ["--no-heading"];
|
|
@@ -21586,7 +21929,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
21586
21929
|
for (const ignored of DEFAULT_IGNORE3) {
|
|
21587
21930
|
args.push("--glob", `!${ignored}/**`, "--glob", `!**/${ignored}/**`);
|
|
21588
21931
|
}
|
|
21589
|
-
const gitignorePath =
|
|
21932
|
+
const gitignorePath = path33.join(base, ".gitignore");
|
|
21590
21933
|
if (await fs26.access(gitignorePath).then(() => true, () => false)) {
|
|
21591
21934
|
args.push("--ignore-file", gitignorePath);
|
|
21592
21935
|
}
|
|
@@ -21758,7 +22101,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
21758
22101
|
const flags = input.case_insensitive ? "i" : "";
|
|
21759
22102
|
const compiled = compileUserRegex(input.pattern, flags);
|
|
21760
22103
|
if (!compiled.ok) {
|
|
21761
|
-
throw new
|
|
22104
|
+
throw new ToolValidationError7({
|
|
21762
22105
|
message: `grep: ${compiled.reason}`,
|
|
21763
22106
|
field: "pattern"
|
|
21764
22107
|
});
|
|
@@ -21776,8 +22119,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
21776
22119
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
21777
22120
|
if (globRe) globRe.lastIndex = 0;
|
|
21778
22121
|
try {
|
|
21779
|
-
const
|
|
21780
|
-
if (!
|
|
22122
|
+
const stat19 = await fs26.stat(full);
|
|
22123
|
+
if (!stat19.isFile() || stat19.size > maxBytes || stopped || signal.aborted) return;
|
|
21781
22124
|
const file = await fs26.open(full, "r");
|
|
21782
22125
|
try {
|
|
21783
22126
|
let bytesReadTotal = 0;
|
|
@@ -21868,7 +22211,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
21868
22211
|
if (DEFAULT_IGNORE3.has(e.name)) continue;
|
|
21869
22212
|
if (e.isSymbolicLink()) continue;
|
|
21870
22213
|
const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
|
|
21871
|
-
const full =
|
|
22214
|
+
const full = path33.join(dir, e.name);
|
|
21872
22215
|
if (e.isDirectory()) {
|
|
21873
22216
|
if (isGitIgnored(rel, true)) continue;
|
|
21874
22217
|
subdirs.push({ full, rel });
|
|
@@ -21988,26 +22331,14 @@ var installTool = {
|
|
|
21988
22331
|
return;
|
|
21989
22332
|
}
|
|
21990
22333
|
}
|
|
21991
|
-
const pkgManager = await detectPackageManager(cwd);
|
|
22334
|
+
const pkgManager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
21992
22335
|
yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
|
|
21993
|
-
const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
|
|
21994
22336
|
const globalFlag = input.global ? ["-g"] : [];
|
|
21995
22337
|
const ignoreScripts = input.lifecycleScripts !== true;
|
|
21996
|
-
const args = [];
|
|
21997
|
-
if (input.dry_run) args.push("--dry-run");
|
|
21998
|
-
if (ignoreScripts) args.push("--ignore-scripts");
|
|
21999
|
-
if (pkgManager === "pnpm") {
|
|
22000
|
-
if (save) args.push(save);
|
|
22001
|
-
args.push("add", ...globalFlag);
|
|
22002
|
-
} else if (pkgManager === "yarn") {
|
|
22003
|
-
args.push("add", ...globalFlag);
|
|
22004
|
-
} else {
|
|
22005
|
-
args.push("install", ...globalFlag);
|
|
22006
|
-
}
|
|
22007
22338
|
const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
|
|
22008
22339
|
(p) => p.trim()
|
|
22009
22340
|
) : [];
|
|
22010
|
-
const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]
|
|
22341
|
+
const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9^~><=*.+-]+)?$/i;
|
|
22011
22342
|
for (const pkg of pkgList) {
|
|
22012
22343
|
if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-") || pkg.length > 200) {
|
|
22013
22344
|
yield {
|
|
@@ -22023,7 +22354,34 @@ var installTool = {
|
|
|
22023
22354
|
return;
|
|
22024
22355
|
}
|
|
22025
22356
|
}
|
|
22026
|
-
|
|
22357
|
+
const hasPkgs = pkgList.length > 0;
|
|
22358
|
+
const args = [];
|
|
22359
|
+
if (input.dry_run) args.push("--dry-run");
|
|
22360
|
+
if (ignoreScripts) args.push("--ignore-scripts");
|
|
22361
|
+
if (pkgManager === "pnpm") {
|
|
22362
|
+
if (hasPkgs) {
|
|
22363
|
+
if (input.save === "dev") args.push("-D");
|
|
22364
|
+
else if (input.save === "optional") args.push("-O");
|
|
22365
|
+
args.push("add", ...globalFlag);
|
|
22366
|
+
} else {
|
|
22367
|
+
args.push("install", ...globalFlag);
|
|
22368
|
+
}
|
|
22369
|
+
} else if (pkgManager === "yarn") {
|
|
22370
|
+
if (hasPkgs) {
|
|
22371
|
+
args.push("add", ...globalFlag);
|
|
22372
|
+
if (input.save === "dev") args.push("--dev");
|
|
22373
|
+
else if (input.save === "optional") args.push("--optional");
|
|
22374
|
+
} else {
|
|
22375
|
+
args.push("install", ...globalFlag);
|
|
22376
|
+
}
|
|
22377
|
+
} else {
|
|
22378
|
+
args.push("install", ...globalFlag);
|
|
22379
|
+
if (hasPkgs) {
|
|
22380
|
+
if (input.save === "dev") args.push("--save-dev");
|
|
22381
|
+
else if (input.save === "optional") args.push("--save-optional");
|
|
22382
|
+
}
|
|
22383
|
+
}
|
|
22384
|
+
if (hasPkgs) args.push(...pkgList);
|
|
22027
22385
|
yield {
|
|
22028
22386
|
type: "log",
|
|
22029
22387
|
text: `Fetching ${pkgList.length || "all"} packages\u2026`,
|
|
@@ -22104,9 +22462,9 @@ var JsonFileTooLargeError = class extends Error {
|
|
|
22104
22462
|
};
|
|
22105
22463
|
async function readJsonFileBounded(filePath, ctx) {
|
|
22106
22464
|
const resolved = await safeResolveReal(filePath, ctx);
|
|
22107
|
-
const
|
|
22108
|
-
if (
|
|
22109
|
-
throw new JsonFileTooLargeError(filePath,
|
|
22465
|
+
const stat19 = await fs27.stat(resolved);
|
|
22466
|
+
if (stat19.size > MAX_JSON_FILE_BYTES) {
|
|
22467
|
+
throw new JsonFileTooLargeError(filePath, stat19.size);
|
|
22110
22468
|
}
|
|
22111
22469
|
return fs27.readFile(resolved, "utf8");
|
|
22112
22470
|
}
|
|
@@ -22512,60 +22870,60 @@ function jmespathSearch(data, query) {
|
|
|
22512
22870
|
}
|
|
22513
22871
|
function validateJsonSchema(data, schema) {
|
|
22514
22872
|
const errors = [];
|
|
22515
|
-
function check(value, s,
|
|
22873
|
+
function check(value, s, path40) {
|
|
22516
22874
|
if (s["type"]) {
|
|
22517
22875
|
const expectedType = s["type"];
|
|
22518
22876
|
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
22519
22877
|
if (expectedType === "integer") {
|
|
22520
|
-
if (!Number.isInteger(value)) errors.push(`${
|
|
22878
|
+
if (!Number.isInteger(value)) errors.push(`${path40}: expected integer, got ${actualType}`);
|
|
22521
22879
|
} else if (expectedType !== actualType) {
|
|
22522
|
-
errors.push(`${
|
|
22880
|
+
errors.push(`${path40}: expected ${expectedType}, got ${actualType}`);
|
|
22523
22881
|
}
|
|
22524
22882
|
}
|
|
22525
22883
|
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
22526
22884
|
try {
|
|
22527
22885
|
new URL(value);
|
|
22528
22886
|
} catch {
|
|
22529
|
-
errors.push(`${
|
|
22887
|
+
errors.push(`${path40}: not a valid URI`);
|
|
22530
22888
|
}
|
|
22531
22889
|
}
|
|
22532
22890
|
if (typeof value === "string" && s["pattern"]) {
|
|
22533
22891
|
const compiled = compileUserRegex(s["pattern"], "");
|
|
22534
22892
|
if (!compiled.ok) {
|
|
22535
|
-
errors.push(`${
|
|
22893
|
+
errors.push(`${path40}: invalid schema pattern \u2014 ${compiled.reason}`);
|
|
22536
22894
|
} else if (!compiled.regex.test(capSubject(value))) {
|
|
22537
|
-
errors.push(`${
|
|
22895
|
+
errors.push(`${path40}: does not match pattern ${s["pattern"]}`);
|
|
22538
22896
|
}
|
|
22539
22897
|
}
|
|
22540
22898
|
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
22541
|
-
errors.push(`${
|
|
22899
|
+
errors.push(`${path40}: string too short (min ${s["minLength"]})`);
|
|
22542
22900
|
}
|
|
22543
22901
|
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
22544
|
-
errors.push(`${
|
|
22902
|
+
errors.push(`${path40}: string too long (max ${s["maxLength"]})`);
|
|
22545
22903
|
}
|
|
22546
22904
|
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
22547
|
-
errors.push(`${
|
|
22905
|
+
errors.push(`${path40}: below minimum ${s["minimum"]}`);
|
|
22548
22906
|
}
|
|
22549
22907
|
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
22550
|
-
errors.push(`${
|
|
22908
|
+
errors.push(`${path40}: above maximum ${s["maximum"]}`);
|
|
22551
22909
|
}
|
|
22552
22910
|
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
22553
22911
|
for (let i = 0; i < value.length; i++) {
|
|
22554
|
-
check(value[i], s["items"], `${
|
|
22912
|
+
check(value[i], s["items"], `${path40}[${i}]`);
|
|
22555
22913
|
}
|
|
22556
22914
|
}
|
|
22557
22915
|
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
22558
22916
|
const props = s["properties"];
|
|
22559
22917
|
for (const [k, propSchema] of Object.entries(props)) {
|
|
22560
|
-
check(value[k], propSchema, `${
|
|
22918
|
+
check(value[k], propSchema, `${path40}.${k}`);
|
|
22561
22919
|
}
|
|
22562
22920
|
}
|
|
22563
22921
|
}
|
|
22564
22922
|
check(data, schema, "$");
|
|
22565
22923
|
return { valid: errors.length === 0, errors };
|
|
22566
22924
|
}
|
|
22567
|
-
function simpleQuery(data,
|
|
22568
|
-
const parts =
|
|
22925
|
+
function simpleQuery(data, path40) {
|
|
22926
|
+
const parts = path40.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
22569
22927
|
let current = data;
|
|
22570
22928
|
for (const part of parts) {
|
|
22571
22929
|
if (current === null || current === void 0) return void 0;
|
|
@@ -23476,6 +23834,18 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
23476
23834
|
},
|
|
23477
23835
|
transitionAction: { type: "string" },
|
|
23478
23836
|
transitionComment: { type: "string" },
|
|
23837
|
+
tickChecks: {
|
|
23838
|
+
type: "array",
|
|
23839
|
+
items: {
|
|
23840
|
+
type: "object",
|
|
23841
|
+
properties: {
|
|
23842
|
+
checkId: { type: "string" },
|
|
23843
|
+
checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
|
|
23844
|
+
},
|
|
23845
|
+
required: ["checkId", "checkStatus"]
|
|
23846
|
+
},
|
|
23847
|
+
description: "`transition_task` (to=done only): flip one or more manual criteria to `passed` before the gate fires. Read ids from kanban get_task. Non-manual criteria are refused."
|
|
23848
|
+
},
|
|
23479
23849
|
attachmentUrl: { type: "string" },
|
|
23480
23850
|
attachmentTitle: { type: "string" },
|
|
23481
23851
|
attachmentType: {
|
|
@@ -24176,6 +24546,9 @@ var kanbanTool = {
|
|
|
24176
24546
|
description: KANBAN_TOOL_DESCRIPTION,
|
|
24177
24547
|
usageHint: KANBAN_TOOL_USAGE_HINT,
|
|
24178
24548
|
permission: "confirm",
|
|
24549
|
+
// WS-046: gives permission decisions something to key on.
|
|
24550
|
+
// The action performed; kanban has no single file or path subject.
|
|
24551
|
+
subjectKey: "action",
|
|
24179
24552
|
mutating: true,
|
|
24180
24553
|
capabilities: ["fs.write"],
|
|
24181
24554
|
icon: "task",
|
|
@@ -24617,6 +24990,7 @@ var kanbanTool = {
|
|
|
24617
24990
|
actor: input.author,
|
|
24618
24991
|
comment: input.transitionComment,
|
|
24619
24992
|
...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
|
|
24993
|
+
...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
|
|
24620
24994
|
...input.attachmentUrl !== void 0 ? {
|
|
24621
24995
|
attachment: {
|
|
24622
24996
|
url: input.attachmentUrl,
|
|
@@ -25009,8 +25383,52 @@ var kanbanTool = {
|
|
|
25009
25383
|
} catch (err) {
|
|
25010
25384
|
return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
|
|
25011
25385
|
}
|
|
25386
|
+
},
|
|
25387
|
+
serialize(output, input) {
|
|
25388
|
+
return serializeKanbanOutput(output, input);
|
|
25012
25389
|
}
|
|
25013
25390
|
};
|
|
25391
|
+
var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
|
|
25392
|
+
var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
|
|
25393
|
+
"get_board",
|
|
25394
|
+
"export_markdown",
|
|
25395
|
+
"export_task_graph"
|
|
25396
|
+
]);
|
|
25397
|
+
function serializeKanbanOutput(output, input) {
|
|
25398
|
+
const action = input && typeof input === "object" ? input.action : void 0;
|
|
25399
|
+
const board = output.board;
|
|
25400
|
+
if (board) {
|
|
25401
|
+
const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
|
|
25402
|
+
let boardBytes = 0;
|
|
25403
|
+
if (!keepFull) {
|
|
25404
|
+
try {
|
|
25405
|
+
boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
|
|
25406
|
+
} catch {
|
|
25407
|
+
boardBytes = 0;
|
|
25408
|
+
}
|
|
25409
|
+
}
|
|
25410
|
+
if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
|
|
25411
|
+
const columns = {};
|
|
25412
|
+
for (const column of board.columns) {
|
|
25413
|
+
columns[column.title || column.id] = board.tasks.filter(
|
|
25414
|
+
(task) => task.columnId === column.id
|
|
25415
|
+
).length;
|
|
25416
|
+
}
|
|
25417
|
+
const compact = {
|
|
25418
|
+
...output,
|
|
25419
|
+
board: {
|
|
25420
|
+
id: board.id,
|
|
25421
|
+
title: board.title,
|
|
25422
|
+
columns,
|
|
25423
|
+
totalTasks: board.tasks.length,
|
|
25424
|
+
note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
|
|
25425
|
+
}
|
|
25426
|
+
};
|
|
25427
|
+
return JSON.stringify(compact, null, 2);
|
|
25428
|
+
}
|
|
25429
|
+
}
|
|
25430
|
+
return JSON.stringify(output, null, 2);
|
|
25431
|
+
}
|
|
25014
25432
|
|
|
25015
25433
|
// src/builtin.ts
|
|
25016
25434
|
init_execute_tool();
|
|
@@ -25125,11 +25543,11 @@ var lintTool = {
|
|
|
25125
25543
|
}
|
|
25126
25544
|
};
|
|
25127
25545
|
async function detectLinter(cwd) {
|
|
25128
|
-
const { stat:
|
|
25546
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
25129
25547
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
25130
25548
|
for (const f of checks) {
|
|
25131
25549
|
try {
|
|
25132
|
-
await
|
|
25550
|
+
await stat19(`${cwd}/${f}`);
|
|
25133
25551
|
if (f.includes("biome")) return "biome";
|
|
25134
25552
|
if (f.includes("eslint")) return "eslint";
|
|
25135
25553
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -25146,11 +25564,12 @@ init_util();
|
|
|
25146
25564
|
var logsTool = {
|
|
25147
25565
|
name: "logs",
|
|
25148
25566
|
category: "Logs",
|
|
25149
|
-
description: "Read
|
|
25150
|
-
usageHint: "DEBUGGING TOOL \u2014 USE CAREFULLY IN AUTONOMOUS MODE:\n\n- Prefer `path` for local files or `service` for containers
|
|
25567
|
+
description: "Read logs from files or Docker containers. Useful for debugging running applications.",
|
|
25568
|
+
usageHint: "DEBUGGING TOOL \u2014 USE CAREFULLY IN AUTONOMOUS MODE:\n\n- Prefer `path` for local files or `service` for Docker containers.\n- Always use `filter` (regex) when possible to reduce noise and token usage.\n- `since` narrows Docker logs to a recent window.",
|
|
25151
25569
|
permission: "confirm",
|
|
25152
25570
|
mutating: false,
|
|
25153
25571
|
timeoutMs: 3e4,
|
|
25572
|
+
maxOutputBytes: 262144,
|
|
25154
25573
|
capabilities: ["shell.restricted"],
|
|
25155
25574
|
icon: "logs",
|
|
25156
25575
|
inputSchema: {
|
|
@@ -25158,7 +25577,7 @@ var logsTool = {
|
|
|
25158
25577
|
properties: {
|
|
25159
25578
|
service: {
|
|
25160
25579
|
type: "string",
|
|
25161
|
-
description: "
|
|
25580
|
+
description: "Docker container name (passed to `docker logs`)"
|
|
25162
25581
|
},
|
|
25163
25582
|
path: {
|
|
25164
25583
|
type: "string",
|
|
@@ -25170,10 +25589,6 @@ var logsTool = {
|
|
|
25170
25589
|
minimum: 0,
|
|
25171
25590
|
maximum: 1e4
|
|
25172
25591
|
},
|
|
25173
|
-
stream: {
|
|
25174
|
-
type: "boolean",
|
|
25175
|
-
description: "Stream logs continuously (like tail -f) (default: false)"
|
|
25176
|
-
},
|
|
25177
25592
|
filter: {
|
|
25178
25593
|
type: "string",
|
|
25179
25594
|
description: "Regex pattern to filter log lines"
|
|
@@ -25181,7 +25596,7 @@ var logsTool = {
|
|
|
25181
25596
|
since: {
|
|
25182
25597
|
type: "string",
|
|
25183
25598
|
enum: ["1h", "6h", "24h", "all"],
|
|
25184
|
-
description:
|
|
25599
|
+
description: 'Only show Docker logs since duration (ignored for files; "all" = no limit)'
|
|
25185
25600
|
},
|
|
25186
25601
|
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
25187
25602
|
}
|
|
@@ -25198,10 +25613,10 @@ var logsTool = {
|
|
|
25198
25613
|
filterRe = compiled.regex;
|
|
25199
25614
|
}
|
|
25200
25615
|
if (input.service) {
|
|
25201
|
-
return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);
|
|
25616
|
+
return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal, input.since);
|
|
25202
25617
|
}
|
|
25203
25618
|
if (input.path) {
|
|
25204
|
-
return await fileLogs(
|
|
25619
|
+
return await fileLogs(await safeResolveReal(input.path, ctx), lines, filterRe);
|
|
25205
25620
|
}
|
|
25206
25621
|
return {
|
|
25207
25622
|
source: "none",
|
|
@@ -25215,7 +25630,7 @@ var logsTool = {
|
|
|
25215
25630
|
async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
25216
25631
|
const args = ["logs"];
|
|
25217
25632
|
if (lines > 0) args.push("--tail", String(lines));
|
|
25218
|
-
if (since) {
|
|
25633
|
+
if (since && since !== "all") {
|
|
25219
25634
|
const sinceMap = { "1h": "1h", "6h": "6h", "24h": "24h" };
|
|
25220
25635
|
args.push("--since", sinceMap[since] ?? "1h");
|
|
25221
25636
|
}
|
|
@@ -25229,7 +25644,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25229
25644
|
};
|
|
25230
25645
|
}
|
|
25231
25646
|
args.push("--timestamps", service);
|
|
25232
|
-
return new Promise((
|
|
25647
|
+
return new Promise((resolve17) => {
|
|
25233
25648
|
let stdout = "";
|
|
25234
25649
|
let stderr = "";
|
|
25235
25650
|
const MAX = 2e5;
|
|
@@ -25245,7 +25660,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25245
25660
|
if (settled) return;
|
|
25246
25661
|
settled = true;
|
|
25247
25662
|
clearTimeout(timer);
|
|
25248
|
-
|
|
25663
|
+
resolve17(result);
|
|
25249
25664
|
};
|
|
25250
25665
|
const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
25251
25666
|
const timer = setTimeout(() => {
|
|
@@ -25280,7 +25695,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25280
25695
|
}
|
|
25281
25696
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
25282
25697
|
var MAX_TAIL_LINES = 1e5;
|
|
25283
|
-
async function fileLogs(
|
|
25698
|
+
async function fileLogs(path40, lines, filterRe) {
|
|
25284
25699
|
const { createInterface } = await import("node:readline");
|
|
25285
25700
|
const { createReadStream: createReadStream2 } = await import("node:fs");
|
|
25286
25701
|
const entries = [];
|
|
@@ -25289,7 +25704,7 @@ async function fileLogs(path39, lines, filterRe, stream) {
|
|
|
25289
25704
|
let writeIdx = 0;
|
|
25290
25705
|
let totalLines = 0;
|
|
25291
25706
|
const rl = createInterface({
|
|
25292
|
-
input: createReadStream2(
|
|
25707
|
+
input: createReadStream2(path40),
|
|
25293
25708
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
25294
25709
|
});
|
|
25295
25710
|
for await (const line of rl) {
|
|
@@ -25310,11 +25725,11 @@ async function fileLogs(path39, lines, filterRe, stream) {
|
|
|
25310
25725
|
if (parsed) entries.push(parsed);
|
|
25311
25726
|
}
|
|
25312
25727
|
return {
|
|
25313
|
-
source:
|
|
25728
|
+
source: path40,
|
|
25314
25729
|
entries,
|
|
25315
25730
|
total: entries.length,
|
|
25316
25731
|
truncated: totalLines > effLines,
|
|
25317
|
-
stream_mode:
|
|
25732
|
+
stream_mode: false
|
|
25318
25733
|
};
|
|
25319
25734
|
}
|
|
25320
25735
|
function parseLogLines(output, filterRe) {
|
|
@@ -25390,25 +25805,12 @@ var outdatedTool = {
|
|
|
25390
25805
|
inputSchema: {
|
|
25391
25806
|
type: "object",
|
|
25392
25807
|
properties: {
|
|
25393
|
-
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
25394
|
-
format: {
|
|
25395
|
-
type: "string",
|
|
25396
|
-
enum: ["list", "table"],
|
|
25397
|
-
description: "Output format (default: list)"
|
|
25398
|
-
},
|
|
25399
|
-
include_deprecated: {
|
|
25400
|
-
type: "boolean",
|
|
25401
|
-
description: "Include deprecated packages (default: false)"
|
|
25402
|
-
},
|
|
25403
|
-
check: {
|
|
25404
|
-
type: "string",
|
|
25405
|
-
description: "Specific package(s) to check (comma-separated)"
|
|
25406
|
-
}
|
|
25808
|
+
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
25407
25809
|
}
|
|
25408
25810
|
},
|
|
25409
25811
|
async execute(input, ctx, opts) {
|
|
25410
25812
|
const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
|
|
25411
|
-
const manager = await detectPackageManager(cwd);
|
|
25813
|
+
const manager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
25412
25814
|
if (manager === "npm") {
|
|
25413
25815
|
try {
|
|
25414
25816
|
const { detectNonJsEcosystem: detectNonJsEcosystem2 } = await Promise.resolve().then(() => (init_legacy_bridge(), legacy_bridge_exports));
|
|
@@ -25461,13 +25863,11 @@ var outdatedTool = {
|
|
|
25461
25863
|
}
|
|
25462
25864
|
}
|
|
25463
25865
|
const args = ["outdated", "--json"];
|
|
25464
|
-
if (input.format === "table") args.push("--table");
|
|
25465
|
-
if (input.include_deprecated) args.push("--include", "deprecated");
|
|
25466
25866
|
return runOutdated(manager, args, cwd, opts.signal);
|
|
25467
25867
|
}
|
|
25468
25868
|
};
|
|
25469
25869
|
function runOutdated(manager, args, cwd, signal) {
|
|
25470
|
-
return new Promise((
|
|
25870
|
+
return new Promise((resolve17) => {
|
|
25471
25871
|
let stdout = "";
|
|
25472
25872
|
let stderr = "";
|
|
25473
25873
|
const MAX = 1e5;
|
|
@@ -25492,10 +25892,10 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
25492
25892
|
});
|
|
25493
25893
|
child.on("close", (code) => {
|
|
25494
25894
|
const result = parseOutdatedOutput(stdout, code ?? 0);
|
|
25495
|
-
|
|
25895
|
+
resolve17(result);
|
|
25496
25896
|
});
|
|
25497
25897
|
child.on("error", (e) => {
|
|
25498
|
-
|
|
25898
|
+
resolve17({
|
|
25499
25899
|
exit_code: 1,
|
|
25500
25900
|
packages: [],
|
|
25501
25901
|
total: 0,
|
|
@@ -25516,27 +25916,39 @@ function parseOutdatedOutput(json2, exitCode) {
|
|
|
25516
25916
|
truncated: false
|
|
25517
25917
|
};
|
|
25518
25918
|
}
|
|
25919
|
+
const truncated = json2.length >= 1e5 || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
|
|
25920
|
+
let parsedOk = false;
|
|
25519
25921
|
try {
|
|
25520
25922
|
const data = JSON.parse(json2);
|
|
25923
|
+
parsedOk = true;
|
|
25521
25924
|
for (const name of Object.keys(data)) {
|
|
25522
|
-
const info = data[name];
|
|
25925
|
+
const info = data[name] ?? {};
|
|
25926
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
25523
25927
|
packages.push({
|
|
25524
25928
|
name,
|
|
25525
|
-
current: info
|
|
25526
|
-
latest: info
|
|
25527
|
-
wanted: info
|
|
25528
|
-
|
|
25529
|
-
|
|
25929
|
+
current: str(info["current"]) ?? "unknown",
|
|
25930
|
+
latest: str(info["latest"]) ?? "unknown",
|
|
25931
|
+
wanted: str(info["wanted"]) ?? "unknown",
|
|
25932
|
+
// npm calls it `type`; pnpm calls it `dependencyType`.
|
|
25933
|
+
type: str(info["type"]) ?? str(info["dependencyType"]) ?? "unknown",
|
|
25934
|
+
location: str(info["location"]) ?? name
|
|
25530
25935
|
});
|
|
25531
25936
|
}
|
|
25532
25937
|
} catch {
|
|
25938
|
+
}
|
|
25939
|
+
const outdatedFound = parsedOk && exitCode === 1;
|
|
25940
|
+
let output = normalizeCommandOutput(json2);
|
|
25941
|
+
if (outdatedFound) {
|
|
25942
|
+
output = `${output}
|
|
25943
|
+
|
|
25944
|
+
Note: exit code 1 from \`outdated\` means outdated packages were found (expected); treated as success.`;
|
|
25533
25945
|
}
|
|
25534
25946
|
return {
|
|
25535
|
-
exit_code: exitCode,
|
|
25947
|
+
exit_code: outdatedFound ? 0 : exitCode,
|
|
25536
25948
|
packages,
|
|
25537
25949
|
total: packages.length,
|
|
25538
|
-
output
|
|
25539
|
-
truncated
|
|
25950
|
+
output,
|
|
25951
|
+
truncated
|
|
25540
25952
|
};
|
|
25541
25953
|
}
|
|
25542
25954
|
|
|
@@ -25545,8 +25957,8 @@ init_util();
|
|
|
25545
25957
|
import { spawn as spawn13 } from "node:child_process";
|
|
25546
25958
|
import * as fs28 from "node:fs/promises";
|
|
25547
25959
|
import * as os9 from "node:os";
|
|
25548
|
-
import * as
|
|
25549
|
-
import { buildChildEnv as buildChildEnv8, toErrorMessage as
|
|
25960
|
+
import * as path34 from "node:path";
|
|
25961
|
+
import { buildChildEnv as buildChildEnv8, toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
|
|
25550
25962
|
var patchTool = {
|
|
25551
25963
|
name: "patch",
|
|
25552
25964
|
category: "Filesystem",
|
|
@@ -25590,26 +26002,26 @@ var patchTool = {
|
|
|
25590
26002
|
try {
|
|
25591
26003
|
dir = input.directory ? await safeResolveReal(input.directory, ctx) : ctx.cwd;
|
|
25592
26004
|
} catch (err) {
|
|
25593
|
-
return refuse(`patch refused: ${
|
|
26005
|
+
return refuse(`patch refused: ${toErrorMessage7(err)}`);
|
|
25594
26006
|
}
|
|
25595
|
-
const realRoot = await fs28.realpath(ctx.projectRoot).catch(() =>
|
|
26007
|
+
const realRoot = await fs28.realpath(ctx.projectRoot).catch(() => path34.resolve(ctx.projectRoot));
|
|
25596
26008
|
const targets = extractDiffTargets(input.patch);
|
|
25597
26009
|
const resolvedTargets = [];
|
|
25598
26010
|
for (const t of targets) {
|
|
25599
26011
|
const stripped = stripPathComponents(t.raw, strip);
|
|
25600
26012
|
if (!stripped) continue;
|
|
25601
|
-
if (
|
|
26013
|
+
if (path34.isAbsolute(stripped)) {
|
|
25602
26014
|
return refuse(`patch refused: target "${t.raw}" strips to absolute path`);
|
|
25603
26015
|
}
|
|
25604
|
-
const candidate =
|
|
26016
|
+
const candidate = path34.resolve(dir, stripped);
|
|
25605
26017
|
let real;
|
|
25606
26018
|
try {
|
|
25607
26019
|
real = await resolveRealInsideRoot(candidate, ctx);
|
|
25608
26020
|
} catch (err) {
|
|
25609
|
-
return refuse(`patch refused: target "${t.raw}" ${
|
|
26021
|
+
return refuse(`patch refused: target "${t.raw}" ${toErrorMessage7(err)}`);
|
|
25610
26022
|
}
|
|
25611
|
-
const rel =
|
|
25612
|
-
if (rel.startsWith("..") ||
|
|
26023
|
+
const rel = path34.relative(realRoot, real);
|
|
26024
|
+
if (rel.startsWith("..") || path34.isAbsolute(rel)) {
|
|
25613
26025
|
return refuse(`patch refused: target "${t.raw}" resolves outside project root`);
|
|
25614
26026
|
}
|
|
25615
26027
|
resolvedTargets.push({ raw: t.raw, deleted: t.deleted, abs: real });
|
|
@@ -25623,11 +26035,11 @@ var patchTool = {
|
|
|
25623
26035
|
beforeContents.set(target.abs, await readTextForTracking(target.abs));
|
|
25624
26036
|
}
|
|
25625
26037
|
}
|
|
25626
|
-
const tmpDir = await fs28.mkdtemp(
|
|
26038
|
+
const tmpDir = await fs28.mkdtemp(path34.join(os9.tmpdir(), ".wstack_patch_"));
|
|
25627
26039
|
try {
|
|
25628
26040
|
await fs28.chmod(tmpDir, 448).catch(() => {
|
|
25629
26041
|
});
|
|
25630
|
-
const patchFile =
|
|
26042
|
+
const patchFile = path34.join(tmpDir, "in.diff");
|
|
25631
26043
|
await fs28.writeFile(patchFile, input.patch, { mode: 384 });
|
|
25632
26044
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
25633
26045
|
const result = await runPatch(args, dir, opts.signal, {
|
|
@@ -25640,8 +26052,8 @@ var patchTool = {
|
|
|
25640
26052
|
for (const target of resolvedTargets) {
|
|
25641
26053
|
const abs = target.abs;
|
|
25642
26054
|
const before = beforeContents.get(abs) ?? null;
|
|
25643
|
-
const
|
|
25644
|
-
if (!
|
|
26055
|
+
const stat19 = await fs28.stat(abs).catch(() => null);
|
|
26056
|
+
if (!stat19?.isFile()) {
|
|
25645
26057
|
if (beforeExisted.has(abs)) {
|
|
25646
26058
|
touched.push(abs);
|
|
25647
26059
|
ctx.session?.recordFileChange?.({
|
|
@@ -25656,7 +26068,7 @@ var patchTool = {
|
|
|
25656
26068
|
const after = await readTextForTracking(abs);
|
|
25657
26069
|
if (after === null || after === before) continue;
|
|
25658
26070
|
touched.push(abs);
|
|
25659
|
-
ctx.recordRead?.(abs,
|
|
26071
|
+
ctx.recordRead?.(abs, stat19.mtimeMs, "write", sha256hex(after));
|
|
25660
26072
|
ctx.session?.recordFileChange?.({
|
|
25661
26073
|
path: abs,
|
|
25662
26074
|
action: before === null ? "created" : "modified",
|
|
@@ -25667,7 +26079,7 @@ var patchTool = {
|
|
|
25667
26079
|
}
|
|
25668
26080
|
if (result.exitCode !== 0) {
|
|
25669
26081
|
if (!dryRun) {
|
|
25670
|
-
const partial = touched.length > 0 ? ` ${touched.length} file(s) were still modified on disk and have been recorded for rewind: ${touched.map((p) =>
|
|
26082
|
+
const partial = touched.length > 0 ? ` ${touched.length} file(s) were still modified on disk and have been recorded for rewind: ${touched.map((p) => path34.relative(realRoot, p) || p).join(", ")}.` : "";
|
|
25671
26083
|
return {
|
|
25672
26084
|
applied: touched.length,
|
|
25673
26085
|
rejected: 1,
|
|
@@ -25675,7 +26087,7 @@ var patchTool = {
|
|
|
25675
26087
|
// success path (which returns GNU patch's dir-relative names).
|
|
25676
26088
|
// `touched` entries are realpaths from resolveRealInsideRoot, and
|
|
25677
26089
|
// realRoot is also a realpath, so path.relative is like-for-like.
|
|
25678
|
-
files: touched.map((p) =>
|
|
26090
|
+
files: touched.map((p) => path34.relative(realRoot, p) || p),
|
|
25679
26091
|
dry_run: dryRun,
|
|
25680
26092
|
message: `patch failed: ${result.stderr || result.stdout}${partial}`
|
|
25681
26093
|
};
|
|
@@ -25691,7 +26103,7 @@ var patchTool = {
|
|
|
25691
26103
|
}
|
|
25692
26104
|
const patched = result.engine === "git" ? [
|
|
25693
26105
|
...new Set(
|
|
25694
|
-
resolvedTargets.map((target) =>
|
|
26106
|
+
resolvedTargets.map((target) => path34.relative(dir, target.abs) || target.abs)
|
|
25695
26107
|
)
|
|
25696
26108
|
] : extractPatchedFiles(result.stdout);
|
|
25697
26109
|
return {
|
|
@@ -25710,8 +26122,8 @@ var patchTool = {
|
|
|
25710
26122
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
25711
26123
|
async function readTextForTracking(absPath) {
|
|
25712
26124
|
try {
|
|
25713
|
-
const
|
|
25714
|
-
if (!
|
|
26125
|
+
const stat19 = await fs28.stat(absPath);
|
|
26126
|
+
if (!stat19.isFile() || stat19.size > MAX_TRACKING_BYTES) return null;
|
|
25715
26127
|
const buf = await fs28.readFile(absPath);
|
|
25716
26128
|
if (buf.includes(0)) return null;
|
|
25717
26129
|
return buf.toString("utf8");
|
|
@@ -25796,7 +26208,7 @@ function runPatch(args, cwd, signal, fallback) {
|
|
|
25796
26208
|
});
|
|
25797
26209
|
}
|
|
25798
26210
|
function runPatchProcess(command, args, cwd, signal) {
|
|
25799
|
-
return new Promise((
|
|
26211
|
+
return new Promise((resolve17) => {
|
|
25800
26212
|
let stdout = "";
|
|
25801
26213
|
let stderr = "";
|
|
25802
26214
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
@@ -25815,11 +26227,11 @@ function runPatchProcess(command, args, cwd, signal) {
|
|
|
25815
26227
|
});
|
|
25816
26228
|
child.on(
|
|
25817
26229
|
"close",
|
|
25818
|
-
(code) =>
|
|
26230
|
+
(code) => resolve17({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
|
|
25819
26231
|
);
|
|
25820
26232
|
child.on(
|
|
25821
26233
|
"error",
|
|
25822
|
-
(e) =>
|
|
26234
|
+
(e) => resolve17({
|
|
25823
26235
|
exitCode: 1,
|
|
25824
26236
|
stdout: "",
|
|
25825
26237
|
stderr: e.message,
|
|
@@ -26197,7 +26609,8 @@ var todoTool = {
|
|
|
26197
26609
|
}
|
|
26198
26610
|
for (const planId of completedPlanIds) {
|
|
26199
26611
|
if (pendingPlanIds.has(planId)) continue;
|
|
26200
|
-
const
|
|
26612
|
+
const meta = ctx.meta;
|
|
26613
|
+
const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
|
|
26201
26614
|
if (typeof planPath !== "string" || !planPath) continue;
|
|
26202
26615
|
try {
|
|
26203
26616
|
const plan = await loadPlan2(planPath);
|
|
@@ -26210,7 +26623,8 @@ var todoTool = {
|
|
|
26210
26623
|
}
|
|
26211
26624
|
for (const taskId of completedTaskIds) {
|
|
26212
26625
|
if (pendingTaskIds.has(taskId)) continue;
|
|
26213
|
-
const
|
|
26626
|
+
const meta = ctx.meta;
|
|
26627
|
+
const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
|
|
26214
26628
|
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
26215
26629
|
try {
|
|
26216
26630
|
const file = await loadTasks3(taskPath);
|
|
@@ -26326,7 +26740,16 @@ var planTool = {
|
|
|
26326
26740
|
sessionPlanPath.lastIndexOf("/"),
|
|
26327
26741
|
sessionPlanPath.lastIndexOf("\\")
|
|
26328
26742
|
);
|
|
26329
|
-
|
|
26743
|
+
if (lastSep < 0) {
|
|
26744
|
+
return {
|
|
26745
|
+
ok: false,
|
|
26746
|
+
message: `Cannot derive the project-scoped plan path: session plan path "${sessionPlanPath}" has no directory component.`,
|
|
26747
|
+
plan: "",
|
|
26748
|
+
count: 0,
|
|
26749
|
+
open: 0
|
|
26750
|
+
};
|
|
26751
|
+
}
|
|
26752
|
+
planPath = sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json";
|
|
26330
26753
|
}
|
|
26331
26754
|
} else {
|
|
26332
26755
|
planPath = sessionPlanPath;
|
|
@@ -26519,6 +26942,7 @@ var planTool = {
|
|
|
26519
26942
|
open: 0
|
|
26520
26943
|
};
|
|
26521
26944
|
}
|
|
26945
|
+
ctx.meta["plan.path.resolved"] = planPath;
|
|
26522
26946
|
await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
|
|
26523
26947
|
if (todosToReplace) {
|
|
26524
26948
|
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
@@ -26551,6 +26975,7 @@ var planTool = {
|
|
|
26551
26975
|
});
|
|
26552
26976
|
return f;
|
|
26553
26977
|
});
|
|
26978
|
+
ctx.meta["task.path.resolved"] = taskPath;
|
|
26554
26979
|
return mkResult(
|
|
26555
26980
|
plan,
|
|
26556
26981
|
true,
|
|
@@ -26584,14 +27009,14 @@ function mkResult(plan, ok, message, todos) {
|
|
|
26584
27009
|
// src/read.ts
|
|
26585
27010
|
init_util();
|
|
26586
27011
|
import * as fs29 from "node:fs/promises";
|
|
26587
|
-
import { FsError, ToolValidationError as
|
|
26588
|
-
import { toErrorMessage as
|
|
27012
|
+
import { FsError, ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
|
|
27013
|
+
import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
|
|
26589
27014
|
var ADVANCED_MODE_META_KEY = "tools.read.advancedMode";
|
|
26590
27015
|
var MAX_BYTES2 = 5 * 1024 * 1024;
|
|
26591
27016
|
var readTool = {
|
|
26592
27017
|
name: "read",
|
|
26593
27018
|
category: "Filesystem",
|
|
26594
|
-
description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed
|
|
27019
|
+
description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed in the form `N\u2192content` (line number, then a `\u2192` separator, then the raw line). The `N\u2192` prefix is display-only \u2014 always strip it before reusing the text, e.g. never include it in `edit.old_string`. When advanced mode is on or `includeSymbols` is set, the result also includes a `symbols` field listing codebase-index symbol names, kinds, and line numbers for the file (not file content).",
|
|
26595
27020
|
usageHint: "FOUNDATIONAL TOOL \u2014 call this before almost any edit operation.\n\nBest practices:\n- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\n- Use `offset` + `limit` for very large files instead of reading everything at once.\n- Default limit is generous (2000 lines) but can be increased.\n- The output format is designed to be directly usable as context for `edit` operations.\n- Set `includeSymbols: true` to also receive the codebase-index symbol listing for the file.\n- Enable advanced mode (`ctx.meta['tools.read.advancedMode'] = true`) to auto-inject symbols on every read.",
|
|
26596
27021
|
selection: {
|
|
26597
27022
|
doNotUseWhen: "you need to search many files for matching content.",
|
|
@@ -26612,11 +27037,13 @@ var readTool = {
|
|
|
26612
27037
|
},
|
|
26613
27038
|
offset: {
|
|
26614
27039
|
type: "integer",
|
|
27040
|
+
minimum: 1,
|
|
26615
27041
|
description: "1-based starting line number. Use together with `limit` for large files."
|
|
26616
27042
|
},
|
|
26617
27043
|
limit: {
|
|
26618
27044
|
type: "integer",
|
|
26619
|
-
|
|
27045
|
+
minimum: 0,
|
|
27046
|
+
description: "Maximum number of lines to return (default 2000). Values above 5000 are clamped to 5000 \u2014 page with `offset` for more."
|
|
26620
27047
|
},
|
|
26621
27048
|
mode: {
|
|
26622
27049
|
type: "string",
|
|
@@ -26632,16 +27059,16 @@ var readTool = {
|
|
|
26632
27059
|
},
|
|
26633
27060
|
async execute(input, ctx, execOpts) {
|
|
26634
27061
|
if (!input?.path) {
|
|
26635
|
-
throw new
|
|
27062
|
+
throw new ToolValidationError8({
|
|
26636
27063
|
message: "read: path is required",
|
|
26637
27064
|
field: "path"
|
|
26638
27065
|
});
|
|
26639
27066
|
}
|
|
26640
27067
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
26641
27068
|
const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
|
|
26642
|
-
let
|
|
27069
|
+
let stat19;
|
|
26643
27070
|
try {
|
|
26644
|
-
|
|
27071
|
+
stat19 = await fs29.stat(absPath);
|
|
26645
27072
|
} catch (err) {
|
|
26646
27073
|
const code = err.code;
|
|
26647
27074
|
if (code === "ENOENT") {
|
|
@@ -26653,14 +27080,14 @@ var readTool = {
|
|
|
26653
27080
|
});
|
|
26654
27081
|
}
|
|
26655
27082
|
throw new FsError({
|
|
26656
|
-
message: `read: failed to stat "${input.path}": ${
|
|
27083
|
+
message: `read: failed to stat "${input.path}": ${toErrorMessage8(err)}`,
|
|
26657
27084
|
code: "FS_READ_FAILED",
|
|
26658
27085
|
path: absPath,
|
|
26659
27086
|
context: { errno: code },
|
|
26660
27087
|
cause: err
|
|
26661
27088
|
});
|
|
26662
27089
|
}
|
|
26663
|
-
if (!
|
|
27090
|
+
if (!stat19.isFile()) {
|
|
26664
27091
|
throw new FsError({
|
|
26665
27092
|
message: `read: "${input.path}" is not a regular file`,
|
|
26666
27093
|
code: "FS_READ_FAILED",
|
|
@@ -26668,23 +27095,23 @@ var readTool = {
|
|
|
26668
27095
|
context: { reason: "not-a-regular-file" }
|
|
26669
27096
|
});
|
|
26670
27097
|
}
|
|
26671
|
-
if (
|
|
27098
|
+
if (stat19.size > MAX_BYTES2) {
|
|
26672
27099
|
throw new FsError({
|
|
26673
|
-
message: `read: file too large (${
|
|
27100
|
+
message: `read: file too large (${stat19.size} bytes, limit ${MAX_BYTES2})`,
|
|
26674
27101
|
code: "FS_READ_FAILED",
|
|
26675
27102
|
path: absPath,
|
|
26676
|
-
context: { size:
|
|
27103
|
+
context: { size: stat19.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
26677
27104
|
});
|
|
26678
27105
|
}
|
|
26679
27106
|
const offset = Math.max(1, input.offset ?? 1);
|
|
26680
27107
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
26681
27108
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
26682
27109
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
26683
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
26684
|
-
ctx.recordRead(absPath,
|
|
27110
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat19.mtimeMs, offset, requestedEnd)) {
|
|
27111
|
+
ctx.recordRead(absPath, stat19.mtimeMs);
|
|
26685
27112
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
26686
27113
|
return {
|
|
26687
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
27114
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat19.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
26688
27115
|
total_lines: prior.totalLines,
|
|
26689
27116
|
encoding: "utf8",
|
|
26690
27117
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -26695,18 +27122,23 @@ var readTool = {
|
|
|
26695
27122
|
}
|
|
26696
27123
|
const buf = await fs29.readFile(absPath);
|
|
26697
27124
|
if (isBinaryBuffer(buf)) {
|
|
26698
|
-
throw new
|
|
27125
|
+
throw new FsError({
|
|
27126
|
+
message: `read: "${input.path}" appears to be binary`,
|
|
27127
|
+
code: "FS_READ_FAILED",
|
|
27128
|
+
path: absPath,
|
|
27129
|
+
context: { reason: "binary" }
|
|
27130
|
+
});
|
|
26699
27131
|
}
|
|
26700
27132
|
const text = buf.toString("utf8");
|
|
26701
27133
|
const contentHash = sha256hex(text);
|
|
26702
27134
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
26703
27135
|
const total = allLines.length;
|
|
26704
27136
|
if (input.mode === "summary") {
|
|
26705
|
-
ctx.recordRead(absPath,
|
|
26706
|
-
rememberReadRange(ctx, absPath,
|
|
27137
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
27138
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, Math.min(total, 200));
|
|
26707
27139
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
26708
27140
|
return {
|
|
26709
|
-
text: summarizeFile(input.path,
|
|
27141
|
+
text: summarizeFile(input.path, stat19.size, allLines),
|
|
26710
27142
|
total_lines: total,
|
|
26711
27143
|
encoding: "utf8",
|
|
26712
27144
|
truncated: total > 200,
|
|
@@ -26718,8 +27150,8 @@ var readTool = {
|
|
|
26718
27150
|
};
|
|
26719
27151
|
}
|
|
26720
27152
|
if (limit === 0) {
|
|
26721
|
-
ctx.recordRead(absPath,
|
|
26722
|
-
rememberReadRange(ctx, absPath,
|
|
27153
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
27154
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, 0);
|
|
26723
27155
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
26724
27156
|
return {
|
|
26725
27157
|
text: "",
|
|
@@ -26731,8 +27163,8 @@ var readTool = {
|
|
|
26731
27163
|
};
|
|
26732
27164
|
}
|
|
26733
27165
|
if (offset > total) {
|
|
26734
|
-
ctx.recordRead(absPath,
|
|
26735
|
-
rememberReadRange(ctx, absPath,
|
|
27166
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
27167
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, total + 1, total + 1);
|
|
26736
27168
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
26737
27169
|
return {
|
|
26738
27170
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
@@ -26747,8 +27179,8 @@ var readTool = {
|
|
|
26747
27179
|
const truncated = offset - 1 + slice.length < total;
|
|
26748
27180
|
const width = String(offset + slice.length - 1).length;
|
|
26749
27181
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
26750
|
-
ctx.recordRead(absPath,
|
|
26751
|
-
rememberReadRange(ctx, absPath,
|
|
27182
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
27183
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, offset, offset + slice.length - 1);
|
|
26752
27184
|
const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
26753
27185
|
return {
|
|
26754
27186
|
text: numbered,
|
|
@@ -26856,8 +27288,8 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
26856
27288
|
// src/replace.ts
|
|
26857
27289
|
import { spawn as spawn14 } from "node:child_process";
|
|
26858
27290
|
import * as fs30 from "node:fs/promises";
|
|
26859
|
-
import * as
|
|
26860
|
-
import { ToolValidationError as
|
|
27291
|
+
import * as path35 from "node:path";
|
|
27292
|
+
import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
|
|
26861
27293
|
import {
|
|
26862
27294
|
atomicWrite as atomicWrite3,
|
|
26863
27295
|
buildChildEnv as buildChildEnv9,
|
|
@@ -26869,12 +27301,13 @@ import {
|
|
|
26869
27301
|
unifiedDiff as unifiedDiff2
|
|
26870
27302
|
} from "@wrongstack/core/utils";
|
|
26871
27303
|
init_util();
|
|
27304
|
+
var MAX_DIFF_BYTES2 = 262144;
|
|
26872
27305
|
var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
26873
27306
|
var replaceTool = {
|
|
26874
27307
|
name: "replace",
|
|
26875
27308
|
category: "Transform",
|
|
26876
27309
|
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.",
|
|
26877
|
-
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.",
|
|
27310
|
+
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.\n5. `replacement` supports regex substitutions: `$1`\u2013`$9` insert capture groups, `$&` inserts the whole match, and `$$` inserts a literal dollar sign.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
|
|
26878
27311
|
permission: "confirm",
|
|
26879
27312
|
// WS-046: gives permission decisions something to key on.
|
|
26880
27313
|
// The file scope being rewritten, not the pattern: a trust rule should say
|
|
@@ -26884,11 +27317,15 @@ var replaceTool = {
|
|
|
26884
27317
|
capabilities: ["fs.write"],
|
|
26885
27318
|
icon: "edit",
|
|
26886
27319
|
timeoutMs: 3e4,
|
|
27320
|
+
maxOutputBytes: 262144,
|
|
26887
27321
|
inputSchema: {
|
|
26888
27322
|
type: "object",
|
|
26889
27323
|
properties: {
|
|
26890
27324
|
pattern: { type: "string", description: "Regex pattern to match" },
|
|
26891
|
-
replacement: {
|
|
27325
|
+
replacement: {
|
|
27326
|
+
type: "string",
|
|
27327
|
+
description: "Replacement string. Supports `$1`\u2013`$9` (capture groups), `$&` (whole match), and `$$` (literal dollar sign) \u2014 same semantics as JavaScript String.replace."
|
|
27328
|
+
},
|
|
26892
27329
|
files: {
|
|
26893
27330
|
type: "string",
|
|
26894
27331
|
description: "File(s) to target: single path, comma-separated list, or glob pattern"
|
|
@@ -26904,19 +27341,19 @@ var replaceTool = {
|
|
|
26904
27341
|
},
|
|
26905
27342
|
async execute(input, ctx) {
|
|
26906
27343
|
if (!input?.pattern) {
|
|
26907
|
-
throw new
|
|
27344
|
+
throw new ToolValidationError9({
|
|
26908
27345
|
message: "replace: pattern is required",
|
|
26909
27346
|
field: "pattern"
|
|
26910
27347
|
});
|
|
26911
27348
|
}
|
|
26912
27349
|
if (input.replacement === void 0) {
|
|
26913
|
-
throw new
|
|
27350
|
+
throw new ToolValidationError9({
|
|
26914
27351
|
message: "replace: replacement is required",
|
|
26915
27352
|
field: "replacement"
|
|
26916
27353
|
});
|
|
26917
27354
|
}
|
|
26918
27355
|
if (!input?.files) {
|
|
26919
|
-
throw new
|
|
27356
|
+
throw new ToolValidationError9({
|
|
26920
27357
|
message: "replace: files is required",
|
|
26921
27358
|
field: "files"
|
|
26922
27359
|
});
|
|
@@ -26924,7 +27361,7 @@ var replaceTool = {
|
|
|
26924
27361
|
const replaceAll = input.replace_all ?? true;
|
|
26925
27362
|
const compiled = compileUserRegex(input.pattern, "g");
|
|
26926
27363
|
if (!compiled.ok) {
|
|
26927
|
-
throw new
|
|
27364
|
+
throw new ToolValidationError9({
|
|
26928
27365
|
message: `replace: ${compiled.reason}`,
|
|
26929
27366
|
field: "pattern"
|
|
26930
27367
|
});
|
|
@@ -26937,6 +27374,9 @@ var replaceTool = {
|
|
|
26937
27374
|
const realRoot = await fs30.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
26938
27375
|
const results = [];
|
|
26939
27376
|
let totalReplacements = 0;
|
|
27377
|
+
let diffBytesUsed = 0;
|
|
27378
|
+
let diffsOmitted = 0;
|
|
27379
|
+
let diffsTruncated = 0;
|
|
26940
27380
|
for (const absPath of fileList) {
|
|
26941
27381
|
const lstat2 = await fs30.lstat(absPath).catch((err) => {
|
|
26942
27382
|
if (err.code === "ENOENT") return null;
|
|
@@ -26950,10 +27390,10 @@ var replaceTool = {
|
|
|
26950
27390
|
} catch {
|
|
26951
27391
|
continue;
|
|
26952
27392
|
}
|
|
26953
|
-
const rel =
|
|
26954
|
-
if (rel.startsWith("..") ||
|
|
26955
|
-
const
|
|
26956
|
-
if (!
|
|
27393
|
+
const rel = path35.relative(realRoot, realPath);
|
|
27394
|
+
if (rel.startsWith("..") || path35.isAbsolute(rel)) continue;
|
|
27395
|
+
const stat19 = await fs30.stat(realPath).catch(() => null);
|
|
27396
|
+
if (!stat19?.isFile()) continue;
|
|
26957
27397
|
let content;
|
|
26958
27398
|
try {
|
|
26959
27399
|
const buf = await fs30.readFile(realPath);
|
|
@@ -26972,13 +27412,13 @@ var replaceTool = {
|
|
|
26972
27412
|
let newContentLf = contentLf;
|
|
26973
27413
|
for (let i = matches.length - 1; i >= 0; i--) {
|
|
26974
27414
|
const m = expectDefined8(matches[i]);
|
|
26975
|
-
newContentLf = newContentLf.slice(0, m.index) + input.replacement + newContentLf.slice(expectDefined8(m.index) + m[0].length);
|
|
27415
|
+
newContentLf = newContentLf.slice(0, m.index) + expandReplacement(input.replacement, m) + newContentLf.slice(expectDefined8(m.index) + m[0].length);
|
|
26976
27416
|
}
|
|
26977
27417
|
re.lastIndex = 0;
|
|
26978
27418
|
totalReplacements += count;
|
|
26979
27419
|
if (!dryRun) {
|
|
26980
27420
|
const newContent = toStyle2(newContentLf, style);
|
|
26981
|
-
await atomicWrite3(realPath, newContent, { mode:
|
|
27421
|
+
await atomicWrite3(realPath, newContent, { mode: stat19.mode & 511 });
|
|
26982
27422
|
const written = await fs30.stat(realPath).catch(() => null);
|
|
26983
27423
|
if (written) {
|
|
26984
27424
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
@@ -26990,24 +27430,76 @@ var replaceTool = {
|
|
|
26990
27430
|
after: newContent
|
|
26991
27431
|
});
|
|
26992
27432
|
}
|
|
26993
|
-
|
|
27433
|
+
let diff = dryRun || matches.length > 0 ? unifiedDiff2(content, toStyle2(newContentLf, style), {
|
|
26994
27434
|
fromFile: absPath,
|
|
26995
27435
|
toFile: absPath
|
|
26996
27436
|
}) : void 0;
|
|
27437
|
+
if (diff !== void 0) {
|
|
27438
|
+
const remaining = MAX_DIFF_BYTES2 - diffBytesUsed;
|
|
27439
|
+
if (remaining <= 0) {
|
|
27440
|
+
diff = void 0;
|
|
27441
|
+
diffsOmitted++;
|
|
27442
|
+
} else {
|
|
27443
|
+
const capped = truncateDiffPayload(diff, remaining);
|
|
27444
|
+
if (capped.truncated) diffsTruncated++;
|
|
27445
|
+
diff = capped.text;
|
|
27446
|
+
diffBytesUsed += Buffer.byteLength(diff, "utf8");
|
|
27447
|
+
}
|
|
27448
|
+
}
|
|
26997
27449
|
results.push({
|
|
26998
27450
|
path: absPath,
|
|
26999
27451
|
replacements: matches.length,
|
|
27000
27452
|
diff
|
|
27001
27453
|
});
|
|
27002
27454
|
}
|
|
27455
|
+
const overBudget = diffsOmitted > 0 || diffsTruncated > 0;
|
|
27003
27456
|
return {
|
|
27004
27457
|
files_modified: results.length,
|
|
27005
27458
|
total_replacements: totalReplacements,
|
|
27006
27459
|
results,
|
|
27007
|
-
dry_run: dryRun
|
|
27460
|
+
dry_run: dryRun,
|
|
27461
|
+
note: overBudget ? `Diff payload exceeded the 256 KiB output budget: ${diffsTruncated} diff(s) truncated, ${diffsOmitted} diff(s) omitted. Replacement counts are complete; use the read tool to inspect individual files.` : void 0
|
|
27008
27462
|
};
|
|
27009
27463
|
}
|
|
27010
27464
|
};
|
|
27465
|
+
function expandReplacement(template, match) {
|
|
27466
|
+
if (!template.includes("$")) return template;
|
|
27467
|
+
let out = "";
|
|
27468
|
+
for (let i = 0; i < template.length; i++) {
|
|
27469
|
+
const ch = template[i];
|
|
27470
|
+
if (ch !== "$") {
|
|
27471
|
+
out += ch;
|
|
27472
|
+
continue;
|
|
27473
|
+
}
|
|
27474
|
+
const next = template[i + 1];
|
|
27475
|
+
if (next === "$") {
|
|
27476
|
+
out += "$";
|
|
27477
|
+
i++;
|
|
27478
|
+
} else if (next === "&") {
|
|
27479
|
+
out += match[0];
|
|
27480
|
+
i++;
|
|
27481
|
+
} else if (next !== void 0 && next >= "1" && next <= "9") {
|
|
27482
|
+
const idx = next.charCodeAt(0) - 48;
|
|
27483
|
+
if (idx < match.length) {
|
|
27484
|
+
out += match[idx] ?? "";
|
|
27485
|
+
i++;
|
|
27486
|
+
} else {
|
|
27487
|
+
out += "$";
|
|
27488
|
+
}
|
|
27489
|
+
} else {
|
|
27490
|
+
out += "$";
|
|
27491
|
+
}
|
|
27492
|
+
}
|
|
27493
|
+
return out;
|
|
27494
|
+
}
|
|
27495
|
+
function passesExtraGlob(extraGlob, name, full) {
|
|
27496
|
+
extraGlob.lastIndex = 0;
|
|
27497
|
+
const nameMatch = extraGlob.test(name);
|
|
27498
|
+
extraGlob.lastIndex = 0;
|
|
27499
|
+
const fullMatch = extraGlob.test(full);
|
|
27500
|
+
extraGlob.lastIndex = 0;
|
|
27501
|
+
return nameMatch || fullMatch;
|
|
27502
|
+
}
|
|
27011
27503
|
async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
27012
27504
|
const base = ctx.cwd;
|
|
27013
27505
|
const normalized = filesInput.trim();
|
|
@@ -27018,8 +27510,9 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
27018
27510
|
const resolved = [];
|
|
27019
27511
|
for (const p of parts) {
|
|
27020
27512
|
const absPath = safeResolve(p, ctx);
|
|
27021
|
-
|
|
27022
|
-
|
|
27513
|
+
if (extraGlob && !passesExtraGlob(extraGlob, path35.basename(absPath), absPath)) continue;
|
|
27514
|
+
const stat19 = await fs30.stat(absPath).catch(() => null);
|
|
27515
|
+
if (stat19?.isFile()) {
|
|
27023
27516
|
resolved.push(absPath);
|
|
27024
27517
|
}
|
|
27025
27518
|
}
|
|
@@ -27030,26 +27523,32 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
27030
27523
|
if (rgAvailable) {
|
|
27031
27524
|
try {
|
|
27032
27525
|
const { promise } = spawnRgFind(pattern, base);
|
|
27033
|
-
|
|
27526
|
+
const files = await promise;
|
|
27527
|
+
if (extraGlob) {
|
|
27528
|
+
return files.filter((f) => passesExtraGlob(extraGlob, path35.basename(f), f));
|
|
27529
|
+
}
|
|
27530
|
+
return files;
|
|
27034
27531
|
} catch {
|
|
27035
27532
|
}
|
|
27036
27533
|
}
|
|
27037
27534
|
return await globNative(pattern, base, extraGlob);
|
|
27038
27535
|
}
|
|
27536
|
+
var rgAvailabilityCache2;
|
|
27039
27537
|
function checkRg() {
|
|
27040
|
-
|
|
27538
|
+
rgAvailabilityCache2 ??= new Promise((resolve17) => {
|
|
27041
27539
|
try {
|
|
27042
27540
|
const p = spawn14("rg", ["--version"], {
|
|
27043
27541
|
env: buildChildEnv9(),
|
|
27044
27542
|
stdio: "ignore",
|
|
27045
27543
|
windowsHide: true
|
|
27046
27544
|
});
|
|
27047
|
-
p.on("error", () =>
|
|
27048
|
-
p.on("close", (code) =>
|
|
27545
|
+
p.on("error", () => resolve17(false));
|
|
27546
|
+
p.on("close", (code) => resolve17(code === 0));
|
|
27049
27547
|
} catch {
|
|
27050
|
-
|
|
27548
|
+
resolve17(false);
|
|
27051
27549
|
}
|
|
27052
27550
|
});
|
|
27551
|
+
return rgAvailabilityCache2;
|
|
27053
27552
|
}
|
|
27054
27553
|
function spawnRgFind(pattern, base) {
|
|
27055
27554
|
const args = ["--files", "--glob", pattern, base];
|
|
@@ -27072,10 +27571,10 @@ function spawnRgFind(pattern, base) {
|
|
|
27072
27571
|
}
|
|
27073
27572
|
});
|
|
27074
27573
|
return {
|
|
27075
|
-
promise: new Promise((
|
|
27574
|
+
promise: new Promise((resolve17, reject) => {
|
|
27076
27575
|
child.on("error", reject);
|
|
27077
27576
|
child.on("close", () => {
|
|
27078
|
-
|
|
27577
|
+
resolve17(buf.split("\n").filter(Boolean));
|
|
27079
27578
|
});
|
|
27080
27579
|
})
|
|
27081
27580
|
};
|
|
@@ -27092,10 +27591,10 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
27092
27591
|
}
|
|
27093
27592
|
for (const e of entries) {
|
|
27094
27593
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
27095
|
-
const full =
|
|
27594
|
+
const full = path35.join(dir, e.name);
|
|
27096
27595
|
try {
|
|
27097
|
-
const
|
|
27098
|
-
if (
|
|
27596
|
+
const stat19 = await fs30.lstat(full);
|
|
27597
|
+
if (stat19.isSymbolicLink()) continue;
|
|
27099
27598
|
} catch {
|
|
27100
27599
|
continue;
|
|
27101
27600
|
}
|
|
@@ -27119,7 +27618,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
27119
27618
|
// src/scaffold.ts
|
|
27120
27619
|
init_util();
|
|
27121
27620
|
import * as fs31 from "node:fs/promises";
|
|
27122
|
-
import * as
|
|
27621
|
+
import * as path36 from "node:path";
|
|
27123
27622
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
|
|
27124
27623
|
var BUILT_IN_TEMPLATES = {
|
|
27125
27624
|
"npm-package": {
|
|
@@ -27270,16 +27769,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
27270
27769
|
let filesCreated = 0;
|
|
27271
27770
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
27272
27771
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
27273
|
-
const joinedPath =
|
|
27274
|
-
const root =
|
|
27275
|
-
const target =
|
|
27276
|
-
const rel =
|
|
27277
|
-
if (rel.startsWith("..") ||
|
|
27772
|
+
const joinedPath = path36.join(cwd, resolvedPath);
|
|
27773
|
+
const root = path36.resolve(ctx.projectRoot);
|
|
27774
|
+
const target = path36.resolve(joinedPath);
|
|
27775
|
+
const rel = path36.relative(root, target);
|
|
27776
|
+
if (rel.startsWith("..") || path36.isAbsolute(rel)) {
|
|
27278
27777
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
27279
27778
|
}
|
|
27280
27779
|
const fullPath = target;
|
|
27281
27780
|
if (!dryRun) {
|
|
27282
|
-
await fs31.mkdir(
|
|
27781
|
+
await fs31.mkdir(path36.dirname(fullPath), { recursive: true });
|
|
27283
27782
|
await atomicWrite4(fullPath, substituteVars(content, name, vars));
|
|
27284
27783
|
}
|
|
27285
27784
|
files.push(resolvedPath);
|
|
@@ -27310,11 +27809,12 @@ function substituteVars(content, name, vars) {
|
|
|
27310
27809
|
}
|
|
27311
27810
|
|
|
27312
27811
|
// src/search.ts
|
|
27313
|
-
import { FetchError as FetchError3, ToolValidationError as
|
|
27812
|
+
import { FetchError as FetchError3, ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
|
|
27314
27813
|
import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
|
|
27315
|
-
import { toErrorMessage as
|
|
27814
|
+
import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
|
|
27316
27815
|
var DEFAULT_NUM = 10;
|
|
27317
27816
|
var MAX_RESULTS = 50;
|
|
27817
|
+
var MAX_SNIPPET_CHARS = 300;
|
|
27318
27818
|
var TIMEOUT_MS3 = 15e3;
|
|
27319
27819
|
var CACHE_TTL_MS = 3e5;
|
|
27320
27820
|
var CACHE_MAX_ENTRIES = 200;
|
|
@@ -27322,7 +27822,7 @@ var cache = /* @__PURE__ */ new Map();
|
|
|
27322
27822
|
var searchTool = {
|
|
27323
27823
|
name: "search",
|
|
27324
27824
|
category: "Search",
|
|
27325
|
-
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.",
|
|
27825
|
+
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. google and bing are best-effort HTML scrapes that fall back to duckduckgo when they return nothing usable.",
|
|
27326
27826
|
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.",
|
|
27327
27827
|
permission: "auto",
|
|
27328
27828
|
mutating: false,
|
|
@@ -27363,7 +27863,7 @@ var searchTool = {
|
|
|
27363
27863
|
},
|
|
27364
27864
|
async *executeStream(input, _ctx, opts) {
|
|
27365
27865
|
if (!input?.query || input.query.trim() === "") {
|
|
27366
|
-
throw new
|
|
27866
|
+
throw new ToolValidationError10({
|
|
27367
27867
|
message: "search: query is required and must be a non-empty string",
|
|
27368
27868
|
field: "query"
|
|
27369
27869
|
});
|
|
@@ -27396,7 +27896,7 @@ var searchTool = {
|
|
|
27396
27896
|
query: input.query,
|
|
27397
27897
|
results: results.slice(0, num),
|
|
27398
27898
|
source: entry.source,
|
|
27399
|
-
truncated: results.length
|
|
27899
|
+
truncated: results.length > num,
|
|
27400
27900
|
cached: true
|
|
27401
27901
|
}
|
|
27402
27902
|
};
|
|
@@ -27408,41 +27908,45 @@ var searchTool = {
|
|
|
27408
27908
|
text: `Querying ${source} for "${input.query}"\u2026`,
|
|
27409
27909
|
data: { source, query: input.query, cached: false }
|
|
27410
27910
|
};
|
|
27411
|
-
let
|
|
27911
|
+
let engine;
|
|
27412
27912
|
let effectiveSource = source;
|
|
27413
27913
|
switch (source) {
|
|
27414
27914
|
case "duckduckgo":
|
|
27415
|
-
|
|
27915
|
+
engine = await duckduckgoSearch(input.query, opts.signal);
|
|
27416
27916
|
break;
|
|
27417
27917
|
case "google":
|
|
27418
|
-
|
|
27918
|
+
engine = await googleSearch(input.query, opts.signal);
|
|
27419
27919
|
break;
|
|
27420
27920
|
case "bing":
|
|
27421
|
-
|
|
27921
|
+
engine = await bingSearch(input.query, opts.signal);
|
|
27422
27922
|
break;
|
|
27423
27923
|
default:
|
|
27424
|
-
throw new
|
|
27924
|
+
throw new ToolValidationError10({
|
|
27425
27925
|
message: `search: unknown source "${source}"`,
|
|
27426
27926
|
field: "source"
|
|
27427
27927
|
});
|
|
27428
27928
|
}
|
|
27429
|
-
let ranked = rankSearchResults(
|
|
27929
|
+
let ranked = rankSearchResults(engine.results, input.query);
|
|
27930
|
+
let engineError = engine.error;
|
|
27430
27931
|
if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
|
|
27431
27932
|
yield {
|
|
27432
27933
|
type: "log",
|
|
27433
27934
|
text: `${source} returned no relevant static results; falling back to duckduckgo`,
|
|
27434
27935
|
data: { source, fallback: "duckduckgo", query: input.query }
|
|
27435
27936
|
};
|
|
27436
|
-
|
|
27437
|
-
ranked = rankSearchResults(
|
|
27937
|
+
const fallback = await duckduckgoSearch(input.query, opts.signal);
|
|
27938
|
+
ranked = rankSearchResults(fallback.results, input.query);
|
|
27939
|
+
engineError = fallback.error;
|
|
27438
27940
|
effectiveSource = "duckduckgo";
|
|
27439
27941
|
}
|
|
27440
27942
|
const finalResults = ranked.slice(0, num);
|
|
27441
|
-
|
|
27442
|
-
|
|
27943
|
+
if (!engineError) {
|
|
27944
|
+
cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
|
|
27945
|
+
pruneCacheEntries();
|
|
27946
|
+
}
|
|
27443
27947
|
yield {
|
|
27444
27948
|
type: "partial_output",
|
|
27445
|
-
text: `${finalResults.length} results from ${effectiveSource}`,
|
|
27949
|
+
text: engineError ? `search failed: ${engineError}` : `${finalResults.length} results from ${effectiveSource}`,
|
|
27446
27950
|
data: { count: finalResults.length, cached: false, source: effectiveSource }
|
|
27447
27951
|
};
|
|
27448
27952
|
yield {
|
|
@@ -27455,8 +27959,9 @@ var searchTool = {
|
|
|
27455
27959
|
snippet: r.snippet
|
|
27456
27960
|
})),
|
|
27457
27961
|
source: effectiveSource,
|
|
27458
|
-
truncated:
|
|
27459
|
-
cached: false
|
|
27962
|
+
truncated: ranked.length > num,
|
|
27963
|
+
cached: false,
|
|
27964
|
+
...engineError ? { error: engineError } : {}
|
|
27460
27965
|
}
|
|
27461
27966
|
};
|
|
27462
27967
|
}
|
|
@@ -27507,18 +28012,18 @@ function shouldFallbackToDuckDuckGo(results, query) {
|
|
|
27507
28012
|
return terms.some((term) => haystack.includes(term));
|
|
27508
28013
|
});
|
|
27509
28014
|
}
|
|
27510
|
-
async function duckduckgoSearch(query,
|
|
28015
|
+
async function duckduckgoSearch(query, signal) {
|
|
27511
28016
|
const encoded = encodeURIComponent(query);
|
|
27512
28017
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
27513
28018
|
try {
|
|
27514
28019
|
const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
|
|
27515
28020
|
const html = await response.text();
|
|
27516
|
-
return parseDuckDuckGo(html,
|
|
28021
|
+
return { results: parseDuckDuckGo(html, MAX_RESULTS) };
|
|
27517
28022
|
} catch (err) {
|
|
27518
28023
|
console.log(
|
|
27519
|
-
JSON.stringify({ level: "debug", event: "search_failed", query, error:
|
|
28024
|
+
JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage9(err) })
|
|
27520
28025
|
);
|
|
27521
|
-
return
|
|
28026
|
+
return { results: [], error: `duckduckgo unreachable: ${toErrorMessage9(err)}` };
|
|
27522
28027
|
}
|
|
27523
28028
|
}
|
|
27524
28029
|
function takeFrom(iter, max) {
|
|
@@ -27552,7 +28057,7 @@ function parseDuckDuckGo(html, num) {
|
|
|
27552
28057
|
results.push({
|
|
27553
28058
|
title: entry.title ?? "",
|
|
27554
28059
|
url: entry.url ?? "",
|
|
27555
|
-
snippet: snippetMatches[i] ?? "",
|
|
28060
|
+
snippet: capSnippet(snippetMatches[i] ?? ""),
|
|
27556
28061
|
score: 1
|
|
27557
28062
|
});
|
|
27558
28063
|
}
|
|
@@ -27577,11 +28082,15 @@ function normalizeDuckDuckGoUrl(raw) {
|
|
|
27577
28082
|
return raw;
|
|
27578
28083
|
}
|
|
27579
28084
|
}
|
|
27580
|
-
async function googleSearch(query,
|
|
28085
|
+
async function googleSearch(query, signal) {
|
|
27581
28086
|
const encoded = encodeURIComponent(query);
|
|
27582
28087
|
const url = `https://www.google.com/search?q=${encoded}&hl=en`;
|
|
27583
|
-
|
|
27584
|
-
|
|
28088
|
+
try {
|
|
28089
|
+
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
|
|
28090
|
+
return { results: parseGoogleResults(html, MAX_RESULTS) };
|
|
28091
|
+
} catch (err) {
|
|
28092
|
+
return { results: [], error: `google unreachable: ${toErrorMessage9(err)}` };
|
|
28093
|
+
}
|
|
27585
28094
|
}
|
|
27586
28095
|
function parseGoogleResults(html, num) {
|
|
27587
28096
|
const results = [];
|
|
@@ -27604,17 +28113,21 @@ function parseGoogleResults(html, num) {
|
|
|
27604
28113
|
results.push({
|
|
27605
28114
|
title: titles[i] ?? "",
|
|
27606
28115
|
url: urls[i] ?? "",
|
|
27607
|
-
snippet: snippets[i] ?? "",
|
|
28116
|
+
snippet: capSnippet(snippets[i] ?? ""),
|
|
27608
28117
|
score: 1
|
|
27609
28118
|
});
|
|
27610
28119
|
}
|
|
27611
28120
|
return results;
|
|
27612
28121
|
}
|
|
27613
|
-
async function bingSearch(query,
|
|
28122
|
+
async function bingSearch(query, signal) {
|
|
27614
28123
|
const encoded = encodeURIComponent(query);
|
|
27615
28124
|
const url = `https://www.bing.com/search?q=${encoded}`;
|
|
27616
|
-
|
|
27617
|
-
|
|
28125
|
+
try {
|
|
28126
|
+
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
|
|
28127
|
+
return { results: parseBingResults(html, MAX_RESULTS) };
|
|
28128
|
+
} catch (err) {
|
|
28129
|
+
return { results: [], error: `bing unreachable: ${toErrorMessage9(err)}` };
|
|
28130
|
+
}
|
|
27618
28131
|
}
|
|
27619
28132
|
function parseBingResults(html, num) {
|
|
27620
28133
|
const results = [];
|
|
@@ -27627,7 +28140,7 @@ function parseBingResults(html, num) {
|
|
|
27627
28140
|
const title = stripTags(expectDefined9(titleMatch[2]));
|
|
27628
28141
|
if (!href || !title) return [];
|
|
27629
28142
|
const snippetMatch = /<p\b[^>]*class=(["'])[^"']*\b(?:b_paractl|b_lineclamp\d*)\b[^"']*\1[^>]*>([\s\S]*?)<\/p>/i.exec(block) ?? /<p\b[^>]*>([\s\S]*?)<\/p>/i.exec(block);
|
|
27630
|
-
const snippet = snippetMatch ? stripTags(expectDefined9(snippetMatch.at(-1))) : "";
|
|
28143
|
+
const snippet = snippetMatch ? capSnippet(stripTags(expectDefined9(snippetMatch.at(-1)))) : "";
|
|
27631
28144
|
return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
|
|
27632
28145
|
}), num);
|
|
27633
28146
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -27694,17 +28207,20 @@ function anySignal(...signals) {
|
|
|
27694
28207
|
function stripTags(html) {
|
|
27695
28208
|
return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
|
|
27696
28209
|
}
|
|
28210
|
+
function capSnippet(snippet) {
|
|
28211
|
+
return snippet.length > MAX_SNIPPET_CHARS ? `${snippet.slice(0, MAX_SNIPPET_CHARS - 1)}\u2026` : snippet;
|
|
28212
|
+
}
|
|
27697
28213
|
function decodeHtmlEntities(text) {
|
|
27698
28214
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
27699
28215
|
}
|
|
27700
28216
|
|
|
27701
28217
|
// src/set-working-dir.ts
|
|
27702
28218
|
import * as fs32 from "node:fs/promises";
|
|
27703
|
-
import { toErrorMessage as
|
|
28219
|
+
import { toErrorMessage as toErrorMessage10 } from "@wrongstack/core/utils";
|
|
27704
28220
|
var setWorkingDirTool = {
|
|
27705
28221
|
name: "set_working_dir",
|
|
27706
28222
|
category: "Context",
|
|
27707
|
-
description: "Change the current working directory for
|
|
28223
|
+
description: "Change the current working directory for subsequent file operations and shell tools (`bash`/`exec` spawn in this directory unless given an explicit cwd). The new directory must be inside the project root. Use this to navigate between subdirectories when working on files in different parts of the project.",
|
|
27708
28224
|
usageHint: "Change the working directory so relative paths in subsequent tool calls resolve from a different directory. Pass `path` to set a new directory, or omit to query the current one. The directory must exist and be inside the project root.",
|
|
27709
28225
|
permission: "confirm",
|
|
27710
28226
|
mutating: true,
|
|
@@ -27734,19 +28250,23 @@ var setWorkingDirTool = {
|
|
|
27734
28250
|
} catch (err) {
|
|
27735
28251
|
return {
|
|
27736
28252
|
current: ctx.workingDir,
|
|
27737
|
-
error:
|
|
28253
|
+
error: toErrorMessage10(err)
|
|
27738
28254
|
};
|
|
27739
28255
|
}
|
|
28256
|
+
let isDirectory = false;
|
|
27740
28257
|
try {
|
|
27741
|
-
await fs32.
|
|
28258
|
+
isDirectory = (await fs32.stat(resolved)).isDirectory();
|
|
27742
28259
|
} catch {
|
|
28260
|
+
isDirectory = false;
|
|
28261
|
+
}
|
|
28262
|
+
if (!isDirectory) {
|
|
27743
28263
|
try {
|
|
27744
28264
|
ctx.setWorkingDir(previous);
|
|
27745
28265
|
} catch {
|
|
27746
28266
|
}
|
|
27747
28267
|
return {
|
|
27748
28268
|
current: ctx.workingDir,
|
|
27749
|
-
error: `Directory does not exist: ${resolved}`
|
|
28269
|
+
error: `Directory does not exist (or is not a directory): ${resolved}`
|
|
27750
28270
|
};
|
|
27751
28271
|
}
|
|
27752
28272
|
return {
|
|
@@ -28201,6 +28721,7 @@ var taskTool = {
|
|
|
28201
28721
|
inProgress: 0
|
|
28202
28722
|
};
|
|
28203
28723
|
}
|
|
28724
|
+
ctx.meta["task.path.resolved"] = taskPath;
|
|
28204
28725
|
if (todosToReplace) {
|
|
28205
28726
|
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
28206
28727
|
signal: AbortSignal.timeout(3e4)
|
|
@@ -28225,6 +28746,7 @@ var taskTool = {
|
|
|
28225
28746
|
formatted = formatPlan2(updated);
|
|
28226
28747
|
return updated;
|
|
28227
28748
|
});
|
|
28749
|
+
ctx.meta["plan.path.resolved"] = planPath;
|
|
28228
28750
|
} catch (err) {
|
|
28229
28751
|
return {
|
|
28230
28752
|
ok: false,
|
|
@@ -28268,7 +28790,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
|
|
|
28268
28790
|
init_spawn_stream();
|
|
28269
28791
|
init_util();
|
|
28270
28792
|
init_legacy_bridge();
|
|
28271
|
-
import * as
|
|
28793
|
+
import * as path37 from "node:path";
|
|
28272
28794
|
var testTool = {
|
|
28273
28795
|
name: "test",
|
|
28274
28796
|
category: "Code Quality",
|
|
@@ -28371,11 +28893,11 @@ var testTool = {
|
|
|
28371
28893
|
}
|
|
28372
28894
|
};
|
|
28373
28895
|
async function detectRunner(cwd) {
|
|
28374
|
-
const { stat:
|
|
28896
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
28375
28897
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
28376
28898
|
for (const f of candidates) {
|
|
28377
28899
|
try {
|
|
28378
|
-
await
|
|
28900
|
+
await stat19(path37.join(cwd, f));
|
|
28379
28901
|
if (f.includes("vitest")) return "vitest";
|
|
28380
28902
|
if (f.includes("jest")) return "jest";
|
|
28381
28903
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -28753,7 +29275,7 @@ var toolUseTool = {
|
|
|
28753
29275
|
// src/tree.ts
|
|
28754
29276
|
init_util();
|
|
28755
29277
|
import * as fs33 from "node:fs/promises";
|
|
28756
|
-
import * as
|
|
29278
|
+
import * as path38 from "node:path";
|
|
28757
29279
|
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
28758
29280
|
var DEFAULT_IGNORE5 = /* @__PURE__ */ new Set([
|
|
28759
29281
|
...DEFAULT_WALK_IGNORE_DIRS4,
|
|
@@ -28773,6 +29295,7 @@ var treeTool = {
|
|
|
28773
29295
|
mutating: false,
|
|
28774
29296
|
capabilities: ["fs.read"],
|
|
28775
29297
|
icon: "tree",
|
|
29298
|
+
maxOutputBytes: 262144,
|
|
28776
29299
|
timeoutMs: 15e3,
|
|
28777
29300
|
inputSchema: {
|
|
28778
29301
|
type: "object",
|
|
@@ -28921,17 +29444,15 @@ async function walkDir(dir, depth, opts) {
|
|
|
28921
29444
|
if (opts.exclude.has(e.name)) return false;
|
|
28922
29445
|
return true;
|
|
28923
29446
|
});
|
|
28924
|
-
|
|
28925
|
-
|
|
28926
|
-
|
|
28927
|
-
|
|
28928
|
-
|
|
28929
|
-
|
|
28930
|
-
|
|
28931
|
-
|
|
28932
|
-
|
|
28933
|
-
opts.onProgress?.();
|
|
28934
|
-
}
|
|
29447
|
+
let dirCount = 0;
|
|
29448
|
+
let fileCount = 0;
|
|
29449
|
+
for (const e of filtered) {
|
|
29450
|
+
if (e.isDirectory()) dirCount++;
|
|
29451
|
+
else if (e.isFile()) fileCount++;
|
|
29452
|
+
}
|
|
29453
|
+
opts.totalDirs.value += dirCount;
|
|
29454
|
+
opts.totalFiles.value += fileCount;
|
|
29455
|
+
opts.onProgress?.();
|
|
28935
29456
|
const items = filtered.sort((a, b) => {
|
|
28936
29457
|
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
28937
29458
|
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
@@ -28959,7 +29480,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
28959
29480
|
opts.retention.outputBytes += lineBytes;
|
|
28960
29481
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
28961
29482
|
const childPrefix = opts.prefix + connector;
|
|
28962
|
-
await walkDir(
|
|
29483
|
+
await walkDir(path38.join(dir, entry.name), depth + 1, {
|
|
28963
29484
|
...opts,
|
|
28964
29485
|
prefix: childPrefix,
|
|
28965
29486
|
isLast
|
|
@@ -28972,7 +29493,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
28972
29493
|
init_spawn_stream();
|
|
28973
29494
|
init_util();
|
|
28974
29495
|
init_legacy_bridge();
|
|
28975
|
-
import * as
|
|
29496
|
+
import * as path39 from "node:path";
|
|
28976
29497
|
var typecheckTool = {
|
|
28977
29498
|
name: "typecheck",
|
|
28978
29499
|
category: "Code Quality",
|
|
@@ -28994,11 +29515,7 @@ var typecheckTool = {
|
|
|
28994
29515
|
},
|
|
28995
29516
|
all: {
|
|
28996
29517
|
type: "boolean",
|
|
28997
|
-
description: "Type-check all
|
|
28998
|
-
},
|
|
28999
|
-
json: {
|
|
29000
|
-
type: "boolean",
|
|
29001
|
-
description: "Emit JSON output from tsc (default: false)"
|
|
29518
|
+
description: "Type-check all workspace packages (pnpm workspaces run `pnpm -r exec tsc --noEmit`; other setups run a single `tsc --noEmit` at cwd) (default: false)"
|
|
29002
29519
|
}
|
|
29003
29520
|
}
|
|
29004
29521
|
},
|
|
@@ -29034,29 +29551,42 @@ var typecheckTool = {
|
|
|
29034
29551
|
};
|
|
29035
29552
|
return;
|
|
29036
29553
|
}
|
|
29037
|
-
let
|
|
29554
|
+
let cmd;
|
|
29555
|
+
let cmdArgs;
|
|
29038
29556
|
let project;
|
|
29039
29557
|
if (input.all) {
|
|
29040
|
-
args = ["--noEmit"];
|
|
29041
29558
|
project = "workspace";
|
|
29559
|
+
const tscArgs = ["--noEmit"];
|
|
29560
|
+
if (input.strict) tscArgs.push("--strict");
|
|
29561
|
+
const manager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
29562
|
+
if (manager === "pnpm") {
|
|
29563
|
+
cmd = "pnpm";
|
|
29564
|
+
cmdArgs = ["-r", "--no-bail", "exec", "tsc", ...tscArgs];
|
|
29565
|
+
} else {
|
|
29566
|
+
cmd = "npx";
|
|
29567
|
+
cmdArgs = ["tsc", ...tscArgs];
|
|
29568
|
+
}
|
|
29042
29569
|
} else {
|
|
29043
29570
|
const tsconfig = input.project ? safeResolve(input.project, ctx) : await findTsConfig(cwd);
|
|
29044
|
-
|
|
29045
|
-
if (input.strict)
|
|
29046
|
-
if (tsconfig)
|
|
29571
|
+
const tscArgs = ["--noEmit"];
|
|
29572
|
+
if (input.strict) tscArgs.push("--strict");
|
|
29573
|
+
if (tsconfig) tscArgs.push("--project", tsconfig);
|
|
29047
29574
|
project = tsconfig ?? "default";
|
|
29575
|
+
cmd = "npx";
|
|
29576
|
+
cmdArgs = ["tsc", ...tscArgs];
|
|
29048
29577
|
}
|
|
29049
|
-
|
|
29050
|
-
yield { type: "log", text: `tsc ${args.join(" ")}`, data: { project } };
|
|
29578
|
+
yield { type: "log", text: `${cmd} ${cmdArgs.join(" ")}`, data: { project } };
|
|
29051
29579
|
const result = yield* spawnStream({
|
|
29052
|
-
cmd
|
|
29053
|
-
args:
|
|
29580
|
+
cmd,
|
|
29581
|
+
args: cmdArgs,
|
|
29054
29582
|
cwd,
|
|
29055
29583
|
signal: opts.signal,
|
|
29056
29584
|
maxBytes: 2e5
|
|
29057
29585
|
});
|
|
29058
|
-
const
|
|
29059
|
-
|
|
29586
|
+
const combined = `${result.stdout}
|
|
29587
|
+
${result.stderr}`;
|
|
29588
|
+
const errors = [...combined.matchAll(/^.*\berror TS\d+:/gm)].length;
|
|
29589
|
+
const warnings = [...combined.matchAll(/^.*\bwarning TS\d+:/gm)].length;
|
|
29060
29590
|
yield {
|
|
29061
29591
|
type: "final",
|
|
29062
29592
|
output: {
|
|
@@ -29071,12 +29601,12 @@ var typecheckTool = {
|
|
|
29071
29601
|
}
|
|
29072
29602
|
};
|
|
29073
29603
|
async function findTsConfig(cwd) {
|
|
29074
|
-
const { stat:
|
|
29604
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
29075
29605
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
29076
29606
|
for (const f of candidates) {
|
|
29077
29607
|
try {
|
|
29078
|
-
const s = await
|
|
29079
|
-
if (s.isFile()) return
|
|
29608
|
+
const s = await stat19(path39.join(cwd, f));
|
|
29609
|
+
if (s.isFile()) return path39.join(cwd, f);
|
|
29080
29610
|
} catch {
|
|
29081
29611
|
}
|
|
29082
29612
|
}
|
|
@@ -29085,21 +29615,32 @@ async function findTsConfig(cwd) {
|
|
|
29085
29615
|
|
|
29086
29616
|
// src/write.ts
|
|
29087
29617
|
import * as fs34 from "node:fs/promises";
|
|
29088
|
-
import { ToolValidationError as
|
|
29089
|
-
import {
|
|
29618
|
+
import { ToolValidationError as ToolValidationError11 } from "@wrongstack/core/types";
|
|
29619
|
+
import {
|
|
29620
|
+
atomicWrite as atomicWrite5,
|
|
29621
|
+
detectNewlineStyle as detectNewlineStyle3,
|
|
29622
|
+
normalizeToLf as normalizeToLf3,
|
|
29623
|
+
toStyle as toStyle3,
|
|
29624
|
+
unifiedDiff as unifiedDiff3
|
|
29625
|
+
} from "@wrongstack/core/utils";
|
|
29090
29626
|
init_util();
|
|
29627
|
+
var MAX_DIFF_BYTES3 = 262144;
|
|
29091
29628
|
var writeTool = {
|
|
29092
29629
|
name: "write",
|
|
29093
29630
|
category: "Filesystem",
|
|
29094
29631
|
description: "Write or completely overwrite a file on disk. This is a high-privilege operation. For modifying existing files, you should almost always prefer the `edit` tool instead, because `edit` is safer and works on the last-read version of the file.",
|
|
29095
|
-
usageHint: "RULES FOR CORRECT USAGE:\n- Use `write` primarily for **new files** or when you want to replace the entire content.\n- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n-
|
|
29632
|
+
usageHint: "RULES FOR CORRECT USAGE:\n- Use `write` primarily for **new files** or when you want to replace the entire content.\n- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n- When overwriting an existing file, the tool reads the current content itself to compute the diff \u2014 but still `read` the file first before large rewrites so you know what you are replacing.\n- When overwriting an existing file, the content is normalized to the dominant line-ending style (CRLF/LF) of the existing file; new files are written verbatim.\n- The path is resolved relative to the project root and protected against escaping the workspace.",
|
|
29096
29633
|
selection: {
|
|
29097
29634
|
doNotUseWhen: "making a precise change to part of an existing file.",
|
|
29098
29635
|
useInstead: ["edit"]
|
|
29099
29636
|
},
|
|
29100
29637
|
permission: "confirm",
|
|
29638
|
+
// WS-046: gives permission decisions something to key on — the file being
|
|
29639
|
+
// written, so trust rules can scope by path.
|
|
29640
|
+
subjectKey: "path",
|
|
29101
29641
|
mutating: true,
|
|
29102
29642
|
timeoutMs: 5e3,
|
|
29643
|
+
maxOutputBytes: 262144,
|
|
29103
29644
|
capabilities: ["fs.write"],
|
|
29104
29645
|
icon: "file",
|
|
29105
29646
|
inputSchema: {
|
|
@@ -29135,13 +29676,13 @@ async function writeFile6(input, ctx, signal) {
|
|
|
29135
29676
|
}
|
|
29136
29677
|
async function prepareWrite(input, ctx) {
|
|
29137
29678
|
if (!input?.path) {
|
|
29138
|
-
throw new
|
|
29679
|
+
throw new ToolValidationError11({
|
|
29139
29680
|
message: "write: path is required",
|
|
29140
29681
|
field: "path"
|
|
29141
29682
|
});
|
|
29142
29683
|
}
|
|
29143
29684
|
if (input.content === void 0) {
|
|
29144
|
-
throw new
|
|
29685
|
+
throw new ToolValidationError11({
|
|
29145
29686
|
message: "write: content is required",
|
|
29146
29687
|
field: "content"
|
|
29147
29688
|
});
|
|
@@ -29150,12 +29691,12 @@ async function prepareWrite(input, ctx) {
|
|
|
29150
29691
|
let existed = false;
|
|
29151
29692
|
let prev = "";
|
|
29152
29693
|
try {
|
|
29153
|
-
const
|
|
29154
|
-
existed =
|
|
29694
|
+
const stat19 = await fs34.stat(absPath);
|
|
29695
|
+
existed = stat19.isFile();
|
|
29155
29696
|
if (existed) {
|
|
29156
29697
|
if (!ctx.hasRead(absPath)) {
|
|
29157
29698
|
prev = await fs34.readFile(absPath, "utf8");
|
|
29158
|
-
ctx.recordRead(absPath,
|
|
29699
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "write", sha256hex(prev));
|
|
29159
29700
|
} else {
|
|
29160
29701
|
prev = await fs34.readFile(absPath, "utf8");
|
|
29161
29702
|
}
|
|
@@ -29168,31 +29709,42 @@ async function prepareWrite(input, ctx) {
|
|
|
29168
29709
|
return { absPath, existed, prev };
|
|
29169
29710
|
}
|
|
29170
29711
|
async function finishWrite(input, ctx, prepared, signal) {
|
|
29712
|
+
const content = prepared.existed ? toStyle3(normalizeToLf3(input.content), detectNewlineStyle3(prepared.prev)) : input.content;
|
|
29171
29713
|
signal?.throwIfAborted();
|
|
29172
|
-
await atomicWrite5(prepared.absPath,
|
|
29173
|
-
const
|
|
29174
|
-
+ (new file, ${
|
|
29175
|
-
const
|
|
29176
|
-
|
|
29714
|
+
await atomicWrite5(prepared.absPath, content);
|
|
29715
|
+
const rawDiff = prepared.existed ? unifiedDiff3(prepared.prev, content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
29716
|
+
+ (new file, ${content.split("\n").length} lines)`;
|
|
29717
|
+
const { text: diff, truncated: diffTruncated } = truncateDiffPayload(rawDiff, MAX_DIFF_BYTES3);
|
|
29718
|
+
const stat19 = await fs34.stat(prepared.absPath);
|
|
29719
|
+
ctx.recordRead(prepared.absPath, stat19.mtimeMs, "write", sha256hex(content));
|
|
29177
29720
|
ctx.session.recordFileChange({
|
|
29178
29721
|
path: prepared.absPath,
|
|
29179
29722
|
action: prepared.existed ? "modified" : "created",
|
|
29180
29723
|
before: prepared.existed ? prepared.prev : null,
|
|
29181
|
-
after:
|
|
29724
|
+
after: content
|
|
29182
29725
|
});
|
|
29183
29726
|
const syntax = await checkSyntax(
|
|
29184
29727
|
prepared.absPath,
|
|
29185
|
-
|
|
29728
|
+
content,
|
|
29186
29729
|
prepared.existed ? prepared.prev : void 0
|
|
29187
29730
|
).catch(() => void 0);
|
|
29188
29731
|
const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
|
|
29732
|
+
const notes = [];
|
|
29733
|
+
if (diffTruncated) {
|
|
29734
|
+
notes.push("Diff truncated to the 256 KiB output budget \u2014 the full write is on disk.");
|
|
29735
|
+
}
|
|
29736
|
+
if (hasSyntaxErrors) {
|
|
29737
|
+
notes.push(
|
|
29738
|
+
syntax.preExisting ? "Syntax check: the file still has parse errors (they pre-date this write) \u2014 see syntax_errors." : `Syntax check: the written content has ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`
|
|
29739
|
+
);
|
|
29740
|
+
}
|
|
29189
29741
|
return {
|
|
29190
29742
|
path: prepared.absPath,
|
|
29191
|
-
bytes_written: Buffer.byteLength(
|
|
29743
|
+
bytes_written: Buffer.byteLength(content, "utf8"),
|
|
29192
29744
|
created: !prepared.existed,
|
|
29193
29745
|
diff,
|
|
29194
29746
|
syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
|
|
29195
|
-
note:
|
|
29747
|
+
note: notes.length > 0 ? notes.join("\n") : void 0
|
|
29196
29748
|
};
|
|
29197
29749
|
}
|
|
29198
29750
|
|