@wrongstack/tools 0.305.1 → 0.306.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_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 +1294 -726
- package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
- package/dist/codebase-index/index.js +223 -152
- package/dist/codebase-index/project-server.js +10 -11
- 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 +1359 -762
- 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 +1294 -726
- package/dist/plan.js +91 -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 -3
- 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.js +3 -1
- package/dist/skill.d.ts +6 -0
- package/dist/skill.js +9 -10
- package/dist/task.js +81 -2
- package/dist/test.js +28 -13
- package/dist/todo.js +79 -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 +1294 -726
- 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 -4
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
|
|
|
@@ -13734,12 +13866,12 @@ function projectIndexServerBuildId(entrypoint) {
|
|
|
13734
13866
|
const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
|
|
13735
13867
|
const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path17.resolve(cleanHref);
|
|
13736
13868
|
try {
|
|
13737
|
-
const
|
|
13738
|
-
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) {
|
|
13739
13871
|
return buildIdCache.buildId;
|
|
13740
13872
|
}
|
|
13741
13873
|
const buildId = createHash4("sha256").update(fs12.readFileSync(file)).digest("hex").slice(0, 24);
|
|
13742
|
-
buildIdCache = { file, mtimeMs:
|
|
13874
|
+
buildIdCache = { file, mtimeMs: stat19.mtimeMs, size: stat19.size, buildId };
|
|
13743
13875
|
return buildId;
|
|
13744
13876
|
} catch {
|
|
13745
13877
|
return `unreadable:${path17.basename(file)}`;
|
|
@@ -13881,8 +14013,8 @@ function isProjectIndexServerHealth(value) {
|
|
|
13881
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";
|
|
13882
14014
|
}
|
|
13883
14015
|
function delay(ms) {
|
|
13884
|
-
return new Promise((
|
|
13885
|
-
const timer = setTimeout(
|
|
14016
|
+
return new Promise((resolve17) => {
|
|
14017
|
+
const timer = setTimeout(resolve17, ms);
|
|
13886
14018
|
timer.unref?.();
|
|
13887
14019
|
});
|
|
13888
14020
|
}
|
|
@@ -14086,7 +14218,7 @@ var ProjectServerConnection = class {
|
|
|
14086
14218
|
return Promise.reject(new Error("codebase-index server connection is not available"));
|
|
14087
14219
|
}
|
|
14088
14220
|
const id = this.nextId++;
|
|
14089
|
-
return new Promise((
|
|
14221
|
+
return new Promise((resolve17, reject) => {
|
|
14090
14222
|
const timer = setTimeout(() => {
|
|
14091
14223
|
const entry = this.pending.get(id);
|
|
14092
14224
|
if (!entry) return;
|
|
@@ -14109,7 +14241,7 @@ var ProjectServerConnection = class {
|
|
|
14109
14241
|
entry.reject(cancellationError(signal));
|
|
14110
14242
|
} : void 0;
|
|
14111
14243
|
this.pending.set(id, {
|
|
14112
|
-
resolve:
|
|
14244
|
+
resolve: resolve17,
|
|
14113
14245
|
reject,
|
|
14114
14246
|
timer,
|
|
14115
14247
|
signal,
|
|
@@ -14178,7 +14310,7 @@ var ProjectServerConnection = class {
|
|
|
14178
14310
|
this.binaryBuffer = [];
|
|
14179
14311
|
this.useBinary = false;
|
|
14180
14312
|
this.textDecoder = null;
|
|
14181
|
-
return new Promise((
|
|
14313
|
+
return new Promise((resolve17, reject) => {
|
|
14182
14314
|
const socket = net3.createConnection(this.endpoint);
|
|
14183
14315
|
this.socket = socket;
|
|
14184
14316
|
const timer = setTimeout(() => {
|
|
@@ -14190,7 +14322,7 @@ var ProjectServerConnection = class {
|
|
|
14190
14322
|
clearTimeout(timer);
|
|
14191
14323
|
this.connectResolve = null;
|
|
14192
14324
|
this.connectReject = null;
|
|
14193
|
-
|
|
14325
|
+
resolve17();
|
|
14194
14326
|
};
|
|
14195
14327
|
const finishReject = (error) => {
|
|
14196
14328
|
clearTimeout(timer);
|
|
@@ -14213,7 +14345,7 @@ var ProjectServerConnection = class {
|
|
|
14213
14345
|
this.onBinaryData(socket, chunk);
|
|
14214
14346
|
return;
|
|
14215
14347
|
}
|
|
14216
|
-
if (!this.textDecoder) this.textDecoder = new
|
|
14348
|
+
if (!this.textDecoder) this.textDecoder = new StringDecoder2("utf8");
|
|
14217
14349
|
this.buffer += this.textDecoder.write(chunk);
|
|
14218
14350
|
while (true) {
|
|
14219
14351
|
const newline = this.buffer.indexOf("\n");
|
|
@@ -15204,9 +15336,9 @@ var ParserWorkerPool = class {
|
|
|
15204
15336
|
for (let i = 0; i < files.length; i++) {
|
|
15205
15337
|
chunks[i % workerCount].push(files[i]);
|
|
15206
15338
|
}
|
|
15207
|
-
return new Promise((
|
|
15339
|
+
return new Promise((resolve17, reject) => {
|
|
15208
15340
|
this.pending.set(batchId, {
|
|
15209
|
-
resolve:
|
|
15341
|
+
resolve: resolve17,
|
|
15210
15342
|
reject,
|
|
15211
15343
|
accumulated: [],
|
|
15212
15344
|
expectedWorkers: workerCount,
|
|
@@ -15237,10 +15369,10 @@ var ParserWorkerPool = class {
|
|
|
15237
15369
|
await Promise.allSettled(
|
|
15238
15370
|
workers.map(
|
|
15239
15371
|
(w) => Promise.race([
|
|
15240
|
-
new Promise((
|
|
15241
|
-
w.once("exit", () =>
|
|
15372
|
+
new Promise((resolve17) => {
|
|
15373
|
+
w.once("exit", () => resolve17());
|
|
15242
15374
|
}),
|
|
15243
|
-
new Promise((
|
|
15375
|
+
new Promise((resolve17) => setTimeout(() => resolve17(), 2e3))
|
|
15244
15376
|
]).then(() => {
|
|
15245
15377
|
if (!w.threadId) return;
|
|
15246
15378
|
return w.terminate().catch(() => {
|
|
@@ -15304,7 +15436,7 @@ function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
|
|
|
15304
15436
|
return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
|
|
15305
15437
|
}
|
|
15306
15438
|
function yieldEventLoop() {
|
|
15307
|
-
return new Promise((
|
|
15439
|
+
return new Promise((resolve17) => setImmediate(resolve17));
|
|
15308
15440
|
}
|
|
15309
15441
|
function throwIfAborted(signal) {
|
|
15310
15442
|
if (!signal?.aborted) return;
|
|
@@ -15332,7 +15464,7 @@ function normalizeComparablePath(value) {
|
|
|
15332
15464
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
15333
15465
|
}
|
|
15334
15466
|
function gitOutput(projectRoot, args) {
|
|
15335
|
-
return new Promise((
|
|
15467
|
+
return new Promise((resolve17, reject) => {
|
|
15336
15468
|
execFile(
|
|
15337
15469
|
"git",
|
|
15338
15470
|
["-C", projectRoot, ...args],
|
|
@@ -15343,7 +15475,7 @@ function gitOutput(projectRoot, args) {
|
|
|
15343
15475
|
},
|
|
15344
15476
|
(error, stdout) => {
|
|
15345
15477
|
if (error) reject(error);
|
|
15346
|
-
else
|
|
15478
|
+
else resolve17(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
|
|
15347
15479
|
}
|
|
15348
15480
|
);
|
|
15349
15481
|
});
|
|
@@ -15575,9 +15707,9 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15575
15707
|
const statReadParse = await Promise.allSettled(
|
|
15576
15708
|
batchFiles.map(
|
|
15577
15709
|
async (file) => {
|
|
15578
|
-
let
|
|
15710
|
+
let stat19;
|
|
15579
15711
|
try {
|
|
15580
|
-
|
|
15712
|
+
stat19 = await fs18.stat(file, statOpts);
|
|
15581
15713
|
} catch (e) {
|
|
15582
15714
|
if (isAbortError(e)) throw e;
|
|
15583
15715
|
return {
|
|
@@ -15589,21 +15721,21 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15589
15721
|
missing: isMissingPathError(e)
|
|
15590
15722
|
};
|
|
15591
15723
|
}
|
|
15592
|
-
if (!
|
|
15724
|
+
if (!stat19.isFile()) return { file, stat: stat19, lang: "", parsed: null };
|
|
15593
15725
|
const lang = detectLang(file);
|
|
15594
|
-
if (!lang) return { file, stat:
|
|
15595
|
-
if (
|
|
15726
|
+
if (!lang) return { file, stat: stat19, lang: "", parsed: null };
|
|
15727
|
+
if (stat19.size > MAX_INDEX_FILE_BYTES) {
|
|
15596
15728
|
return {
|
|
15597
15729
|
file,
|
|
15598
|
-
stat:
|
|
15730
|
+
stat: stat19,
|
|
15599
15731
|
lang,
|
|
15600
15732
|
parsed: null,
|
|
15601
|
-
error: `file too large (${
|
|
15733
|
+
error: `file too large (${stat19.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
15602
15734
|
};
|
|
15603
15735
|
}
|
|
15604
15736
|
const meta = existingMeta.get(file);
|
|
15605
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
15606
|
-
return { file, stat:
|
|
15737
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat19.mtimeMs)) {
|
|
15738
|
+
return { file, stat: stat19, lang, parsed: null, skippedMeta: meta };
|
|
15607
15739
|
}
|
|
15608
15740
|
let content;
|
|
15609
15741
|
try {
|
|
@@ -15612,7 +15744,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15612
15744
|
if (isAbortError(e)) throw e;
|
|
15613
15745
|
return {
|
|
15614
15746
|
file,
|
|
15615
|
-
stat:
|
|
15747
|
+
stat: stat19,
|
|
15616
15748
|
lang,
|
|
15617
15749
|
parsed: null,
|
|
15618
15750
|
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
@@ -15622,15 +15754,15 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15622
15754
|
if (!force && meta && meta.contentHash && contentHash === meta.contentHash) {
|
|
15623
15755
|
return {
|
|
15624
15756
|
file,
|
|
15625
|
-
stat:
|
|
15757
|
+
stat: stat19,
|
|
15626
15758
|
lang,
|
|
15627
15759
|
parsed: null,
|
|
15628
15760
|
content,
|
|
15629
15761
|
contentHash,
|
|
15630
|
-
skippedMeta: { ...meta, mtimeMs: Math.floor(
|
|
15762
|
+
skippedMeta: { ...meta, mtimeMs: Math.floor(stat19.mtimeMs) }
|
|
15631
15763
|
};
|
|
15632
15764
|
}
|
|
15633
|
-
return { file, stat:
|
|
15765
|
+
return { file, stat: stat19, lang, parsed: null, content, contentHash };
|
|
15634
15766
|
}
|
|
15635
15767
|
)
|
|
15636
15768
|
);
|
|
@@ -15709,7 +15841,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15709
15841
|
filesFailed++;
|
|
15710
15842
|
continue;
|
|
15711
15843
|
}
|
|
15712
|
-
const { stat:
|
|
15844
|
+
const { stat: stat19, lang, parsed } = result;
|
|
15713
15845
|
if (result.skippedMeta) {
|
|
15714
15846
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
15715
15847
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -15733,7 +15865,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15733
15865
|
store.upsertFile({
|
|
15734
15866
|
file,
|
|
15735
15867
|
lang,
|
|
15736
|
-
mtimeMs: Math.floor(
|
|
15868
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
15737
15869
|
symbolCount: 0,
|
|
15738
15870
|
lastIndexed: Date.now(),
|
|
15739
15871
|
contentHash: result.contentHash ?? ""
|
|
@@ -15747,7 +15879,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15747
15879
|
store.replaceEmptyFile({
|
|
15748
15880
|
file,
|
|
15749
15881
|
lang,
|
|
15750
|
-
mtimeMs: Math.floor(
|
|
15882
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
15751
15883
|
symbolCount: 0,
|
|
15752
15884
|
lastIndexed: Date.now(),
|
|
15753
15885
|
contentHash: result.contentHash ?? ""
|
|
@@ -15761,7 +15893,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15761
15893
|
lang,
|
|
15762
15894
|
symbols: parsed.symbols,
|
|
15763
15895
|
refs: parsed.refs ?? [],
|
|
15764
|
-
mtimeMs: Math.floor(
|
|
15896
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
15765
15897
|
symbolCount: parsed.symbols.length,
|
|
15766
15898
|
contentHash: result.contentHash ?? ""
|
|
15767
15899
|
});
|
|
@@ -16086,7 +16218,7 @@ function callIndexOp(op, args, opts) {
|
|
|
16086
16218
|
opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
|
|
16087
16219
|
);
|
|
16088
16220
|
}
|
|
16089
|
-
return new Promise((
|
|
16221
|
+
return new Promise((resolve17, reject) => {
|
|
16090
16222
|
const id = nextRpcId++;
|
|
16091
16223
|
const timer = setTimeout(() => {
|
|
16092
16224
|
pending.delete(id);
|
|
@@ -16108,7 +16240,7 @@ function callIndexOp(op, args, opts) {
|
|
|
16108
16240
|
pending.set(id, {
|
|
16109
16241
|
resolve: (v) => {
|
|
16110
16242
|
cleanup();
|
|
16111
|
-
|
|
16243
|
+
resolve17(v);
|
|
16112
16244
|
},
|
|
16113
16245
|
reject: (e) => {
|
|
16114
16246
|
cleanup();
|
|
@@ -16259,6 +16391,160 @@ async function outgoingCallsService2(args) {
|
|
|
16259
16391
|
}
|
|
16260
16392
|
|
|
16261
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;
|
|
16262
16548
|
var codebaseIndexTool = {
|
|
16263
16549
|
name: "codebase-index",
|
|
16264
16550
|
category: "Project",
|
|
@@ -16284,12 +16570,23 @@ var codebaseIndexTool = {
|
|
|
16284
16570
|
},
|
|
16285
16571
|
langs: {
|
|
16286
16572
|
type: "array",
|
|
16287
|
-
items: { type: "string" },
|
|
16288
|
-
description:
|
|
16573
|
+
items: { type: "string", enum: [...INDEXABLE_LANG_IDS] },
|
|
16574
|
+
description: `Limit reindex to specific languages: ${INDEXABLE_LANG_IDS.join(", ")}`
|
|
16289
16575
|
}
|
|
16290
16576
|
}
|
|
16291
16577
|
},
|
|
16292
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
|
+
}
|
|
16293
16590
|
if (isIndexing()) {
|
|
16294
16591
|
return {
|
|
16295
16592
|
filesIndexed: 0,
|
|
@@ -16311,23 +16608,32 @@ var codebaseIndexTool = {
|
|
|
16311
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.`
|
|
16312
16609
|
};
|
|
16313
16610
|
}
|
|
16314
|
-
|
|
16611
|
+
const result = await runStartupIndex({
|
|
16315
16612
|
projectRoot: ctx.projectRoot,
|
|
16316
16613
|
force: input.force ?? false,
|
|
16317
16614
|
langs: input.langs,
|
|
16318
16615
|
indexDir: codebaseIndexDirOverride(ctx),
|
|
16319
16616
|
signal: execOpts?.signal
|
|
16320
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;
|
|
16321
16626
|
}
|
|
16322
16627
|
};
|
|
16323
16628
|
|
|
16324
16629
|
// src/codebase-index/codebase-incoming-calls-tool.ts
|
|
16630
|
+
import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
|
|
16325
16631
|
var codebaseIncomingCallsTool = {
|
|
16326
16632
|
name: "codebase-incoming-calls",
|
|
16327
16633
|
category: "Project",
|
|
16328
16634
|
icon: "index",
|
|
16329
|
-
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.
|
|
16330
|
-
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.',
|
|
16331
16637
|
permission: "auto",
|
|
16332
16638
|
mutating: false,
|
|
16333
16639
|
capabilities: ["fs.read"],
|
|
@@ -16379,16 +16685,27 @@ var codebaseIncomingCallsTool = {
|
|
|
16379
16685
|
}
|
|
16380
16686
|
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
|
|
16381
16687
|
const transitive = input.transitive === true;
|
|
16382
|
-
|
|
16383
|
-
|
|
16384
|
-
|
|
16385
|
-
|
|
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 {
|
|
16386
16702
|
symbol: input.symbol,
|
|
16387
|
-
|
|
16388
|
-
|
|
16389
|
-
|
|
16390
|
-
}
|
|
16391
|
-
|
|
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;
|
|
16392
16709
|
if (!symbolFound) {
|
|
16393
16710
|
let hasPersistedIndex = state.ready;
|
|
16394
16711
|
if (!hasPersistedIndex) {
|
|
@@ -16433,12 +16750,13 @@ var codebaseIncomingCallsTool = {
|
|
|
16433
16750
|
};
|
|
16434
16751
|
|
|
16435
16752
|
// src/codebase-index/codebase-outgoing-calls-tool.ts
|
|
16753
|
+
import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
|
|
16436
16754
|
var codebaseOutgoingCallsTool = {
|
|
16437
16755
|
name: "codebase-outgoing-calls",
|
|
16438
16756
|
category: "Project",
|
|
16439
16757
|
icon: "index",
|
|
16440
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.",
|
|
16441
|
-
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.',
|
|
16442
16760
|
permission: "auto",
|
|
16443
16761
|
mutating: false,
|
|
16444
16762
|
capabilities: ["fs.read"],
|
|
@@ -16490,16 +16808,27 @@ var codebaseOutgoingCallsTool = {
|
|
|
16490
16808
|
}
|
|
16491
16809
|
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
|
|
16492
16810
|
const transitive = input.transitive === true;
|
|
16493
|
-
|
|
16494
|
-
|
|
16495
|
-
|
|
16496
|
-
|
|
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 {
|
|
16497
16825
|
symbol: input.symbol,
|
|
16498
|
-
|
|
16499
|
-
|
|
16500
|
-
|
|
16501
|
-
}
|
|
16502
|
-
|
|
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;
|
|
16503
16832
|
if (!symbolFound) {
|
|
16504
16833
|
let hasPersistedIndex = state.ready;
|
|
16505
16834
|
if (!hasPersistedIndex) {
|
|
@@ -16543,135 +16872,9 @@ var codebaseOutgoingCallsTool = {
|
|
|
16543
16872
|
}
|
|
16544
16873
|
};
|
|
16545
16874
|
|
|
16546
|
-
// src/codebase-index/codebase-
|
|
16547
|
-
var
|
|
16548
|
-
name: "codebase-
|
|
16549
|
-
category: "Project",
|
|
16550
|
-
icon: "index",
|
|
16551
|
-
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).",
|
|
16552
|
-
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.",
|
|
16553
|
-
permission: "auto",
|
|
16554
|
-
mutating: false,
|
|
16555
|
-
capabilities: ["fs.read"],
|
|
16556
|
-
// The index host has its own 30s read watchdog. Leave enough headroom for
|
|
16557
|
-
// worker teardown and structured timeout reporting.
|
|
16558
|
-
timeoutMs: 35e3,
|
|
16559
|
-
inputSchema: {
|
|
16560
|
-
type: "object",
|
|
16561
|
-
properties: {
|
|
16562
|
-
query: {
|
|
16563
|
-
type: "string",
|
|
16564
|
-
description: "Search query \u2014 searches symbol names, signatures, and doc comments"
|
|
16565
|
-
},
|
|
16566
|
-
kind: {
|
|
16567
|
-
type: "string",
|
|
16568
|
-
enum: [
|
|
16569
|
-
"class",
|
|
16570
|
-
"interface",
|
|
16571
|
-
"enum",
|
|
16572
|
-
"type",
|
|
16573
|
-
"function",
|
|
16574
|
-
"method",
|
|
16575
|
-
"var",
|
|
16576
|
-
"const",
|
|
16577
|
-
"let",
|
|
16578
|
-
"property",
|
|
16579
|
-
"parameter",
|
|
16580
|
-
"namespace",
|
|
16581
|
-
"object",
|
|
16582
|
-
"literal",
|
|
16583
|
-
"schema",
|
|
16584
|
-
"struct",
|
|
16585
|
-
"trait",
|
|
16586
|
-
"impl",
|
|
16587
|
-
"static",
|
|
16588
|
-
"mod"
|
|
16589
|
-
],
|
|
16590
|
-
description: "Filter by indexed symbol kind"
|
|
16591
|
-
},
|
|
16592
|
-
lang: {
|
|
16593
|
-
type: "string",
|
|
16594
|
-
enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
|
|
16595
|
-
description: "Filter by indexed language"
|
|
16596
|
-
},
|
|
16597
|
-
lspKind: {
|
|
16598
|
-
type: "integer",
|
|
16599
|
-
description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
|
|
16600
|
-
},
|
|
16601
|
-
file: {
|
|
16602
|
-
type: "string",
|
|
16603
|
-
description: "Filter to files matching this path substring"
|
|
16604
|
-
},
|
|
16605
|
-
limit: {
|
|
16606
|
-
type: "integer",
|
|
16607
|
-
description: "Maximum results to return (default 20, max 100)",
|
|
16608
|
-
minimum: 1,
|
|
16609
|
-
maximum: 100
|
|
16610
|
-
},
|
|
16611
|
-
preferLsp: {
|
|
16612
|
-
type: "boolean",
|
|
16613
|
-
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."
|
|
16614
|
-
}
|
|
16615
|
-
},
|
|
16616
|
-
required: ["query"]
|
|
16617
|
-
},
|
|
16618
|
-
async execute(input, ctx, execOpts) {
|
|
16619
|
-
const state = getIndexState();
|
|
16620
|
-
if (state.indexing && !state.ready) {
|
|
16621
|
-
return {
|
|
16622
|
-
results: [],
|
|
16623
|
-
total: 0,
|
|
16624
|
-
query: input.query,
|
|
16625
|
-
indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
|
|
16626
|
-
};
|
|
16627
|
-
}
|
|
16628
|
-
if (state.lastError) {
|
|
16629
|
-
const circuit = state.circuit;
|
|
16630
|
-
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.";
|
|
16631
|
-
return {
|
|
16632
|
-
results: [],
|
|
16633
|
-
total: 0,
|
|
16634
|
-
query: input.query,
|
|
16635
|
-
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
16636
|
-
};
|
|
16637
|
-
}
|
|
16638
|
-
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
|
|
16639
|
-
const { results, total } = await searchCodebaseIndex(
|
|
16640
|
-
{
|
|
16641
|
-
projectRoot: ctx.projectRoot,
|
|
16642
|
-
indexDir: codebaseIndexDirOverride(ctx),
|
|
16643
|
-
query: input.query,
|
|
16644
|
-
kind: input.kind?.toLowerCase(),
|
|
16645
|
-
lang: input.lang?.toLowerCase(),
|
|
16646
|
-
file: input.file,
|
|
16647
|
-
lspKind: input.lspKind,
|
|
16648
|
-
limit
|
|
16649
|
-
},
|
|
16650
|
-
{ signal: execOpts?.signal }
|
|
16651
|
-
);
|
|
16652
|
-
let hasPersistedIndex = state.ready || total > 0;
|
|
16653
|
-
if (!hasPersistedIndex) {
|
|
16654
|
-
try {
|
|
16655
|
-
const stats = await codebaseIndexStats(
|
|
16656
|
-
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
16657
|
-
{ signal: execOpts?.signal }
|
|
16658
|
-
);
|
|
16659
|
-
hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
|
|
16660
|
-
} catch {
|
|
16661
|
-
}
|
|
16662
|
-
}
|
|
16663
|
-
return {
|
|
16664
|
-
results,
|
|
16665
|
-
total,
|
|
16666
|
-
query: input.query,
|
|
16667
|
-
...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
|
|
16668
|
-
};
|
|
16669
|
-
}
|
|
16670
|
-
};
|
|
16671
|
-
|
|
16672
|
-
// src/codebase-index/codebase-stats-tool.ts
|
|
16673
|
-
var codebaseStatsTool = {
|
|
16674
|
-
name: "codebase-stats",
|
|
16875
|
+
// src/codebase-index/codebase-stats-tool.ts
|
|
16876
|
+
var codebaseStatsTool = {
|
|
16877
|
+
name: "codebase-stats",
|
|
16675
16878
|
category: "Project",
|
|
16676
16879
|
icon: "index",
|
|
16677
16880
|
description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
|
|
@@ -16762,9 +16965,9 @@ import * as path25 from "node:path";
|
|
|
16762
16965
|
var deadCodeScanTool = {
|
|
16763
16966
|
name: "dead-code-scan",
|
|
16764
16967
|
category: "Project",
|
|
16765
|
-
icon: "
|
|
16968
|
+
icon: "index",
|
|
16766
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).",
|
|
16767
|
-
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).",
|
|
16768
16971
|
permission: "auto",
|
|
16769
16972
|
mutating: false,
|
|
16770
16973
|
capabilities: ["fs.read"],
|
|
@@ -17527,12 +17730,14 @@ import { statSync as statSync3 } from "node:fs";
|
|
|
17527
17730
|
import * as fs22 from "node:fs/promises";
|
|
17528
17731
|
import * as path27 from "node:path";
|
|
17529
17732
|
import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
|
|
17733
|
+
import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
|
|
17530
17734
|
var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
|
|
17735
|
+
var MAX_GIT_DIFF_CHARS = 1e5;
|
|
17531
17736
|
var diffTool = {
|
|
17532
17737
|
name: "diff",
|
|
17533
17738
|
category: "Filesystem",
|
|
17534
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.",
|
|
17535
|
-
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).',
|
|
17536
17741
|
permission: "auto",
|
|
17537
17742
|
mutating: false,
|
|
17538
17743
|
maxOutputBytes: 262144,
|
|
@@ -17565,11 +17770,12 @@ var diffTool = {
|
|
|
17565
17770
|
mode: {
|
|
17566
17771
|
type: "string",
|
|
17567
17772
|
enum: ["unified", "side-by-side", "stat"],
|
|
17568
|
-
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.'
|
|
17569
17774
|
},
|
|
17570
17775
|
context: {
|
|
17571
17776
|
type: "integer",
|
|
17572
|
-
|
|
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."
|
|
17573
17779
|
}
|
|
17574
17780
|
}
|
|
17575
17781
|
},
|
|
@@ -17582,16 +17788,31 @@ var diffTool = {
|
|
|
17582
17788
|
};
|
|
17583
17789
|
async function gitDiff(input, ctx, signal) {
|
|
17584
17790
|
if (input.a?.startsWith("-")) {
|
|
17585
|
-
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
|
+
});
|
|
17586
17795
|
}
|
|
17587
17796
|
if (input.b?.startsWith("-")) {
|
|
17588
|
-
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
|
+
});
|
|
17589
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;
|
|
17590
17806
|
const gitDir = findGitDir(ctx.cwd);
|
|
17591
17807
|
if (!gitDir) {
|
|
17592
|
-
return { diff: "", files: [], truncated: false, mode:
|
|
17808
|
+
return { diff: "", files: [], truncated: false, mode: effectiveMode };
|
|
17593
17809
|
}
|
|
17594
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
|
+
}
|
|
17595
17816
|
if (input.staged) args.push("--staged");
|
|
17596
17817
|
if (input.a) args.push(input.a);
|
|
17597
17818
|
if (input.b) args.push(input.b);
|
|
@@ -17600,19 +17821,30 @@ async function gitDiff(input, ctx, signal) {
|
|
|
17600
17821
|
args.push("--", ...files.map((f) => f.trim()));
|
|
17601
17822
|
}
|
|
17602
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
|
+
}
|
|
17603
17834
|
return {
|
|
17604
|
-
diff
|
|
17835
|
+
diff,
|
|
17605
17836
|
files: [],
|
|
17606
|
-
truncated
|
|
17607
|
-
mode:
|
|
17837
|
+
truncated,
|
|
17838
|
+
mode: effectiveMode,
|
|
17839
|
+
note: sideBySideNote
|
|
17608
17840
|
};
|
|
17609
17841
|
}
|
|
17610
17842
|
function findGitDir(cwd) {
|
|
17611
17843
|
let dir = cwd;
|
|
17612
17844
|
for (let i = 0; i < 20; i++) {
|
|
17613
17845
|
try {
|
|
17614
|
-
const
|
|
17615
|
-
if (
|
|
17846
|
+
const stat19 = statSync3(path27.join(dir, ".git"));
|
|
17847
|
+
if (stat19.isDirectory()) return dir;
|
|
17616
17848
|
} catch {
|
|
17617
17849
|
}
|
|
17618
17850
|
const parent = path27.dirname(dir);
|
|
@@ -17622,7 +17854,7 @@ function findGitDir(cwd) {
|
|
|
17622
17854
|
return null;
|
|
17623
17855
|
}
|
|
17624
17856
|
function runGit(args, cwd, signal) {
|
|
17625
|
-
return new Promise((
|
|
17857
|
+
return new Promise((resolve17) => {
|
|
17626
17858
|
let stdout = "";
|
|
17627
17859
|
let stderr = "";
|
|
17628
17860
|
const child = spawn7("git", args, {
|
|
@@ -17638,8 +17870,8 @@ function runGit(args, cwd, signal) {
|
|
|
17638
17870
|
child.stderr?.on("data", (c) => {
|
|
17639
17871
|
stderr += c.toString();
|
|
17640
17872
|
});
|
|
17641
|
-
child.on("close", (code) =>
|
|
17642
|
-
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 }));
|
|
17643
17875
|
});
|
|
17644
17876
|
}
|
|
17645
17877
|
async function fileDiff(input, ctx, _signal) {
|
|
@@ -17650,19 +17882,19 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
17650
17882
|
diff: "No files specified",
|
|
17651
17883
|
files: [],
|
|
17652
17884
|
truncated: false,
|
|
17653
|
-
mode:
|
|
17885
|
+
mode: "dump"
|
|
17654
17886
|
};
|
|
17655
17887
|
}
|
|
17656
17888
|
const results = [];
|
|
17657
17889
|
let truncated = false;
|
|
17658
17890
|
for (const file of files) {
|
|
17659
|
-
const absPath =
|
|
17660
|
-
const
|
|
17661
|
-
if (!
|
|
17662
|
-
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) {
|
|
17663
17895
|
truncated = true;
|
|
17664
17896
|
results.push(
|
|
17665
|
-
`--- ${file} (skipped: ${
|
|
17897
|
+
`--- ${file} (skipped: ${stat19.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
|
|
17666
17898
|
);
|
|
17667
17899
|
continue;
|
|
17668
17900
|
}
|
|
@@ -17674,7 +17906,10 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
17674
17906
|
diff: results.join("\n\n"),
|
|
17675
17907
|
files,
|
|
17676
17908
|
truncated,
|
|
17677
|
-
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
|
|
17678
17913
|
};
|
|
17679
17914
|
}
|
|
17680
17915
|
function formatWithLineNumbers(file, lines) {
|
|
@@ -17687,11 +17922,12 @@ ${numbered}`;
|
|
|
17687
17922
|
// src/document.ts
|
|
17688
17923
|
init_util();
|
|
17689
17924
|
import * as fs23 from "node:fs/promises";
|
|
17925
|
+
import * as path28 from "node:path";
|
|
17690
17926
|
var documentTool = {
|
|
17691
17927
|
name: "document",
|
|
17692
17928
|
category: "Project",
|
|
17693
|
-
description: "DEPRECATED \u2014
|
|
17694
|
-
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).",
|
|
17695
17931
|
permission: "auto",
|
|
17696
17932
|
mutating: false,
|
|
17697
17933
|
timeoutMs: 3e4,
|
|
@@ -17727,7 +17963,11 @@ var documentTool = {
|
|
|
17727
17963
|
const results = [];
|
|
17728
17964
|
let filesProcessed = 0;
|
|
17729
17965
|
let itemsDocumented = 0;
|
|
17730
|
-
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)] : [];
|
|
17731
17971
|
for (const absPath of fileList) {
|
|
17732
17972
|
try {
|
|
17733
17973
|
const content = await fs23.readFile(absPath, "utf8");
|
|
@@ -17760,14 +18000,21 @@ var documentTool = {
|
|
|
17760
18000
|
};
|
|
17761
18001
|
}
|
|
17762
18002
|
};
|
|
17763
|
-
async function resolveFiles(filesInput, cwd) {
|
|
17764
|
-
const files =
|
|
18003
|
+
async function resolveFiles(filesInput, cwd, ctx) {
|
|
18004
|
+
const files = filesInput.split(",");
|
|
17765
18005
|
const resolved = [];
|
|
17766
18006
|
for (const f of files) {
|
|
17767
|
-
const
|
|
18007
|
+
const entry = f.trim();
|
|
18008
|
+
if (!entry) continue;
|
|
18009
|
+
let absPath;
|
|
18010
|
+
try {
|
|
18011
|
+
absPath = ensureInsideRoot(path28.resolve(cwd, entry), ctx);
|
|
18012
|
+
} catch {
|
|
18013
|
+
continue;
|
|
18014
|
+
}
|
|
17768
18015
|
try {
|
|
17769
|
-
const
|
|
17770
|
-
if (
|
|
18016
|
+
const stat19 = await fs23.stat(absPath);
|
|
18017
|
+
if (stat19.isFile()) resolved.push(absPath);
|
|
17771
18018
|
} catch {
|
|
17772
18019
|
}
|
|
17773
18020
|
}
|
|
@@ -17837,7 +18084,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
|
|
|
17837
18084
|
// src/e2e.ts
|
|
17838
18085
|
init_util();
|
|
17839
18086
|
import { open, readdir as readdir7 } from "node:fs/promises";
|
|
17840
|
-
import * as
|
|
18087
|
+
import * as path29 from "node:path";
|
|
17841
18088
|
async function readBoundedText(filePath, maxBytes) {
|
|
17842
18089
|
let handle;
|
|
17843
18090
|
try {
|
|
@@ -17887,8 +18134,8 @@ var MAX_PACKAGE_BYTES = 512 * 1024;
|
|
|
17887
18134
|
var MAX_CONFIG_BYTES = 512 * 1024;
|
|
17888
18135
|
var MAX_SPEC_SAMPLES = 20;
|
|
17889
18136
|
function relativePath(root, target) {
|
|
17890
|
-
const value =
|
|
17891
|
-
return value.split(
|
|
18137
|
+
const value = path29.relative(root, target) || ".";
|
|
18138
|
+
return value.split(path29.sep).join("/");
|
|
17892
18139
|
}
|
|
17893
18140
|
function escapeRegExp(value) {
|
|
17894
18141
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -17960,7 +18207,7 @@ async function scanWorkspace(root, maxDepth, signal) {
|
|
|
17960
18207
|
}
|
|
17961
18208
|
for (const entry of entries) {
|
|
17962
18209
|
signal.throwIfAborted();
|
|
17963
|
-
const absolutePath =
|
|
18210
|
+
const absolutePath = path29.join(current.directory, entry.name);
|
|
17964
18211
|
if (entry.isFile()) {
|
|
17965
18212
|
if (entry.name === "package.json") result.packageFiles.push(absolutePath);
|
|
17966
18213
|
const framework = CONFIG_NAMES.get(entry.name);
|
|
@@ -18021,9 +18268,9 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
|
|
|
18021
18268
|
if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
|
|
18022
18269
|
if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
|
|
18023
18270
|
if (directory === scanRoot) break;
|
|
18024
|
-
const parent =
|
|
18025
|
-
const relativeParent =
|
|
18026
|
-
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)) {
|
|
18027
18274
|
break;
|
|
18028
18275
|
}
|
|
18029
18276
|
directory = parent;
|
|
@@ -18068,7 +18315,7 @@ function isSpec(framework, filename) {
|
|
|
18068
18315
|
return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
|
|
18069
18316
|
}
|
|
18070
18317
|
async function collectSpecs(root, framework, testDirectory, signal) {
|
|
18071
|
-
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")];
|
|
18072
18319
|
const samples = [];
|
|
18073
18320
|
let count = 0;
|
|
18074
18321
|
let scanned = 0;
|
|
@@ -18087,7 +18334,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
|
|
|
18087
18334
|
}
|
|
18088
18335
|
for (const entry of entries) {
|
|
18089
18336
|
signal.throwIfAborted();
|
|
18090
|
-
const target =
|
|
18337
|
+
const target = path29.join(directory, entry.name);
|
|
18091
18338
|
if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
|
|
18092
18339
|
else if (entry.isFile() && isSpec(framework, entry.name)) {
|
|
18093
18340
|
count += 1;
|
|
@@ -18107,7 +18354,7 @@ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
|
|
|
18107
18354
|
const found = packagesByDirectory.get(directory);
|
|
18108
18355
|
if (found) return found;
|
|
18109
18356
|
if (directory === scanRoot) return void 0;
|
|
18110
|
-
const parent =
|
|
18357
|
+
const parent = path29.dirname(directory);
|
|
18111
18358
|
if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
|
|
18112
18359
|
directory = parent;
|
|
18113
18360
|
}
|
|
@@ -18118,13 +18365,13 @@ async function discoverE2EProjects(root, options) {
|
|
|
18118
18365
|
const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
|
|
18119
18366
|
(info) => Boolean(info)
|
|
18120
18367
|
);
|
|
18121
|
-
const packagesByDirectory = new Map(packages.map((info) => [
|
|
18368
|
+
const packagesByDirectory = new Map(packages.map((info) => [path29.dirname(info.path), info]));
|
|
18122
18369
|
const candidates = /* @__PURE__ */ new Map();
|
|
18123
18370
|
for (const config of scan.configs) {
|
|
18124
18371
|
if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
|
|
18125
18372
|
continue;
|
|
18126
18373
|
}
|
|
18127
|
-
const projectRoot =
|
|
18374
|
+
const projectRoot = path29.dirname(config.absolutePath);
|
|
18128
18375
|
candidates.set(`${config.framework}:${projectRoot}`, {
|
|
18129
18376
|
framework: config.framework,
|
|
18130
18377
|
root: projectRoot,
|
|
@@ -18132,7 +18379,7 @@ async function discoverE2EProjects(root, options) {
|
|
|
18132
18379
|
});
|
|
18133
18380
|
}
|
|
18134
18381
|
for (const info of packages) {
|
|
18135
|
-
const projectRoot =
|
|
18382
|
+
const projectRoot = path29.dirname(info.path);
|
|
18136
18383
|
for (const framework of frameworkFromPackage(info)) {
|
|
18137
18384
|
if (options.framework && options.framework !== "all" && framework !== options.framework)
|
|
18138
18385
|
continue;
|
|
@@ -18148,10 +18395,10 @@ async function discoverE2EProjects(root, options) {
|
|
|
18148
18395
|
const scripts = matchingScripts(info, candidate.framework);
|
|
18149
18396
|
const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
|
|
18150
18397
|
const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
|
|
18151
|
-
const resolvedTestDirectory = testDirectory ?
|
|
18152
|
-
const relativeTestDirectory = resolvedTestDirectory ?
|
|
18398
|
+
const resolvedTestDirectory = testDirectory ? path29.resolve(candidate.root, testDirectory) : void 0;
|
|
18399
|
+
const relativeTestDirectory = resolvedTestDirectory ? path29.relative(root, resolvedTestDirectory) : void 0;
|
|
18153
18400
|
const unsafeTestDirectory = Boolean(
|
|
18154
|
-
relativeTestDirectory && (relativeTestDirectory.startsWith("..") ||
|
|
18401
|
+
relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path29.isAbsolute(relativeTestDirectory))
|
|
18155
18402
|
);
|
|
18156
18403
|
const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
|
|
18157
18404
|
const warnings = [];
|
|
@@ -18259,7 +18506,7 @@ import {
|
|
|
18259
18506
|
toStyle,
|
|
18260
18507
|
unifiedDiff
|
|
18261
18508
|
} from "@wrongstack/core/utils";
|
|
18262
|
-
import { ToolValidationError } from "@wrongstack/core/types";
|
|
18509
|
+
import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
|
|
18263
18510
|
|
|
18264
18511
|
// src/_edit-match.ts
|
|
18265
18512
|
var TIER_LABEL = {
|
|
@@ -18487,7 +18734,7 @@ function prefixSimilarity(a, b) {
|
|
|
18487
18734
|
}
|
|
18488
18735
|
|
|
18489
18736
|
// src/_syntax-check.ts
|
|
18490
|
-
import * as
|
|
18737
|
+
import * as path30 from "node:path";
|
|
18491
18738
|
var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
18492
18739
|
var MAX_CHECK_CHARS = 15e5;
|
|
18493
18740
|
var MAX_ERRORS = 5;
|
|
@@ -18511,15 +18758,15 @@ async function checkSyntax(filePath, content, previousContent) {
|
|
|
18511
18758
|
return { errors, preExisting };
|
|
18512
18759
|
}
|
|
18513
18760
|
function isJsoncFile(filePath) {
|
|
18514
|
-
const base =
|
|
18761
|
+
const base = path30.basename(filePath).toLowerCase();
|
|
18515
18762
|
if (base.endsWith(".jsonc")) return true;
|
|
18516
18763
|
if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
|
|
18517
|
-
const dir =
|
|
18764
|
+
const dir = path30.basename(path30.dirname(filePath)).toLowerCase();
|
|
18518
18765
|
return dir === ".vscode";
|
|
18519
18766
|
}
|
|
18520
18767
|
async function parseErrors(filePath, content) {
|
|
18521
18768
|
if (content.length > MAX_CHECK_CHARS) return void 0;
|
|
18522
|
-
const ext =
|
|
18769
|
+
const ext = path30.extname(filePath).toLowerCase();
|
|
18523
18770
|
if (ext === ".json" || ext === ".jsonc") {
|
|
18524
18771
|
try {
|
|
18525
18772
|
JSON.parse(content);
|
|
@@ -18546,7 +18793,7 @@ async function parseErrors(filePath, content) {
|
|
|
18546
18793
|
ts2.ScriptKind.JSX
|
|
18547
18794
|
);
|
|
18548
18795
|
const sourceFile = ts2.createSourceFile(
|
|
18549
|
-
|
|
18796
|
+
path30.basename(filePath),
|
|
18550
18797
|
content,
|
|
18551
18798
|
ts2.ScriptTarget.Latest,
|
|
18552
18799
|
/* setParentNodes */
|
|
@@ -18573,6 +18820,7 @@ function formatDiag(ts2, diag, content, sourceFile) {
|
|
|
18573
18820
|
|
|
18574
18821
|
// src/edit.ts
|
|
18575
18822
|
init_util();
|
|
18823
|
+
var MAX_DIFF_BYTES = 262144;
|
|
18576
18824
|
var editTool = {
|
|
18577
18825
|
name: "edit",
|
|
18578
18826
|
category: "Filesystem",
|
|
@@ -18583,46 +18831,62 @@ var editTool = {
|
|
|
18583
18831
|
useInstead: ["write", "patch"]
|
|
18584
18832
|
},
|
|
18585
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",
|
|
18586
18837
|
mutating: true,
|
|
18587
18838
|
capabilities: ["fs.write"],
|
|
18588
18839
|
icon: "edit",
|
|
18589
18840
|
timeoutMs: 5e3,
|
|
18841
|
+
maxOutputBytes: 262144,
|
|
18590
18842
|
inputSchema: {
|
|
18591
18843
|
type: "object",
|
|
18592
18844
|
properties: {
|
|
18593
|
-
path: {
|
|
18594
|
-
|
|
18595
|
-
|
|
18596
|
-
|
|
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
|
+
}
|
|
18597
18861
|
},
|
|
18598
18862
|
required: ["path", "old_string", "new_string"]
|
|
18599
18863
|
},
|
|
18600
18864
|
async execute(input, ctx, opts) {
|
|
18601
18865
|
if (!input?.path) {
|
|
18602
|
-
throw new
|
|
18866
|
+
throw new ToolValidationError3({ message: "edit: path is required", field: "path" });
|
|
18603
18867
|
}
|
|
18604
18868
|
if (input.old_string === void 0) {
|
|
18605
|
-
throw new
|
|
18869
|
+
throw new ToolValidationError3({
|
|
18606
18870
|
message: "edit: old_string is required",
|
|
18607
18871
|
field: "old_string"
|
|
18608
18872
|
});
|
|
18609
18873
|
}
|
|
18610
18874
|
if (input.new_string === void 0) {
|
|
18611
|
-
throw new
|
|
18875
|
+
throw new ToolValidationError3({
|
|
18612
18876
|
message: "edit: new_string is required",
|
|
18613
18877
|
field: "new_string"
|
|
18614
18878
|
});
|
|
18615
18879
|
}
|
|
18616
18880
|
if (input.old_string === "") {
|
|
18617
|
-
throw new
|
|
18881
|
+
throw new ToolValidationError3({
|
|
18618
18882
|
message: "edit: old_string cannot be empty",
|
|
18619
18883
|
field: "old_string"
|
|
18620
18884
|
});
|
|
18621
18885
|
}
|
|
18622
18886
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
18623
|
-
const
|
|
18887
|
+
const stat19 = await fs24.stat(absPath).catch((err) => {
|
|
18624
18888
|
if (err.code === "ENOENT") {
|
|
18625
|
-
throw new
|
|
18889
|
+
throw new ToolValidationError3({
|
|
18626
18890
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
18627
18891
|
field: "path",
|
|
18628
18892
|
context: { exists: false }
|
|
@@ -18630,8 +18894,8 @@ var editTool = {
|
|
|
18630
18894
|
}
|
|
18631
18895
|
throw err;
|
|
18632
18896
|
});
|
|
18633
|
-
if (!
|
|
18634
|
-
throw new
|
|
18897
|
+
if (!stat19.isFile()) {
|
|
18898
|
+
throw new ToolValidationError3({
|
|
18635
18899
|
message: `edit: "${input.path}" is not a regular file`,
|
|
18636
18900
|
field: "path"
|
|
18637
18901
|
});
|
|
@@ -18644,7 +18908,7 @@ var editTool = {
|
|
|
18644
18908
|
const lastReadHash = ctx.lastReadHash?.(absPath);
|
|
18645
18909
|
if (lastReadHash !== void 0) {
|
|
18646
18910
|
if (lastReadHash !== originalHash) {
|
|
18647
|
-
throw new
|
|
18911
|
+
throw new ToolValidationError3({
|
|
18648
18912
|
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
18649
18913
|
field: "path",
|
|
18650
18914
|
context: { reason: "external_modification" }
|
|
@@ -18653,15 +18917,15 @@ var editTool = {
|
|
|
18653
18917
|
} else {
|
|
18654
18918
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
18655
18919
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
18656
|
-
throw new
|
|
18920
|
+
throw new ToolValidationError3({
|
|
18657
18921
|
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
18658
18922
|
field: "path",
|
|
18659
18923
|
context: { reason: "external_modification" }
|
|
18660
18924
|
});
|
|
18661
18925
|
}
|
|
18662
18926
|
}
|
|
18663
|
-
if (autoRead && updated.mtimeMs >
|
|
18664
|
-
throw new
|
|
18927
|
+
if (autoRead && updated.mtimeMs > stat19.mtimeMs + mtimeTolerance) {
|
|
18928
|
+
throw new ToolValidationError3({
|
|
18665
18929
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
18666
18930
|
field: "path",
|
|
18667
18931
|
context: { reason: "auto_read_race" }
|
|
@@ -18673,6 +18937,9 @@ var editTool = {
|
|
|
18673
18937
|
const oldLf = normalizeToLf(input.old_string);
|
|
18674
18938
|
const newLf = normalizeToLf(input.new_string);
|
|
18675
18939
|
if (oldLf === newLf) {
|
|
18940
|
+
if (!fileLf.includes(oldLf)) {
|
|
18941
|
+
throw noMatchError(input.path, fileLf, oldLf);
|
|
18942
|
+
}
|
|
18676
18943
|
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
|
|
18677
18944
|
return {
|
|
18678
18945
|
path: absPath,
|
|
@@ -18686,26 +18953,20 @@ var editTool = {
|
|
|
18686
18953
|
const ladder = findLadderMatches(fileLf, oldLf);
|
|
18687
18954
|
if (!ladder) {
|
|
18688
18955
|
opts?.signal?.throwIfAborted();
|
|
18689
|
-
|
|
18690
|
-
throw new ToolValidationError({
|
|
18691
|
-
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
|
|
18692
|
-
${hint.snippet}
|
|
18693
|
-
Compare this against your old_string and retry with the file's actual text.` : ""}`,
|
|
18694
|
-
field: "old_string"
|
|
18695
|
-
});
|
|
18956
|
+
throw noMatchError(input.path, fileLf, oldLf);
|
|
18696
18957
|
}
|
|
18697
18958
|
const { tier, matches } = ladder;
|
|
18698
18959
|
const count = matches.length;
|
|
18699
18960
|
if (ladder.ambiguous) {
|
|
18700
18961
|
const lines = matches.map((m) => m.startLine);
|
|
18701
|
-
throw new
|
|
18962
|
+
throw new ToolValidationError3({
|
|
18702
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.`,
|
|
18703
18964
|
field: "old_string",
|
|
18704
18965
|
context: { occurrences: count, matchTier: tier }
|
|
18705
18966
|
});
|
|
18706
18967
|
}
|
|
18707
18968
|
if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
|
|
18708
|
-
throw new
|
|
18969
|
+
throw new ToolValidationError3({
|
|
18709
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.`,
|
|
18710
18971
|
field: "old_string",
|
|
18711
18972
|
context: { matchTier: tier }
|
|
@@ -18713,7 +18974,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
18713
18974
|
}
|
|
18714
18975
|
if (count > 1 && !input.replace_all) {
|
|
18715
18976
|
const lines = matches.map((m) => m.startLine);
|
|
18716
|
-
throw new
|
|
18977
|
+
throw new ToolValidationError3({
|
|
18717
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.`,
|
|
18718
18979
|
field: "old_string",
|
|
18719
18980
|
context: { occurrences: count, matchTier: tier }
|
|
@@ -18754,16 +19015,20 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
18754
19015
|
after: newFile
|
|
18755
19016
|
});
|
|
18756
19017
|
opts?.signal?.throwIfAborted();
|
|
18757
|
-
const diff =
|
|
18758
|
-
|
|
18759
|
-
|
|
18760
|
-
|
|
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;
|
|
18761
19026
|
const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
|
|
18762
19027
|
let syntaxNote;
|
|
18763
19028
|
if (syntax && syntax.errors.length > 0) {
|
|
18764
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.`;
|
|
18765
19030
|
}
|
|
18766
|
-
const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
|
|
19031
|
+
const notes = [autoReadNote, tierNote, diffNote, syntaxNote].filter(Boolean);
|
|
18767
19032
|
return {
|
|
18768
19033
|
path: absPath,
|
|
18769
19034
|
replacements: input.replace_all ? count : 1,
|
|
@@ -18774,6 +19039,15 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
18774
19039
|
};
|
|
18775
19040
|
}
|
|
18776
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
|
+
}
|
|
18777
19051
|
|
|
18778
19052
|
// src/exec.ts
|
|
18779
19053
|
import { spawn as spawn8 } from "node:child_process";
|
|
@@ -18782,7 +19056,7 @@ import {
|
|
|
18782
19056
|
emitProcessOutput as emitProcessOutput3,
|
|
18783
19057
|
emitProcessStarted as emitProcessStarted3
|
|
18784
19058
|
} from "@wrongstack/core/observability";
|
|
18785
|
-
import { toErrorMessage as
|
|
19059
|
+
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils/error";
|
|
18786
19060
|
|
|
18787
19061
|
// src/_danger-detect.ts
|
|
18788
19062
|
var argHas = (args, value) => args.includes(value);
|
|
@@ -19114,7 +19388,7 @@ init_win32_resolve();
|
|
|
19114
19388
|
|
|
19115
19389
|
// src/exec-kill-guard.ts
|
|
19116
19390
|
import * as os8 from "node:os";
|
|
19117
|
-
import * as
|
|
19391
|
+
import * as path31 from "node:path";
|
|
19118
19392
|
var isWin3 = os8.platform() === "win32";
|
|
19119
19393
|
async function checkExecKillCommand(cmd, args) {
|
|
19120
19394
|
if (!cmd) return { blocked: false };
|
|
@@ -19327,7 +19601,7 @@ async function checkKillTarget(target) {
|
|
|
19327
19601
|
reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
|
|
19328
19602
|
};
|
|
19329
19603
|
}
|
|
19330
|
-
const currentImage =
|
|
19604
|
+
const currentImage = path31.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
|
|
19331
19605
|
const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
|
|
19332
19606
|
if (targetsNodeRuntime && currentImage === "node") {
|
|
19333
19607
|
return {
|
|
@@ -19949,6 +20223,7 @@ function isExecCommandAllowed(cmd) {
|
|
|
19949
20223
|
var MAX_ARGS = 20;
|
|
19950
20224
|
var MAX_OUTPUT2 = 2e5;
|
|
19951
20225
|
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
20226
|
+
var MAX_TIMEOUT_MS = 6e5;
|
|
19952
20227
|
var BLOCKED_ARG_PATTERNS = {
|
|
19953
20228
|
python: [],
|
|
19954
20229
|
// git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
|
|
@@ -20074,8 +20349,8 @@ var SAFE_DANGER = { level: "safe", reasons: [] };
|
|
|
20074
20349
|
var execTool = {
|
|
20075
20350
|
name: "exec",
|
|
20076
20351
|
category: "Shell",
|
|
20077
|
-
description: "Execute a
|
|
20078
|
-
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.",
|
|
20079
20354
|
selection: {
|
|
20080
20355
|
doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
|
|
20081
20356
|
useInstead: ["bash"]
|
|
@@ -20091,7 +20366,13 @@ var execTool = {
|
|
|
20091
20366
|
subjectKey: "command",
|
|
20092
20367
|
mutating: true,
|
|
20093
20368
|
riskTier: "standard",
|
|
20094
|
-
|
|
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,
|
|
20095
20376
|
capabilities: ["shell.restricted"],
|
|
20096
20377
|
icon: "terminal",
|
|
20097
20378
|
inputSchema: {
|
|
@@ -20112,7 +20393,7 @@ var execTool = {
|
|
|
20112
20393
|
},
|
|
20113
20394
|
timeout: {
|
|
20114
20395
|
type: "integer",
|
|
20115
|
-
description: "Per-command timeout in milliseconds."
|
|
20396
|
+
description: "Per-command timeout in milliseconds (default 30000, max 600000)."
|
|
20116
20397
|
}
|
|
20117
20398
|
},
|
|
20118
20399
|
required: ["command"]
|
|
@@ -20156,7 +20437,7 @@ var execTool = {
|
|
|
20156
20437
|
};
|
|
20157
20438
|
}
|
|
20158
20439
|
const args = (input.args ?? []).slice(0, MAX_ARGS);
|
|
20159
|
-
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));
|
|
20160
20441
|
const danger = detectDanger(cmd, args, dangerBypass);
|
|
20161
20442
|
const killCheck = await checkExecKillCommand(cmd, args);
|
|
20162
20443
|
if (killCheck.blocked) {
|
|
@@ -20184,15 +20465,16 @@ var execTool = {
|
|
|
20184
20465
|
danger
|
|
20185
20466
|
};
|
|
20186
20467
|
}
|
|
20468
|
+
const defaultCwd = ctx.workingDir ?? ctx.cwd;
|
|
20187
20469
|
let cwd;
|
|
20188
20470
|
try {
|
|
20189
|
-
cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(
|
|
20471
|
+
cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(defaultCwd, ctx);
|
|
20190
20472
|
} catch {
|
|
20191
20473
|
return {
|
|
20192
20474
|
command: cmd,
|
|
20193
20475
|
args,
|
|
20194
20476
|
stdout: "",
|
|
20195
|
-
stderr: `cwd "${input.cwd ??
|
|
20477
|
+
stderr: `cwd "${input.cwd ?? defaultCwd}" resolves outside project root`,
|
|
20196
20478
|
exitCode: 1,
|
|
20197
20479
|
truncated: false,
|
|
20198
20480
|
allowed: false,
|
|
@@ -20204,7 +20486,7 @@ var execTool = {
|
|
|
20204
20486
|
}
|
|
20205
20487
|
};
|
|
20206
20488
|
function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
20207
|
-
return new Promise((
|
|
20489
|
+
return new Promise((resolve17) => {
|
|
20208
20490
|
let stdout = "";
|
|
20209
20491
|
let stderr = "";
|
|
20210
20492
|
let killed = false;
|
|
@@ -20212,7 +20494,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
20212
20494
|
const finish = (result) => {
|
|
20213
20495
|
if (resolvedOnce.value) return;
|
|
20214
20496
|
resolvedOnce.value = true;
|
|
20215
|
-
|
|
20497
|
+
resolve17(result);
|
|
20216
20498
|
};
|
|
20217
20499
|
const startedAt = Date.now();
|
|
20218
20500
|
let stdoutBytes = 0;
|
|
@@ -20264,7 +20546,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
20264
20546
|
command: cmd,
|
|
20265
20547
|
args,
|
|
20266
20548
|
stdout: "",
|
|
20267
|
-
stderr: `spawn failed: ${
|
|
20549
|
+
stderr: `spawn failed: ${toErrorMessage6(err)}`,
|
|
20268
20550
|
exitCode: 1,
|
|
20269
20551
|
truncated: false,
|
|
20270
20552
|
allowed: true,
|
|
@@ -20367,7 +20649,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
20367
20649
|
}
|
|
20368
20650
|
|
|
20369
20651
|
// src/fetch.ts
|
|
20370
|
-
import { FetchError as FetchError2, ToolError, ToolValidationError as
|
|
20652
|
+
import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
|
|
20371
20653
|
import TurndownService from "turndown";
|
|
20372
20654
|
|
|
20373
20655
|
// src/_fetch-guard.ts
|
|
@@ -20377,7 +20659,7 @@ import {
|
|
|
20377
20659
|
isPrivateIPv4 as isPrivateIPv42,
|
|
20378
20660
|
isPrivateIPv6 as isPrivateIPv62
|
|
20379
20661
|
} from "@wrongstack/core/utils";
|
|
20380
|
-
import { FetchError, ToolValidationError as
|
|
20662
|
+
import { FetchError, ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
20381
20663
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
20382
20664
|
var nativeGlobalFetch = globalThis.fetch;
|
|
20383
20665
|
var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
|
|
@@ -20448,13 +20730,13 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
20448
20730
|
for (; ; ) {
|
|
20449
20731
|
const parsed = new URL(currentUrl);
|
|
20450
20732
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
20451
|
-
throw new
|
|
20733
|
+
throw new ToolValidationError4({
|
|
20452
20734
|
message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
|
|
20453
20735
|
field: "url"
|
|
20454
20736
|
});
|
|
20455
20737
|
}
|
|
20456
20738
|
if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
20457
|
-
throw new
|
|
20739
|
+
throw new ToolValidationError4({
|
|
20458
20740
|
message: "fetch: redirect to http:// blocked (HTTPS required by default)",
|
|
20459
20741
|
field: "url"
|
|
20460
20742
|
});
|
|
@@ -20470,6 +20752,10 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
20470
20752
|
if (res.status < 300 || res.status > 399) {
|
|
20471
20753
|
return res;
|
|
20472
20754
|
}
|
|
20755
|
+
try {
|
|
20756
|
+
await res.body?.cancel();
|
|
20757
|
+
} catch {
|
|
20758
|
+
}
|
|
20473
20759
|
redirectCount++;
|
|
20474
20760
|
if (redirectCount > maxRedirects) {
|
|
20475
20761
|
throw new FetchError({
|
|
@@ -20493,7 +20779,7 @@ async function assertNotPrivate(hostname2) {
|
|
|
20493
20779
|
if (ALLOW_PRIVATE) return;
|
|
20494
20780
|
const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
20495
20781
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
20496
|
-
throw new
|
|
20782
|
+
throw new ToolValidationError4({
|
|
20497
20783
|
message: "fetch: blocked localhost target",
|
|
20498
20784
|
field: "url"
|
|
20499
20785
|
});
|
|
@@ -20501,14 +20787,14 @@ async function assertNotPrivate(hostname2) {
|
|
|
20501
20787
|
const ipVersion = net4.isIP(host);
|
|
20502
20788
|
if (ipVersion === 4) {
|
|
20503
20789
|
if (isPrivateIPv42(host)) {
|
|
20504
|
-
throw new
|
|
20790
|
+
throw new ToolValidationError4({
|
|
20505
20791
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
20506
20792
|
field: "url"
|
|
20507
20793
|
});
|
|
20508
20794
|
}
|
|
20509
20795
|
} else if (ipVersion === 6) {
|
|
20510
20796
|
if (isPrivateIPv62(host)) {
|
|
20511
|
-
throw new
|
|
20797
|
+
throw new ToolValidationError4({
|
|
20512
20798
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
20513
20799
|
field: "url"
|
|
20514
20800
|
});
|
|
@@ -20519,14 +20805,14 @@ async function assertNotPrivate(hostname2) {
|
|
|
20519
20805
|
for (const r of records) {
|
|
20520
20806
|
const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
|
|
20521
20807
|
if (bad) {
|
|
20522
|
-
throw new
|
|
20808
|
+
throw new ToolValidationError4({
|
|
20523
20809
|
message: `fetch: resolved to private address ${r.address}`,
|
|
20524
20810
|
field: "url"
|
|
20525
20811
|
});
|
|
20526
20812
|
}
|
|
20527
20813
|
}
|
|
20528
20814
|
} catch (err) {
|
|
20529
|
-
if (err instanceof
|
|
20815
|
+
if (err instanceof ToolValidationError4) throw err;
|
|
20530
20816
|
}
|
|
20531
20817
|
}
|
|
20532
20818
|
}
|
|
@@ -20543,6 +20829,8 @@ TD.addRule("stripDangerousElements", {
|
|
|
20543
20829
|
filter: ["script", "style", "noscript"],
|
|
20544
20830
|
replacement: () => ""
|
|
20545
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()));
|
|
20546
20834
|
var MAX_BYTES = 131072;
|
|
20547
20835
|
var TIMEOUT_MS = 2e4;
|
|
20548
20836
|
var combineSignals = (signals) => AbortSignal.any(signals);
|
|
@@ -20572,7 +20860,7 @@ var fetchTool = {
|
|
|
20572
20860
|
format: {
|
|
20573
20861
|
type: "string",
|
|
20574
20862
|
enum: ["markdown", "text", "raw"],
|
|
20575
|
-
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).'
|
|
20576
20864
|
}
|
|
20577
20865
|
},
|
|
20578
20866
|
required: ["url"]
|
|
@@ -20601,20 +20889,26 @@ var fetchTool = {
|
|
|
20601
20889
|
},
|
|
20602
20890
|
async *executeStream(input, ctx, opts) {
|
|
20603
20891
|
if (!input?.url) {
|
|
20604
|
-
throw new
|
|
20892
|
+
throw new ToolValidationError5({
|
|
20605
20893
|
message: "fetch: url is required",
|
|
20606
20894
|
field: "url"
|
|
20607
20895
|
});
|
|
20608
20896
|
}
|
|
20609
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
|
+
}
|
|
20610
20904
|
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
20611
|
-
throw new
|
|
20905
|
+
throw new ToolValidationError5({
|
|
20612
20906
|
message: `fetch: unsupported protocol "${u.protocol}"`,
|
|
20613
20907
|
field: "url"
|
|
20614
20908
|
});
|
|
20615
20909
|
}
|
|
20616
20910
|
if (u.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
20617
|
-
throw new
|
|
20911
|
+
throw new ToolValidationError5({
|
|
20618
20912
|
message: "fetch: http:// blocked (HTTPS required by default)",
|
|
20619
20913
|
field: "url"
|
|
20620
20914
|
});
|
|
@@ -20804,8 +21098,9 @@ var formatTool = {
|
|
|
20804
21098
|
type: "final",
|
|
20805
21099
|
output: {
|
|
20806
21100
|
fixer: bridge.language,
|
|
20807
|
-
|
|
20808
|
-
|
|
21101
|
+
// Language-bridge runs don't report per-file counts.
|
|
21102
|
+
files_checked: void 0,
|
|
21103
|
+
files_changed: void 0,
|
|
20809
21104
|
output: normalizeCommandOutput(run.output || run.error || ""),
|
|
20810
21105
|
truncated: run.truncated
|
|
20811
21106
|
}
|
|
@@ -20832,11 +21127,14 @@ var formatTool = {
|
|
|
20832
21127
|
text: `Running ${detected}\u2026`,
|
|
20833
21128
|
data: { fixer: detected, check: !!input.check }
|
|
20834
21129
|
};
|
|
20835
|
-
const
|
|
20836
|
-
|
|
20837
|
-
if (
|
|
20838
|
-
|
|
20839
|
-
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);
|
|
20840
21138
|
}
|
|
20841
21139
|
const result = yield* spawnStream({
|
|
20842
21140
|
cmd: detected,
|
|
@@ -20845,39 +21143,70 @@ var formatTool = {
|
|
|
20845
21143
|
signal: opts.signal,
|
|
20846
21144
|
maxBytes: 1e5
|
|
20847
21145
|
});
|
|
20848
|
-
const
|
|
21146
|
+
const combinedOut = `${result.stdout}
|
|
21147
|
+
${result.stderr}`;
|
|
21148
|
+
const counts = parseFormatterCounts(detected, combinedOut);
|
|
20849
21149
|
yield {
|
|
20850
21150
|
type: "final",
|
|
20851
21151
|
output: {
|
|
20852
21152
|
fixer: detected,
|
|
20853
|
-
files_checked:
|
|
20854
|
-
files_changed: changed,
|
|
21153
|
+
files_checked: counts.checked,
|
|
21154
|
+
files_changed: counts.changed,
|
|
20855
21155
|
output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
|
|
20856
21156
|
truncated: result.truncated
|
|
20857
21157
|
}
|
|
20858
21158
|
};
|
|
20859
21159
|
}
|
|
20860
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
|
+
}
|
|
20861
21170
|
async function detectFixer(cwd) {
|
|
20862
|
-
const
|
|
20863
|
-
|
|
20864
|
-
await stat18(`${cwd}/biome.json`);
|
|
20865
|
-
return "biome";
|
|
20866
|
-
} catch {
|
|
21171
|
+
const fs35 = await import("node:fs/promises");
|
|
21172
|
+
const exists = async (file) => {
|
|
20867
21173
|
try {
|
|
20868
|
-
await
|
|
20869
|
-
return
|
|
21174
|
+
await fs35.stat(`${cwd}/${file}`);
|
|
21175
|
+
return true;
|
|
20870
21176
|
} catch {
|
|
20871
|
-
return
|
|
21177
|
+
return false;
|
|
20872
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";
|
|
20873
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 {
|
|
21201
|
+
}
|
|
21202
|
+
return "biome";
|
|
20874
21203
|
}
|
|
20875
21204
|
|
|
20876
21205
|
// src/git.ts
|
|
20877
21206
|
init_util();
|
|
20878
21207
|
import { spawn as spawn9 } from "node:child_process";
|
|
20879
21208
|
import { statSync as statSync4 } from "node:fs";
|
|
20880
|
-
import { dirname as dirname13, resolve as
|
|
21209
|
+
import { dirname as dirname13, resolve as resolve14, sep as sep6 } from "node:path";
|
|
20881
21210
|
import { assessCommitSafety } from "@wrongstack/core/coordination";
|
|
20882
21211
|
import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
|
|
20883
21212
|
var TIMEOUT_MS2 = 3e4;
|
|
@@ -21038,8 +21367,8 @@ function validateWorktreeInput(input, projectRoot) {
|
|
|
21038
21367
|
return reject(`unsafe worktree path: ${input.worktreePath}`);
|
|
21039
21368
|
}
|
|
21040
21369
|
if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
|
|
21041
|
-
const root =
|
|
21042
|
-
const abs =
|
|
21370
|
+
const root = resolve14(projectRoot);
|
|
21371
|
+
const abs = resolve14(root, input.worktreePath);
|
|
21043
21372
|
if (abs !== root && !abs.startsWith(root + sep6)) {
|
|
21044
21373
|
return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
|
|
21045
21374
|
}
|
|
@@ -21051,8 +21380,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
21051
21380
|
let dir = cwd;
|
|
21052
21381
|
for (let i = 0; i < 20; i++) {
|
|
21053
21382
|
try {
|
|
21054
|
-
const
|
|
21055
|
-
if (
|
|
21383
|
+
const stat19 = statSync4(`${dir}/.git`);
|
|
21384
|
+
if (stat19.isDirectory() || stat19.isFile()) return dir;
|
|
21056
21385
|
} catch {
|
|
21057
21386
|
}
|
|
21058
21387
|
if (dir === root) break;
|
|
@@ -21133,7 +21462,7 @@ function buildArgs(input) {
|
|
|
21133
21462
|
}
|
|
21134
21463
|
}
|
|
21135
21464
|
function runGit2(args, cwd, signal) {
|
|
21136
|
-
return new Promise((
|
|
21465
|
+
return new Promise((resolve17) => {
|
|
21137
21466
|
let stdout = "";
|
|
21138
21467
|
let stderr = "";
|
|
21139
21468
|
const child = spawn9("git", args, {
|
|
@@ -21154,7 +21483,7 @@ function runGit2(args, cwd, signal) {
|
|
|
21154
21483
|
}
|
|
21155
21484
|
});
|
|
21156
21485
|
child.on("error", (err) => {
|
|
21157
|
-
|
|
21486
|
+
resolve17({
|
|
21158
21487
|
command: args[0],
|
|
21159
21488
|
stdout: normalizeCommandOutput(stdout),
|
|
21160
21489
|
stderr: err.message,
|
|
@@ -21163,7 +21492,7 @@ function runGit2(args, cwd, signal) {
|
|
|
21163
21492
|
});
|
|
21164
21493
|
});
|
|
21165
21494
|
child.on("close", (code) => {
|
|
21166
|
-
|
|
21495
|
+
resolve17({
|
|
21167
21496
|
command: args[0],
|
|
21168
21497
|
stdout: normalizeCommandOutput(stdout),
|
|
21169
21498
|
stderr: normalizeCommandOutput(stderr),
|
|
@@ -21176,8 +21505,9 @@ function runGit2(args, cwd, signal) {
|
|
|
21176
21505
|
|
|
21177
21506
|
// src/glob.ts
|
|
21178
21507
|
import * as fs25 from "node:fs/promises";
|
|
21179
|
-
import * as
|
|
21508
|
+
import * as path32 from "node:path";
|
|
21180
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";
|
|
21181
21511
|
|
|
21182
21512
|
// src/_concurrency.ts
|
|
21183
21513
|
async function mapWithConcurrency2(items, limit, fn) {
|
|
@@ -21203,7 +21533,7 @@ var WALK_CONCURRENCY = 16;
|
|
|
21203
21533
|
var globTool = {
|
|
21204
21534
|
name: "glob",
|
|
21205
21535
|
category: "Filesystem",
|
|
21206
|
-
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.",
|
|
21207
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.",
|
|
21208
21538
|
selection: {
|
|
21209
21539
|
doNotUseWhen: "you need to search inside file contents.",
|
|
@@ -21214,7 +21544,7 @@ var globTool = {
|
|
|
21214
21544
|
capabilities: ["fs.read"],
|
|
21215
21545
|
icon: "folder",
|
|
21216
21546
|
maxOutputBytes: 65536,
|
|
21217
|
-
timeoutMs:
|
|
21547
|
+
timeoutMs: 15e3,
|
|
21218
21548
|
inputSchema: {
|
|
21219
21549
|
type: "object",
|
|
21220
21550
|
properties: {
|
|
@@ -21228,13 +21558,20 @@ var globTool = {
|
|
|
21228
21558
|
},
|
|
21229
21559
|
limit: {
|
|
21230
21560
|
type: "integer",
|
|
21231
|
-
|
|
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."
|
|
21232
21564
|
}
|
|
21233
21565
|
},
|
|
21234
21566
|
required: ["pattern"]
|
|
21235
21567
|
},
|
|
21236
21568
|
async execute(input, ctx, opts) {
|
|
21237
|
-
if (!input?.pattern)
|
|
21569
|
+
if (!input?.pattern) {
|
|
21570
|
+
throw new ToolValidationError6({
|
|
21571
|
+
message: "glob: pattern is required",
|
|
21572
|
+
field: "pattern"
|
|
21573
|
+
});
|
|
21574
|
+
}
|
|
21238
21575
|
const signal = opts?.signal;
|
|
21239
21576
|
const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
|
|
21240
21577
|
const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
|
|
@@ -21279,7 +21616,7 @@ var globTool = {
|
|
|
21279
21616
|
const name = e.name;
|
|
21280
21617
|
if (DEFAULT_IGNORE2.has(name)) continue;
|
|
21281
21618
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
21282
|
-
const full =
|
|
21619
|
+
const full = path32.join(dir, name);
|
|
21283
21620
|
if (e.isDirectory()) {
|
|
21284
21621
|
if (isGitIgnored(rel, true)) continue;
|
|
21285
21622
|
subdirs.push({ full, rel });
|
|
@@ -21329,8 +21666,8 @@ var globTool = {
|
|
|
21329
21666
|
// src/grep.ts
|
|
21330
21667
|
import { spawn as spawn10 } from "node:child_process";
|
|
21331
21668
|
import * as fs26 from "node:fs/promises";
|
|
21332
|
-
import * as
|
|
21333
|
-
import { ToolValidationError as
|
|
21669
|
+
import * as path33 from "node:path";
|
|
21670
|
+
import { ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
|
|
21334
21671
|
import {
|
|
21335
21672
|
buildChildEnv as buildChildEnv5,
|
|
21336
21673
|
compileGlob as compileGlob3,
|
|
@@ -21534,7 +21871,7 @@ var grepTool = {
|
|
|
21534
21871
|
},
|
|
21535
21872
|
async *executeStream(input, ctx, opts) {
|
|
21536
21873
|
if (!input?.pattern) {
|
|
21537
|
-
throw new
|
|
21874
|
+
throw new ToolValidationError7({
|
|
21538
21875
|
message: "grep: pattern is required",
|
|
21539
21876
|
field: "pattern"
|
|
21540
21877
|
});
|
|
@@ -21544,12 +21881,12 @@ var grepTool = {
|
|
|
21544
21881
|
const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
|
|
21545
21882
|
const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
|
|
21546
21883
|
if (!validation.ok) {
|
|
21547
|
-
throw new
|
|
21884
|
+
throw new ToolValidationError7({
|
|
21548
21885
|
message: `grep: ${validation.reason}`,
|
|
21549
21886
|
field: "pattern"
|
|
21550
21887
|
});
|
|
21551
21888
|
}
|
|
21552
|
-
const rgAvailable = await detectRg(
|
|
21889
|
+
const rgAvailable = await detectRg();
|
|
21553
21890
|
if (rgAvailable) {
|
|
21554
21891
|
try {
|
|
21555
21892
|
yield* runRgStream(input, base, mode, limit, opts.signal);
|
|
@@ -21562,16 +21899,23 @@ var grepTool = {
|
|
|
21562
21899
|
yield { type: "final", output: out };
|
|
21563
21900
|
}
|
|
21564
21901
|
};
|
|
21565
|
-
|
|
21566
|
-
|
|
21902
|
+
var rgAvailabilityCache;
|
|
21903
|
+
function detectRg() {
|
|
21904
|
+
rgAvailabilityCache ??= new Promise((resolve17) => {
|
|
21567
21905
|
try {
|
|
21568
|
-
const p = spawn10("rg", ["--version"], {
|
|
21569
|
-
|
|
21570
|
-
|
|
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));
|
|
21571
21914
|
} catch {
|
|
21572
|
-
|
|
21915
|
+
resolve17(false);
|
|
21573
21916
|
}
|
|
21574
21917
|
});
|
|
21918
|
+
return rgAvailabilityCache;
|
|
21575
21919
|
}
|
|
21576
21920
|
async function* runRgStream(input, base, mode, limit, signal) {
|
|
21577
21921
|
const args = ["--no-heading"];
|
|
@@ -21585,7 +21929,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
21585
21929
|
for (const ignored of DEFAULT_IGNORE3) {
|
|
21586
21930
|
args.push("--glob", `!${ignored}/**`, "--glob", `!**/${ignored}/**`);
|
|
21587
21931
|
}
|
|
21588
|
-
const gitignorePath =
|
|
21932
|
+
const gitignorePath = path33.join(base, ".gitignore");
|
|
21589
21933
|
if (await fs26.access(gitignorePath).then(() => true, () => false)) {
|
|
21590
21934
|
args.push("--ignore-file", gitignorePath);
|
|
21591
21935
|
}
|
|
@@ -21757,7 +22101,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
21757
22101
|
const flags = input.case_insensitive ? "i" : "";
|
|
21758
22102
|
const compiled = compileUserRegex(input.pattern, flags);
|
|
21759
22103
|
if (!compiled.ok) {
|
|
21760
|
-
throw new
|
|
22104
|
+
throw new ToolValidationError7({
|
|
21761
22105
|
message: `grep: ${compiled.reason}`,
|
|
21762
22106
|
field: "pattern"
|
|
21763
22107
|
});
|
|
@@ -21775,8 +22119,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
21775
22119
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
21776
22120
|
if (globRe) globRe.lastIndex = 0;
|
|
21777
22121
|
try {
|
|
21778
|
-
const
|
|
21779
|
-
if (!
|
|
22122
|
+
const stat19 = await fs26.stat(full);
|
|
22123
|
+
if (!stat19.isFile() || stat19.size > maxBytes || stopped || signal.aborted) return;
|
|
21780
22124
|
const file = await fs26.open(full, "r");
|
|
21781
22125
|
try {
|
|
21782
22126
|
let bytesReadTotal = 0;
|
|
@@ -21867,7 +22211,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
21867
22211
|
if (DEFAULT_IGNORE3.has(e.name)) continue;
|
|
21868
22212
|
if (e.isSymbolicLink()) continue;
|
|
21869
22213
|
const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
|
|
21870
|
-
const full =
|
|
22214
|
+
const full = path33.join(dir, e.name);
|
|
21871
22215
|
if (e.isDirectory()) {
|
|
21872
22216
|
if (isGitIgnored(rel, true)) continue;
|
|
21873
22217
|
subdirs.push({ full, rel });
|
|
@@ -21987,26 +22331,14 @@ var installTool = {
|
|
|
21987
22331
|
return;
|
|
21988
22332
|
}
|
|
21989
22333
|
}
|
|
21990
|
-
const pkgManager = await detectPackageManager(cwd);
|
|
22334
|
+
const pkgManager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
21991
22335
|
yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
|
|
21992
|
-
const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
|
|
21993
22336
|
const globalFlag = input.global ? ["-g"] : [];
|
|
21994
22337
|
const ignoreScripts = input.lifecycleScripts !== true;
|
|
21995
|
-
const args = [];
|
|
21996
|
-
if (input.dry_run) args.push("--dry-run");
|
|
21997
|
-
if (ignoreScripts) args.push("--ignore-scripts");
|
|
21998
|
-
if (pkgManager === "pnpm") {
|
|
21999
|
-
if (save) args.push(save);
|
|
22000
|
-
args.push("add", ...globalFlag);
|
|
22001
|
-
} else if (pkgManager === "yarn") {
|
|
22002
|
-
args.push("add", ...globalFlag);
|
|
22003
|
-
} else {
|
|
22004
|
-
args.push("install", ...globalFlag);
|
|
22005
|
-
}
|
|
22006
22338
|
const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
|
|
22007
22339
|
(p) => p.trim()
|
|
22008
22340
|
) : [];
|
|
22009
|
-
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;
|
|
22010
22342
|
for (const pkg of pkgList) {
|
|
22011
22343
|
if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-") || pkg.length > 200) {
|
|
22012
22344
|
yield {
|
|
@@ -22022,7 +22354,34 @@ var installTool = {
|
|
|
22022
22354
|
return;
|
|
22023
22355
|
}
|
|
22024
22356
|
}
|
|
22025
|
-
|
|
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);
|
|
22026
22385
|
yield {
|
|
22027
22386
|
type: "log",
|
|
22028
22387
|
text: `Fetching ${pkgList.length || "all"} packages\u2026`,
|
|
@@ -22103,9 +22462,9 @@ var JsonFileTooLargeError = class extends Error {
|
|
|
22103
22462
|
};
|
|
22104
22463
|
async function readJsonFileBounded(filePath, ctx) {
|
|
22105
22464
|
const resolved = await safeResolveReal(filePath, ctx);
|
|
22106
|
-
const
|
|
22107
|
-
if (
|
|
22108
|
-
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);
|
|
22109
22468
|
}
|
|
22110
22469
|
return fs27.readFile(resolved, "utf8");
|
|
22111
22470
|
}
|
|
@@ -22511,60 +22870,60 @@ function jmespathSearch(data, query) {
|
|
|
22511
22870
|
}
|
|
22512
22871
|
function validateJsonSchema(data, schema) {
|
|
22513
22872
|
const errors = [];
|
|
22514
|
-
function check(value, s,
|
|
22873
|
+
function check(value, s, path40) {
|
|
22515
22874
|
if (s["type"]) {
|
|
22516
22875
|
const expectedType = s["type"];
|
|
22517
22876
|
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
22518
22877
|
if (expectedType === "integer") {
|
|
22519
|
-
if (!Number.isInteger(value)) errors.push(`${
|
|
22878
|
+
if (!Number.isInteger(value)) errors.push(`${path40}: expected integer, got ${actualType}`);
|
|
22520
22879
|
} else if (expectedType !== actualType) {
|
|
22521
|
-
errors.push(`${
|
|
22880
|
+
errors.push(`${path40}: expected ${expectedType}, got ${actualType}`);
|
|
22522
22881
|
}
|
|
22523
22882
|
}
|
|
22524
22883
|
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
22525
22884
|
try {
|
|
22526
22885
|
new URL(value);
|
|
22527
22886
|
} catch {
|
|
22528
|
-
errors.push(`${
|
|
22887
|
+
errors.push(`${path40}: not a valid URI`);
|
|
22529
22888
|
}
|
|
22530
22889
|
}
|
|
22531
22890
|
if (typeof value === "string" && s["pattern"]) {
|
|
22532
22891
|
const compiled = compileUserRegex(s["pattern"], "");
|
|
22533
22892
|
if (!compiled.ok) {
|
|
22534
|
-
errors.push(`${
|
|
22893
|
+
errors.push(`${path40}: invalid schema pattern \u2014 ${compiled.reason}`);
|
|
22535
22894
|
} else if (!compiled.regex.test(capSubject(value))) {
|
|
22536
|
-
errors.push(`${
|
|
22895
|
+
errors.push(`${path40}: does not match pattern ${s["pattern"]}`);
|
|
22537
22896
|
}
|
|
22538
22897
|
}
|
|
22539
22898
|
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
22540
|
-
errors.push(`${
|
|
22899
|
+
errors.push(`${path40}: string too short (min ${s["minLength"]})`);
|
|
22541
22900
|
}
|
|
22542
22901
|
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
22543
|
-
errors.push(`${
|
|
22902
|
+
errors.push(`${path40}: string too long (max ${s["maxLength"]})`);
|
|
22544
22903
|
}
|
|
22545
22904
|
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
22546
|
-
errors.push(`${
|
|
22905
|
+
errors.push(`${path40}: below minimum ${s["minimum"]}`);
|
|
22547
22906
|
}
|
|
22548
22907
|
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
22549
|
-
errors.push(`${
|
|
22908
|
+
errors.push(`${path40}: above maximum ${s["maximum"]}`);
|
|
22550
22909
|
}
|
|
22551
22910
|
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
22552
22911
|
for (let i = 0; i < value.length; i++) {
|
|
22553
|
-
check(value[i], s["items"], `${
|
|
22912
|
+
check(value[i], s["items"], `${path40}[${i}]`);
|
|
22554
22913
|
}
|
|
22555
22914
|
}
|
|
22556
22915
|
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
22557
22916
|
const props = s["properties"];
|
|
22558
22917
|
for (const [k, propSchema] of Object.entries(props)) {
|
|
22559
|
-
check(value[k], propSchema, `${
|
|
22918
|
+
check(value[k], propSchema, `${path40}.${k}`);
|
|
22560
22919
|
}
|
|
22561
22920
|
}
|
|
22562
22921
|
}
|
|
22563
22922
|
check(data, schema, "$");
|
|
22564
22923
|
return { valid: errors.length === 0, errors };
|
|
22565
22924
|
}
|
|
22566
|
-
function simpleQuery(data,
|
|
22567
|
-
const parts =
|
|
22925
|
+
function simpleQuery(data, path40) {
|
|
22926
|
+
const parts = path40.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
22568
22927
|
let current = data;
|
|
22569
22928
|
for (const part of parts) {
|
|
22570
22929
|
if (current === null || current === void 0) return void 0;
|
|
@@ -23475,6 +23834,18 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
23475
23834
|
},
|
|
23476
23835
|
transitionAction: { type: "string" },
|
|
23477
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
|
+
},
|
|
23478
23849
|
attachmentUrl: { type: "string" },
|
|
23479
23850
|
attachmentTitle: { type: "string" },
|
|
23480
23851
|
attachmentType: {
|
|
@@ -24076,6 +24447,7 @@ function sourceStatus(task) {
|
|
|
24076
24447
|
function todoStatus(task) {
|
|
24077
24448
|
const status = sourceStatus(task);
|
|
24078
24449
|
if (status === "completed") return "completed";
|
|
24450
|
+
if (status === "review" && task.assignment?.status === "completed") return "completed";
|
|
24079
24451
|
if (status === "in_progress" || status === "review") return "in_progress";
|
|
24080
24452
|
return "pending";
|
|
24081
24453
|
}
|
|
@@ -24175,6 +24547,9 @@ var kanbanTool = {
|
|
|
24175
24547
|
description: KANBAN_TOOL_DESCRIPTION,
|
|
24176
24548
|
usageHint: KANBAN_TOOL_USAGE_HINT,
|
|
24177
24549
|
permission: "confirm",
|
|
24550
|
+
// WS-046: gives permission decisions something to key on.
|
|
24551
|
+
// The action performed; kanban has no single file or path subject.
|
|
24552
|
+
subjectKey: "action",
|
|
24178
24553
|
mutating: true,
|
|
24179
24554
|
capabilities: ["fs.write"],
|
|
24180
24555
|
icon: "task",
|
|
@@ -24616,6 +24991,7 @@ var kanbanTool = {
|
|
|
24616
24991
|
actor: input.author,
|
|
24617
24992
|
comment: input.transitionComment,
|
|
24618
24993
|
...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
|
|
24994
|
+
...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
|
|
24619
24995
|
...input.attachmentUrl !== void 0 ? {
|
|
24620
24996
|
attachment: {
|
|
24621
24997
|
url: input.attachmentUrl,
|
|
@@ -25008,8 +25384,52 @@ var kanbanTool = {
|
|
|
25008
25384
|
} catch (err) {
|
|
25009
25385
|
return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
|
|
25010
25386
|
}
|
|
25387
|
+
},
|
|
25388
|
+
serialize(output, input) {
|
|
25389
|
+
return serializeKanbanOutput(output, input);
|
|
25011
25390
|
}
|
|
25012
25391
|
};
|
|
25392
|
+
var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
|
|
25393
|
+
var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
|
|
25394
|
+
"get_board",
|
|
25395
|
+
"export_markdown",
|
|
25396
|
+
"export_task_graph"
|
|
25397
|
+
]);
|
|
25398
|
+
function serializeKanbanOutput(output, input) {
|
|
25399
|
+
const action = input && typeof input === "object" ? input.action : void 0;
|
|
25400
|
+
const board = output.board;
|
|
25401
|
+
if (board) {
|
|
25402
|
+
const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
|
|
25403
|
+
let boardBytes = 0;
|
|
25404
|
+
if (!keepFull) {
|
|
25405
|
+
try {
|
|
25406
|
+
boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
|
|
25407
|
+
} catch {
|
|
25408
|
+
boardBytes = 0;
|
|
25409
|
+
}
|
|
25410
|
+
}
|
|
25411
|
+
if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
|
|
25412
|
+
const columns = {};
|
|
25413
|
+
for (const column of board.columns) {
|
|
25414
|
+
columns[column.title || column.id] = board.tasks.filter(
|
|
25415
|
+
(task) => task.columnId === column.id
|
|
25416
|
+
).length;
|
|
25417
|
+
}
|
|
25418
|
+
const compact = {
|
|
25419
|
+
...output,
|
|
25420
|
+
board: {
|
|
25421
|
+
id: board.id,
|
|
25422
|
+
title: board.title,
|
|
25423
|
+
columns,
|
|
25424
|
+
totalTasks: board.tasks.length,
|
|
25425
|
+
note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
|
|
25426
|
+
}
|
|
25427
|
+
};
|
|
25428
|
+
return JSON.stringify(compact, null, 2);
|
|
25429
|
+
}
|
|
25430
|
+
}
|
|
25431
|
+
return JSON.stringify(output, null, 2);
|
|
25432
|
+
}
|
|
25013
25433
|
|
|
25014
25434
|
// src/builtin.ts
|
|
25015
25435
|
init_execute_tool();
|
|
@@ -25124,11 +25544,11 @@ var lintTool = {
|
|
|
25124
25544
|
}
|
|
25125
25545
|
};
|
|
25126
25546
|
async function detectLinter(cwd) {
|
|
25127
|
-
const { stat:
|
|
25547
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
25128
25548
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
25129
25549
|
for (const f of checks) {
|
|
25130
25550
|
try {
|
|
25131
|
-
await
|
|
25551
|
+
await stat19(`${cwd}/${f}`);
|
|
25132
25552
|
if (f.includes("biome")) return "biome";
|
|
25133
25553
|
if (f.includes("eslint")) return "eslint";
|
|
25134
25554
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -25145,11 +25565,12 @@ init_util();
|
|
|
25145
25565
|
var logsTool = {
|
|
25146
25566
|
name: "logs",
|
|
25147
25567
|
category: "Logs",
|
|
25148
|
-
description: "Read
|
|
25149
|
-
usageHint: "DEBUGGING TOOL \u2014 USE CAREFULLY IN AUTONOMOUS MODE:\n\n- Prefer `path` for local files or `service` for containers
|
|
25568
|
+
description: "Read logs from files or Docker containers. Useful for debugging running applications.",
|
|
25569
|
+
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.",
|
|
25150
25570
|
permission: "confirm",
|
|
25151
25571
|
mutating: false,
|
|
25152
25572
|
timeoutMs: 3e4,
|
|
25573
|
+
maxOutputBytes: 262144,
|
|
25153
25574
|
capabilities: ["shell.restricted"],
|
|
25154
25575
|
icon: "logs",
|
|
25155
25576
|
inputSchema: {
|
|
@@ -25157,7 +25578,7 @@ var logsTool = {
|
|
|
25157
25578
|
properties: {
|
|
25158
25579
|
service: {
|
|
25159
25580
|
type: "string",
|
|
25160
|
-
description: "
|
|
25581
|
+
description: "Docker container name (passed to `docker logs`)"
|
|
25161
25582
|
},
|
|
25162
25583
|
path: {
|
|
25163
25584
|
type: "string",
|
|
@@ -25169,10 +25590,6 @@ var logsTool = {
|
|
|
25169
25590
|
minimum: 0,
|
|
25170
25591
|
maximum: 1e4
|
|
25171
25592
|
},
|
|
25172
|
-
stream: {
|
|
25173
|
-
type: "boolean",
|
|
25174
|
-
description: "Stream logs continuously (like tail -f) (default: false)"
|
|
25175
|
-
},
|
|
25176
25593
|
filter: {
|
|
25177
25594
|
type: "string",
|
|
25178
25595
|
description: "Regex pattern to filter log lines"
|
|
@@ -25180,7 +25597,7 @@ var logsTool = {
|
|
|
25180
25597
|
since: {
|
|
25181
25598
|
type: "string",
|
|
25182
25599
|
enum: ["1h", "6h", "24h", "all"],
|
|
25183
|
-
description:
|
|
25600
|
+
description: 'Only show Docker logs since duration (ignored for files; "all" = no limit)'
|
|
25184
25601
|
},
|
|
25185
25602
|
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
25186
25603
|
}
|
|
@@ -25197,10 +25614,10 @@ var logsTool = {
|
|
|
25197
25614
|
filterRe = compiled.regex;
|
|
25198
25615
|
}
|
|
25199
25616
|
if (input.service) {
|
|
25200
|
-
return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);
|
|
25617
|
+
return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal, input.since);
|
|
25201
25618
|
}
|
|
25202
25619
|
if (input.path) {
|
|
25203
|
-
return await fileLogs(
|
|
25620
|
+
return await fileLogs(await safeResolveReal(input.path, ctx), lines, filterRe);
|
|
25204
25621
|
}
|
|
25205
25622
|
return {
|
|
25206
25623
|
source: "none",
|
|
@@ -25214,7 +25631,7 @@ var logsTool = {
|
|
|
25214
25631
|
async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
25215
25632
|
const args = ["logs"];
|
|
25216
25633
|
if (lines > 0) args.push("--tail", String(lines));
|
|
25217
|
-
if (since) {
|
|
25634
|
+
if (since && since !== "all") {
|
|
25218
25635
|
const sinceMap = { "1h": "1h", "6h": "6h", "24h": "24h" };
|
|
25219
25636
|
args.push("--since", sinceMap[since] ?? "1h");
|
|
25220
25637
|
}
|
|
@@ -25228,7 +25645,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25228
25645
|
};
|
|
25229
25646
|
}
|
|
25230
25647
|
args.push("--timestamps", service);
|
|
25231
|
-
return new Promise((
|
|
25648
|
+
return new Promise((resolve17) => {
|
|
25232
25649
|
let stdout = "";
|
|
25233
25650
|
let stderr = "";
|
|
25234
25651
|
const MAX = 2e5;
|
|
@@ -25244,7 +25661,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25244
25661
|
if (settled) return;
|
|
25245
25662
|
settled = true;
|
|
25246
25663
|
clearTimeout(timer);
|
|
25247
|
-
|
|
25664
|
+
resolve17(result);
|
|
25248
25665
|
};
|
|
25249
25666
|
const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
25250
25667
|
const timer = setTimeout(() => {
|
|
@@ -25279,7 +25696,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25279
25696
|
}
|
|
25280
25697
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
25281
25698
|
var MAX_TAIL_LINES = 1e5;
|
|
25282
|
-
async function fileLogs(
|
|
25699
|
+
async function fileLogs(path40, lines, filterRe) {
|
|
25283
25700
|
const { createInterface } = await import("node:readline");
|
|
25284
25701
|
const { createReadStream: createReadStream2 } = await import("node:fs");
|
|
25285
25702
|
const entries = [];
|
|
@@ -25288,7 +25705,7 @@ async function fileLogs(path39, lines, filterRe, stream) {
|
|
|
25288
25705
|
let writeIdx = 0;
|
|
25289
25706
|
let totalLines = 0;
|
|
25290
25707
|
const rl = createInterface({
|
|
25291
|
-
input: createReadStream2(
|
|
25708
|
+
input: createReadStream2(path40),
|
|
25292
25709
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
25293
25710
|
});
|
|
25294
25711
|
for await (const line of rl) {
|
|
@@ -25309,11 +25726,11 @@ async function fileLogs(path39, lines, filterRe, stream) {
|
|
|
25309
25726
|
if (parsed) entries.push(parsed);
|
|
25310
25727
|
}
|
|
25311
25728
|
return {
|
|
25312
|
-
source:
|
|
25729
|
+
source: path40,
|
|
25313
25730
|
entries,
|
|
25314
25731
|
total: entries.length,
|
|
25315
25732
|
truncated: totalLines > effLines,
|
|
25316
|
-
stream_mode:
|
|
25733
|
+
stream_mode: false
|
|
25317
25734
|
};
|
|
25318
25735
|
}
|
|
25319
25736
|
function parseLogLines(output, filterRe) {
|
|
@@ -25389,25 +25806,12 @@ var outdatedTool = {
|
|
|
25389
25806
|
inputSchema: {
|
|
25390
25807
|
type: "object",
|
|
25391
25808
|
properties: {
|
|
25392
|
-
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
25393
|
-
format: {
|
|
25394
|
-
type: "string",
|
|
25395
|
-
enum: ["list", "table"],
|
|
25396
|
-
description: "Output format (default: list)"
|
|
25397
|
-
},
|
|
25398
|
-
include_deprecated: {
|
|
25399
|
-
type: "boolean",
|
|
25400
|
-
description: "Include deprecated packages (default: false)"
|
|
25401
|
-
},
|
|
25402
|
-
check: {
|
|
25403
|
-
type: "string",
|
|
25404
|
-
description: "Specific package(s) to check (comma-separated)"
|
|
25405
|
-
}
|
|
25809
|
+
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
25406
25810
|
}
|
|
25407
25811
|
},
|
|
25408
25812
|
async execute(input, ctx, opts) {
|
|
25409
25813
|
const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
|
|
25410
|
-
const manager = await detectPackageManager(cwd);
|
|
25814
|
+
const manager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
25411
25815
|
if (manager === "npm") {
|
|
25412
25816
|
try {
|
|
25413
25817
|
const { detectNonJsEcosystem: detectNonJsEcosystem2 } = await Promise.resolve().then(() => (init_legacy_bridge(), legacy_bridge_exports));
|
|
@@ -25460,13 +25864,11 @@ var outdatedTool = {
|
|
|
25460
25864
|
}
|
|
25461
25865
|
}
|
|
25462
25866
|
const args = ["outdated", "--json"];
|
|
25463
|
-
if (input.format === "table") args.push("--table");
|
|
25464
|
-
if (input.include_deprecated) args.push("--include", "deprecated");
|
|
25465
25867
|
return runOutdated(manager, args, cwd, opts.signal);
|
|
25466
25868
|
}
|
|
25467
25869
|
};
|
|
25468
25870
|
function runOutdated(manager, args, cwd, signal) {
|
|
25469
|
-
return new Promise((
|
|
25871
|
+
return new Promise((resolve17) => {
|
|
25470
25872
|
let stdout = "";
|
|
25471
25873
|
let stderr = "";
|
|
25472
25874
|
const MAX = 1e5;
|
|
@@ -25491,10 +25893,10 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
25491
25893
|
});
|
|
25492
25894
|
child.on("close", (code) => {
|
|
25493
25895
|
const result = parseOutdatedOutput(stdout, code ?? 0);
|
|
25494
|
-
|
|
25896
|
+
resolve17(result);
|
|
25495
25897
|
});
|
|
25496
25898
|
child.on("error", (e) => {
|
|
25497
|
-
|
|
25899
|
+
resolve17({
|
|
25498
25900
|
exit_code: 1,
|
|
25499
25901
|
packages: [],
|
|
25500
25902
|
total: 0,
|
|
@@ -25515,27 +25917,39 @@ function parseOutdatedOutput(json2, exitCode) {
|
|
|
25515
25917
|
truncated: false
|
|
25516
25918
|
};
|
|
25517
25919
|
}
|
|
25920
|
+
const truncated = json2.length >= 1e5 || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
|
|
25921
|
+
let parsedOk = false;
|
|
25518
25922
|
try {
|
|
25519
25923
|
const data = JSON.parse(json2);
|
|
25924
|
+
parsedOk = true;
|
|
25520
25925
|
for (const name of Object.keys(data)) {
|
|
25521
|
-
const info = data[name];
|
|
25926
|
+
const info = data[name] ?? {};
|
|
25927
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
25522
25928
|
packages.push({
|
|
25523
25929
|
name,
|
|
25524
|
-
current: info
|
|
25525
|
-
latest: info
|
|
25526
|
-
wanted: info
|
|
25527
|
-
|
|
25528
|
-
|
|
25930
|
+
current: str(info["current"]) ?? "unknown",
|
|
25931
|
+
latest: str(info["latest"]) ?? "unknown",
|
|
25932
|
+
wanted: str(info["wanted"]) ?? "unknown",
|
|
25933
|
+
// npm calls it `type`; pnpm calls it `dependencyType`.
|
|
25934
|
+
type: str(info["type"]) ?? str(info["dependencyType"]) ?? "unknown",
|
|
25935
|
+
location: str(info["location"]) ?? name
|
|
25529
25936
|
});
|
|
25530
25937
|
}
|
|
25531
25938
|
} catch {
|
|
25939
|
+
}
|
|
25940
|
+
const outdatedFound = parsedOk && exitCode === 1;
|
|
25941
|
+
let output = normalizeCommandOutput(json2);
|
|
25942
|
+
if (outdatedFound) {
|
|
25943
|
+
output = `${output}
|
|
25944
|
+
|
|
25945
|
+
Note: exit code 1 from \`outdated\` means outdated packages were found (expected); treated as success.`;
|
|
25532
25946
|
}
|
|
25533
25947
|
return {
|
|
25534
|
-
exit_code: exitCode,
|
|
25948
|
+
exit_code: outdatedFound ? 0 : exitCode,
|
|
25535
25949
|
packages,
|
|
25536
25950
|
total: packages.length,
|
|
25537
|
-
output
|
|
25538
|
-
truncated
|
|
25951
|
+
output,
|
|
25952
|
+
truncated
|
|
25539
25953
|
};
|
|
25540
25954
|
}
|
|
25541
25955
|
|
|
@@ -25544,8 +25958,8 @@ init_util();
|
|
|
25544
25958
|
import { spawn as spawn13 } from "node:child_process";
|
|
25545
25959
|
import * as fs28 from "node:fs/promises";
|
|
25546
25960
|
import * as os9 from "node:os";
|
|
25547
|
-
import * as
|
|
25548
|
-
import { buildChildEnv as buildChildEnv8, toErrorMessage as
|
|
25961
|
+
import * as path34 from "node:path";
|
|
25962
|
+
import { buildChildEnv as buildChildEnv8, toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
|
|
25549
25963
|
var patchTool = {
|
|
25550
25964
|
name: "patch",
|
|
25551
25965
|
category: "Filesystem",
|
|
@@ -25589,26 +26003,26 @@ var patchTool = {
|
|
|
25589
26003
|
try {
|
|
25590
26004
|
dir = input.directory ? await safeResolveReal(input.directory, ctx) : ctx.cwd;
|
|
25591
26005
|
} catch (err) {
|
|
25592
|
-
return refuse(`patch refused: ${
|
|
26006
|
+
return refuse(`patch refused: ${toErrorMessage7(err)}`);
|
|
25593
26007
|
}
|
|
25594
|
-
const realRoot = await fs28.realpath(ctx.projectRoot).catch(() =>
|
|
26008
|
+
const realRoot = await fs28.realpath(ctx.projectRoot).catch(() => path34.resolve(ctx.projectRoot));
|
|
25595
26009
|
const targets = extractDiffTargets(input.patch);
|
|
25596
26010
|
const resolvedTargets = [];
|
|
25597
26011
|
for (const t of targets) {
|
|
25598
26012
|
const stripped = stripPathComponents(t.raw, strip);
|
|
25599
26013
|
if (!stripped) continue;
|
|
25600
|
-
if (
|
|
26014
|
+
if (path34.isAbsolute(stripped)) {
|
|
25601
26015
|
return refuse(`patch refused: target "${t.raw}" strips to absolute path`);
|
|
25602
26016
|
}
|
|
25603
|
-
const candidate =
|
|
26017
|
+
const candidate = path34.resolve(dir, stripped);
|
|
25604
26018
|
let real;
|
|
25605
26019
|
try {
|
|
25606
26020
|
real = await resolveRealInsideRoot(candidate, ctx);
|
|
25607
26021
|
} catch (err) {
|
|
25608
|
-
return refuse(`patch refused: target "${t.raw}" ${
|
|
26022
|
+
return refuse(`patch refused: target "${t.raw}" ${toErrorMessage7(err)}`);
|
|
25609
26023
|
}
|
|
25610
|
-
const rel =
|
|
25611
|
-
if (rel.startsWith("..") ||
|
|
26024
|
+
const rel = path34.relative(realRoot, real);
|
|
26025
|
+
if (rel.startsWith("..") || path34.isAbsolute(rel)) {
|
|
25612
26026
|
return refuse(`patch refused: target "${t.raw}" resolves outside project root`);
|
|
25613
26027
|
}
|
|
25614
26028
|
resolvedTargets.push({ raw: t.raw, deleted: t.deleted, abs: real });
|
|
@@ -25622,11 +26036,11 @@ var patchTool = {
|
|
|
25622
26036
|
beforeContents.set(target.abs, await readTextForTracking(target.abs));
|
|
25623
26037
|
}
|
|
25624
26038
|
}
|
|
25625
|
-
const tmpDir = await fs28.mkdtemp(
|
|
26039
|
+
const tmpDir = await fs28.mkdtemp(path34.join(os9.tmpdir(), ".wstack_patch_"));
|
|
25626
26040
|
try {
|
|
25627
26041
|
await fs28.chmod(tmpDir, 448).catch(() => {
|
|
25628
26042
|
});
|
|
25629
|
-
const patchFile =
|
|
26043
|
+
const patchFile = path34.join(tmpDir, "in.diff");
|
|
25630
26044
|
await fs28.writeFile(patchFile, input.patch, { mode: 384 });
|
|
25631
26045
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
25632
26046
|
const result = await runPatch(args, dir, opts.signal, {
|
|
@@ -25639,8 +26053,8 @@ var patchTool = {
|
|
|
25639
26053
|
for (const target of resolvedTargets) {
|
|
25640
26054
|
const abs = target.abs;
|
|
25641
26055
|
const before = beforeContents.get(abs) ?? null;
|
|
25642
|
-
const
|
|
25643
|
-
if (!
|
|
26056
|
+
const stat19 = await fs28.stat(abs).catch(() => null);
|
|
26057
|
+
if (!stat19?.isFile()) {
|
|
25644
26058
|
if (beforeExisted.has(abs)) {
|
|
25645
26059
|
touched.push(abs);
|
|
25646
26060
|
ctx.session?.recordFileChange?.({
|
|
@@ -25655,7 +26069,7 @@ var patchTool = {
|
|
|
25655
26069
|
const after = await readTextForTracking(abs);
|
|
25656
26070
|
if (after === null || after === before) continue;
|
|
25657
26071
|
touched.push(abs);
|
|
25658
|
-
ctx.recordRead?.(abs,
|
|
26072
|
+
ctx.recordRead?.(abs, stat19.mtimeMs, "write", sha256hex(after));
|
|
25659
26073
|
ctx.session?.recordFileChange?.({
|
|
25660
26074
|
path: abs,
|
|
25661
26075
|
action: before === null ? "created" : "modified",
|
|
@@ -25666,7 +26080,7 @@ var patchTool = {
|
|
|
25666
26080
|
}
|
|
25667
26081
|
if (result.exitCode !== 0) {
|
|
25668
26082
|
if (!dryRun) {
|
|
25669
|
-
const partial = touched.length > 0 ? ` ${touched.length} file(s) were still modified on disk and have been recorded for rewind: ${touched.map((p) =>
|
|
26083
|
+
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(", ")}.` : "";
|
|
25670
26084
|
return {
|
|
25671
26085
|
applied: touched.length,
|
|
25672
26086
|
rejected: 1,
|
|
@@ -25674,7 +26088,7 @@ var patchTool = {
|
|
|
25674
26088
|
// success path (which returns GNU patch's dir-relative names).
|
|
25675
26089
|
// `touched` entries are realpaths from resolveRealInsideRoot, and
|
|
25676
26090
|
// realRoot is also a realpath, so path.relative is like-for-like.
|
|
25677
|
-
files: touched.map((p) =>
|
|
26091
|
+
files: touched.map((p) => path34.relative(realRoot, p) || p),
|
|
25678
26092
|
dry_run: dryRun,
|
|
25679
26093
|
message: `patch failed: ${result.stderr || result.stdout}${partial}`
|
|
25680
26094
|
};
|
|
@@ -25690,7 +26104,7 @@ var patchTool = {
|
|
|
25690
26104
|
}
|
|
25691
26105
|
const patched = result.engine === "git" ? [
|
|
25692
26106
|
...new Set(
|
|
25693
|
-
resolvedTargets.map((target) =>
|
|
26107
|
+
resolvedTargets.map((target) => path34.relative(dir, target.abs) || target.abs)
|
|
25694
26108
|
)
|
|
25695
26109
|
] : extractPatchedFiles(result.stdout);
|
|
25696
26110
|
return {
|
|
@@ -25709,8 +26123,8 @@ var patchTool = {
|
|
|
25709
26123
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
25710
26124
|
async function readTextForTracking(absPath) {
|
|
25711
26125
|
try {
|
|
25712
|
-
const
|
|
25713
|
-
if (!
|
|
26126
|
+
const stat19 = await fs28.stat(absPath);
|
|
26127
|
+
if (!stat19.isFile() || stat19.size > MAX_TRACKING_BYTES) return null;
|
|
25714
26128
|
const buf = await fs28.readFile(absPath);
|
|
25715
26129
|
if (buf.includes(0)) return null;
|
|
25716
26130
|
return buf.toString("utf8");
|
|
@@ -25795,7 +26209,7 @@ function runPatch(args, cwd, signal, fallback) {
|
|
|
25795
26209
|
});
|
|
25796
26210
|
}
|
|
25797
26211
|
function runPatchProcess(command, args, cwd, signal) {
|
|
25798
|
-
return new Promise((
|
|
26212
|
+
return new Promise((resolve17) => {
|
|
25799
26213
|
let stdout = "";
|
|
25800
26214
|
let stderr = "";
|
|
25801
26215
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
@@ -25814,11 +26228,11 @@ function runPatchProcess(command, args, cwd, signal) {
|
|
|
25814
26228
|
});
|
|
25815
26229
|
child.on(
|
|
25816
26230
|
"close",
|
|
25817
|
-
(code) =>
|
|
26231
|
+
(code) => resolve17({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
|
|
25818
26232
|
);
|
|
25819
26233
|
child.on(
|
|
25820
26234
|
"error",
|
|
25821
|
-
(e) =>
|
|
26235
|
+
(e) => resolve17({
|
|
25822
26236
|
exitCode: 1,
|
|
25823
26237
|
stdout: "",
|
|
25824
26238
|
stderr: e.message,
|
|
@@ -25985,6 +26399,20 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
|
25985
26399
|
}
|
|
25986
26400
|
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
25987
26401
|
if (!task || task.status === "completed") continue;
|
|
26402
|
+
const stage = task.lifecycle?.currentStage;
|
|
26403
|
+
if (stage === "backlog" || stage === "todo") {
|
|
26404
|
+
const started = await execute({
|
|
26405
|
+
action: "start_task",
|
|
26406
|
+
boardId: board.id,
|
|
26407
|
+
taskId: task.id,
|
|
26408
|
+
author: actor,
|
|
26409
|
+
agentId: actor,
|
|
26410
|
+
transitionComment: `Auto-started for completion: ${item.content}`
|
|
26411
|
+
});
|
|
26412
|
+
if (!started.ok) {
|
|
26413
|
+
continue;
|
|
26414
|
+
}
|
|
26415
|
+
}
|
|
25988
26416
|
await execute({
|
|
25989
26417
|
action: "mark_assignment",
|
|
25990
26418
|
boardId: board.id,
|
|
@@ -26196,7 +26624,8 @@ var todoTool = {
|
|
|
26196
26624
|
}
|
|
26197
26625
|
for (const planId of completedPlanIds) {
|
|
26198
26626
|
if (pendingPlanIds.has(planId)) continue;
|
|
26199
|
-
const
|
|
26627
|
+
const meta = ctx.meta;
|
|
26628
|
+
const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
|
|
26200
26629
|
if (typeof planPath !== "string" || !planPath) continue;
|
|
26201
26630
|
try {
|
|
26202
26631
|
const plan = await loadPlan2(planPath);
|
|
@@ -26209,7 +26638,8 @@ var todoTool = {
|
|
|
26209
26638
|
}
|
|
26210
26639
|
for (const taskId of completedTaskIds) {
|
|
26211
26640
|
if (pendingTaskIds.has(taskId)) continue;
|
|
26212
|
-
const
|
|
26641
|
+
const meta = ctx.meta;
|
|
26642
|
+
const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
|
|
26213
26643
|
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
26214
26644
|
try {
|
|
26215
26645
|
const file = await loadTasks3(taskPath);
|
|
@@ -26325,7 +26755,16 @@ var planTool = {
|
|
|
26325
26755
|
sessionPlanPath.lastIndexOf("/"),
|
|
26326
26756
|
sessionPlanPath.lastIndexOf("\\")
|
|
26327
26757
|
);
|
|
26328
|
-
|
|
26758
|
+
if (lastSep < 0) {
|
|
26759
|
+
return {
|
|
26760
|
+
ok: false,
|
|
26761
|
+
message: `Cannot derive the project-scoped plan path: session plan path "${sessionPlanPath}" has no directory component.`,
|
|
26762
|
+
plan: "",
|
|
26763
|
+
count: 0,
|
|
26764
|
+
open: 0
|
|
26765
|
+
};
|
|
26766
|
+
}
|
|
26767
|
+
planPath = sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json";
|
|
26329
26768
|
}
|
|
26330
26769
|
} else {
|
|
26331
26770
|
planPath = sessionPlanPath;
|
|
@@ -26518,6 +26957,7 @@ var planTool = {
|
|
|
26518
26957
|
open: 0
|
|
26519
26958
|
};
|
|
26520
26959
|
}
|
|
26960
|
+
ctx.meta["plan.path.resolved"] = planPath;
|
|
26521
26961
|
await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
|
|
26522
26962
|
if (todosToReplace) {
|
|
26523
26963
|
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
@@ -26550,6 +26990,7 @@ var planTool = {
|
|
|
26550
26990
|
});
|
|
26551
26991
|
return f;
|
|
26552
26992
|
});
|
|
26993
|
+
ctx.meta["task.path.resolved"] = taskPath;
|
|
26553
26994
|
return mkResult(
|
|
26554
26995
|
plan,
|
|
26555
26996
|
true,
|
|
@@ -26583,14 +27024,14 @@ function mkResult(plan, ok, message, todos) {
|
|
|
26583
27024
|
// src/read.ts
|
|
26584
27025
|
init_util();
|
|
26585
27026
|
import * as fs29 from "node:fs/promises";
|
|
26586
|
-
import { FsError, ToolValidationError as
|
|
26587
|
-
import { toErrorMessage as
|
|
27027
|
+
import { FsError, ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
|
|
27028
|
+
import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
|
|
26588
27029
|
var ADVANCED_MODE_META_KEY = "tools.read.advancedMode";
|
|
26589
27030
|
var MAX_BYTES2 = 5 * 1024 * 1024;
|
|
26590
27031
|
var readTool = {
|
|
26591
27032
|
name: "read",
|
|
26592
27033
|
category: "Filesystem",
|
|
26593
|
-
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
|
|
27034
|
+
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).",
|
|
26594
27035
|
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.",
|
|
26595
27036
|
selection: {
|
|
26596
27037
|
doNotUseWhen: "you need to search many files for matching content.",
|
|
@@ -26611,11 +27052,13 @@ var readTool = {
|
|
|
26611
27052
|
},
|
|
26612
27053
|
offset: {
|
|
26613
27054
|
type: "integer",
|
|
27055
|
+
minimum: 1,
|
|
26614
27056
|
description: "1-based starting line number. Use together with `limit` for large files."
|
|
26615
27057
|
},
|
|
26616
27058
|
limit: {
|
|
26617
27059
|
type: "integer",
|
|
26618
|
-
|
|
27060
|
+
minimum: 0,
|
|
27061
|
+
description: "Maximum number of lines to return (default 2000). Values above 5000 are clamped to 5000 \u2014 page with `offset` for more."
|
|
26619
27062
|
},
|
|
26620
27063
|
mode: {
|
|
26621
27064
|
type: "string",
|
|
@@ -26631,16 +27074,16 @@ var readTool = {
|
|
|
26631
27074
|
},
|
|
26632
27075
|
async execute(input, ctx, execOpts) {
|
|
26633
27076
|
if (!input?.path) {
|
|
26634
|
-
throw new
|
|
27077
|
+
throw new ToolValidationError8({
|
|
26635
27078
|
message: "read: path is required",
|
|
26636
27079
|
field: "path"
|
|
26637
27080
|
});
|
|
26638
27081
|
}
|
|
26639
27082
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
26640
27083
|
const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
|
|
26641
|
-
let
|
|
27084
|
+
let stat19;
|
|
26642
27085
|
try {
|
|
26643
|
-
|
|
27086
|
+
stat19 = await fs29.stat(absPath);
|
|
26644
27087
|
} catch (err) {
|
|
26645
27088
|
const code = err.code;
|
|
26646
27089
|
if (code === "ENOENT") {
|
|
@@ -26652,14 +27095,14 @@ var readTool = {
|
|
|
26652
27095
|
});
|
|
26653
27096
|
}
|
|
26654
27097
|
throw new FsError({
|
|
26655
|
-
message: `read: failed to stat "${input.path}": ${
|
|
27098
|
+
message: `read: failed to stat "${input.path}": ${toErrorMessage8(err)}`,
|
|
26656
27099
|
code: "FS_READ_FAILED",
|
|
26657
27100
|
path: absPath,
|
|
26658
27101
|
context: { errno: code },
|
|
26659
27102
|
cause: err
|
|
26660
27103
|
});
|
|
26661
27104
|
}
|
|
26662
|
-
if (!
|
|
27105
|
+
if (!stat19.isFile()) {
|
|
26663
27106
|
throw new FsError({
|
|
26664
27107
|
message: `read: "${input.path}" is not a regular file`,
|
|
26665
27108
|
code: "FS_READ_FAILED",
|
|
@@ -26667,23 +27110,23 @@ var readTool = {
|
|
|
26667
27110
|
context: { reason: "not-a-regular-file" }
|
|
26668
27111
|
});
|
|
26669
27112
|
}
|
|
26670
|
-
if (
|
|
27113
|
+
if (stat19.size > MAX_BYTES2) {
|
|
26671
27114
|
throw new FsError({
|
|
26672
|
-
message: `read: file too large (${
|
|
27115
|
+
message: `read: file too large (${stat19.size} bytes, limit ${MAX_BYTES2})`,
|
|
26673
27116
|
code: "FS_READ_FAILED",
|
|
26674
27117
|
path: absPath,
|
|
26675
|
-
context: { size:
|
|
27118
|
+
context: { size: stat19.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
26676
27119
|
});
|
|
26677
27120
|
}
|
|
26678
27121
|
const offset = Math.max(1, input.offset ?? 1);
|
|
26679
27122
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
26680
27123
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
26681
27124
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
26682
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
26683
|
-
ctx.recordRead(absPath,
|
|
27125
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat19.mtimeMs, offset, requestedEnd)) {
|
|
27126
|
+
ctx.recordRead(absPath, stat19.mtimeMs);
|
|
26684
27127
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
26685
27128
|
return {
|
|
26686
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
27129
|
+
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.]`,
|
|
26687
27130
|
total_lines: prior.totalLines,
|
|
26688
27131
|
encoding: "utf8",
|
|
26689
27132
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -26694,18 +27137,23 @@ var readTool = {
|
|
|
26694
27137
|
}
|
|
26695
27138
|
const buf = await fs29.readFile(absPath);
|
|
26696
27139
|
if (isBinaryBuffer(buf)) {
|
|
26697
|
-
throw new
|
|
27140
|
+
throw new FsError({
|
|
27141
|
+
message: `read: "${input.path}" appears to be binary`,
|
|
27142
|
+
code: "FS_READ_FAILED",
|
|
27143
|
+
path: absPath,
|
|
27144
|
+
context: { reason: "binary" }
|
|
27145
|
+
});
|
|
26698
27146
|
}
|
|
26699
27147
|
const text = buf.toString("utf8");
|
|
26700
27148
|
const contentHash = sha256hex(text);
|
|
26701
27149
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
26702
27150
|
const total = allLines.length;
|
|
26703
27151
|
if (input.mode === "summary") {
|
|
26704
|
-
ctx.recordRead(absPath,
|
|
26705
|
-
rememberReadRange(ctx, absPath,
|
|
27152
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
27153
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, Math.min(total, 200));
|
|
26706
27154
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
26707
27155
|
return {
|
|
26708
|
-
text: summarizeFile(input.path,
|
|
27156
|
+
text: summarizeFile(input.path, stat19.size, allLines),
|
|
26709
27157
|
total_lines: total,
|
|
26710
27158
|
encoding: "utf8",
|
|
26711
27159
|
truncated: total > 200,
|
|
@@ -26717,8 +27165,8 @@ var readTool = {
|
|
|
26717
27165
|
};
|
|
26718
27166
|
}
|
|
26719
27167
|
if (limit === 0) {
|
|
26720
|
-
ctx.recordRead(absPath,
|
|
26721
|
-
rememberReadRange(ctx, absPath,
|
|
27168
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
27169
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, 0);
|
|
26722
27170
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
26723
27171
|
return {
|
|
26724
27172
|
text: "",
|
|
@@ -26730,8 +27178,8 @@ var readTool = {
|
|
|
26730
27178
|
};
|
|
26731
27179
|
}
|
|
26732
27180
|
if (offset > total) {
|
|
26733
|
-
ctx.recordRead(absPath,
|
|
26734
|
-
rememberReadRange(ctx, absPath,
|
|
27181
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
27182
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, total + 1, total + 1);
|
|
26735
27183
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
26736
27184
|
return {
|
|
26737
27185
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
@@ -26746,8 +27194,8 @@ var readTool = {
|
|
|
26746
27194
|
const truncated = offset - 1 + slice.length < total;
|
|
26747
27195
|
const width = String(offset + slice.length - 1).length;
|
|
26748
27196
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
26749
|
-
ctx.recordRead(absPath,
|
|
26750
|
-
rememberReadRange(ctx, absPath,
|
|
27197
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
27198
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, offset, offset + slice.length - 1);
|
|
26751
27199
|
const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
26752
27200
|
return {
|
|
26753
27201
|
text: numbered,
|
|
@@ -26855,8 +27303,8 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
26855
27303
|
// src/replace.ts
|
|
26856
27304
|
import { spawn as spawn14 } from "node:child_process";
|
|
26857
27305
|
import * as fs30 from "node:fs/promises";
|
|
26858
|
-
import * as
|
|
26859
|
-
import { ToolValidationError as
|
|
27306
|
+
import * as path35 from "node:path";
|
|
27307
|
+
import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
|
|
26860
27308
|
import {
|
|
26861
27309
|
atomicWrite as atomicWrite3,
|
|
26862
27310
|
buildChildEnv as buildChildEnv9,
|
|
@@ -26868,12 +27316,13 @@ import {
|
|
|
26868
27316
|
unifiedDiff as unifiedDiff2
|
|
26869
27317
|
} from "@wrongstack/core/utils";
|
|
26870
27318
|
init_util();
|
|
27319
|
+
var MAX_DIFF_BYTES2 = 262144;
|
|
26871
27320
|
var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
26872
27321
|
var replaceTool = {
|
|
26873
27322
|
name: "replace",
|
|
26874
27323
|
category: "Transform",
|
|
26875
27324
|
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.",
|
|
26876
|
-
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.",
|
|
27325
|
+
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.",
|
|
26877
27326
|
permission: "confirm",
|
|
26878
27327
|
// WS-046: gives permission decisions something to key on.
|
|
26879
27328
|
// The file scope being rewritten, not the pattern: a trust rule should say
|
|
@@ -26883,11 +27332,15 @@ var replaceTool = {
|
|
|
26883
27332
|
capabilities: ["fs.write"],
|
|
26884
27333
|
icon: "edit",
|
|
26885
27334
|
timeoutMs: 3e4,
|
|
27335
|
+
maxOutputBytes: 262144,
|
|
26886
27336
|
inputSchema: {
|
|
26887
27337
|
type: "object",
|
|
26888
27338
|
properties: {
|
|
26889
27339
|
pattern: { type: "string", description: "Regex pattern to match" },
|
|
26890
|
-
replacement: {
|
|
27340
|
+
replacement: {
|
|
27341
|
+
type: "string",
|
|
27342
|
+
description: "Replacement string. Supports `$1`\u2013`$9` (capture groups), `$&` (whole match), and `$$` (literal dollar sign) \u2014 same semantics as JavaScript String.replace."
|
|
27343
|
+
},
|
|
26891
27344
|
files: {
|
|
26892
27345
|
type: "string",
|
|
26893
27346
|
description: "File(s) to target: single path, comma-separated list, or glob pattern"
|
|
@@ -26903,19 +27356,19 @@ var replaceTool = {
|
|
|
26903
27356
|
},
|
|
26904
27357
|
async execute(input, ctx) {
|
|
26905
27358
|
if (!input?.pattern) {
|
|
26906
|
-
throw new
|
|
27359
|
+
throw new ToolValidationError9({
|
|
26907
27360
|
message: "replace: pattern is required",
|
|
26908
27361
|
field: "pattern"
|
|
26909
27362
|
});
|
|
26910
27363
|
}
|
|
26911
27364
|
if (input.replacement === void 0) {
|
|
26912
|
-
throw new
|
|
27365
|
+
throw new ToolValidationError9({
|
|
26913
27366
|
message: "replace: replacement is required",
|
|
26914
27367
|
field: "replacement"
|
|
26915
27368
|
});
|
|
26916
27369
|
}
|
|
26917
27370
|
if (!input?.files) {
|
|
26918
|
-
throw new
|
|
27371
|
+
throw new ToolValidationError9({
|
|
26919
27372
|
message: "replace: files is required",
|
|
26920
27373
|
field: "files"
|
|
26921
27374
|
});
|
|
@@ -26923,7 +27376,7 @@ var replaceTool = {
|
|
|
26923
27376
|
const replaceAll = input.replace_all ?? true;
|
|
26924
27377
|
const compiled = compileUserRegex(input.pattern, "g");
|
|
26925
27378
|
if (!compiled.ok) {
|
|
26926
|
-
throw new
|
|
27379
|
+
throw new ToolValidationError9({
|
|
26927
27380
|
message: `replace: ${compiled.reason}`,
|
|
26928
27381
|
field: "pattern"
|
|
26929
27382
|
});
|
|
@@ -26936,6 +27389,9 @@ var replaceTool = {
|
|
|
26936
27389
|
const realRoot = await fs30.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
26937
27390
|
const results = [];
|
|
26938
27391
|
let totalReplacements = 0;
|
|
27392
|
+
let diffBytesUsed = 0;
|
|
27393
|
+
let diffsOmitted = 0;
|
|
27394
|
+
let diffsTruncated = 0;
|
|
26939
27395
|
for (const absPath of fileList) {
|
|
26940
27396
|
const lstat2 = await fs30.lstat(absPath).catch((err) => {
|
|
26941
27397
|
if (err.code === "ENOENT") return null;
|
|
@@ -26949,10 +27405,10 @@ var replaceTool = {
|
|
|
26949
27405
|
} catch {
|
|
26950
27406
|
continue;
|
|
26951
27407
|
}
|
|
26952
|
-
const rel =
|
|
26953
|
-
if (rel.startsWith("..") ||
|
|
26954
|
-
const
|
|
26955
|
-
if (!
|
|
27408
|
+
const rel = path35.relative(realRoot, realPath);
|
|
27409
|
+
if (rel.startsWith("..") || path35.isAbsolute(rel)) continue;
|
|
27410
|
+
const stat19 = await fs30.stat(realPath).catch(() => null);
|
|
27411
|
+
if (!stat19?.isFile()) continue;
|
|
26956
27412
|
let content;
|
|
26957
27413
|
try {
|
|
26958
27414
|
const buf = await fs30.readFile(realPath);
|
|
@@ -26971,13 +27427,13 @@ var replaceTool = {
|
|
|
26971
27427
|
let newContentLf = contentLf;
|
|
26972
27428
|
for (let i = matches.length - 1; i >= 0; i--) {
|
|
26973
27429
|
const m = expectDefined8(matches[i]);
|
|
26974
|
-
newContentLf = newContentLf.slice(0, m.index) + input.replacement + newContentLf.slice(expectDefined8(m.index) + m[0].length);
|
|
27430
|
+
newContentLf = newContentLf.slice(0, m.index) + expandReplacement(input.replacement, m) + newContentLf.slice(expectDefined8(m.index) + m[0].length);
|
|
26975
27431
|
}
|
|
26976
27432
|
re.lastIndex = 0;
|
|
26977
27433
|
totalReplacements += count;
|
|
26978
27434
|
if (!dryRun) {
|
|
26979
27435
|
const newContent = toStyle2(newContentLf, style);
|
|
26980
|
-
await atomicWrite3(realPath, newContent, { mode:
|
|
27436
|
+
await atomicWrite3(realPath, newContent, { mode: stat19.mode & 511 });
|
|
26981
27437
|
const written = await fs30.stat(realPath).catch(() => null);
|
|
26982
27438
|
if (written) {
|
|
26983
27439
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
@@ -26989,24 +27445,76 @@ var replaceTool = {
|
|
|
26989
27445
|
after: newContent
|
|
26990
27446
|
});
|
|
26991
27447
|
}
|
|
26992
|
-
|
|
27448
|
+
let diff = dryRun || matches.length > 0 ? unifiedDiff2(content, toStyle2(newContentLf, style), {
|
|
26993
27449
|
fromFile: absPath,
|
|
26994
27450
|
toFile: absPath
|
|
26995
27451
|
}) : void 0;
|
|
27452
|
+
if (diff !== void 0) {
|
|
27453
|
+
const remaining = MAX_DIFF_BYTES2 - diffBytesUsed;
|
|
27454
|
+
if (remaining <= 0) {
|
|
27455
|
+
diff = void 0;
|
|
27456
|
+
diffsOmitted++;
|
|
27457
|
+
} else {
|
|
27458
|
+
const capped = truncateDiffPayload(diff, remaining);
|
|
27459
|
+
if (capped.truncated) diffsTruncated++;
|
|
27460
|
+
diff = capped.text;
|
|
27461
|
+
diffBytesUsed += Buffer.byteLength(diff, "utf8");
|
|
27462
|
+
}
|
|
27463
|
+
}
|
|
26996
27464
|
results.push({
|
|
26997
27465
|
path: absPath,
|
|
26998
27466
|
replacements: matches.length,
|
|
26999
27467
|
diff
|
|
27000
27468
|
});
|
|
27001
27469
|
}
|
|
27470
|
+
const overBudget = diffsOmitted > 0 || diffsTruncated > 0;
|
|
27002
27471
|
return {
|
|
27003
27472
|
files_modified: results.length,
|
|
27004
27473
|
total_replacements: totalReplacements,
|
|
27005
27474
|
results,
|
|
27006
|
-
dry_run: dryRun
|
|
27475
|
+
dry_run: dryRun,
|
|
27476
|
+
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
|
|
27007
27477
|
};
|
|
27008
27478
|
}
|
|
27009
27479
|
};
|
|
27480
|
+
function expandReplacement(template, match) {
|
|
27481
|
+
if (!template.includes("$")) return template;
|
|
27482
|
+
let out = "";
|
|
27483
|
+
for (let i = 0; i < template.length; i++) {
|
|
27484
|
+
const ch = template[i];
|
|
27485
|
+
if (ch !== "$") {
|
|
27486
|
+
out += ch;
|
|
27487
|
+
continue;
|
|
27488
|
+
}
|
|
27489
|
+
const next = template[i + 1];
|
|
27490
|
+
if (next === "$") {
|
|
27491
|
+
out += "$";
|
|
27492
|
+
i++;
|
|
27493
|
+
} else if (next === "&") {
|
|
27494
|
+
out += match[0];
|
|
27495
|
+
i++;
|
|
27496
|
+
} else if (next !== void 0 && next >= "1" && next <= "9") {
|
|
27497
|
+
const idx = next.charCodeAt(0) - 48;
|
|
27498
|
+
if (idx < match.length) {
|
|
27499
|
+
out += match[idx] ?? "";
|
|
27500
|
+
i++;
|
|
27501
|
+
} else {
|
|
27502
|
+
out += "$";
|
|
27503
|
+
}
|
|
27504
|
+
} else {
|
|
27505
|
+
out += "$";
|
|
27506
|
+
}
|
|
27507
|
+
}
|
|
27508
|
+
return out;
|
|
27509
|
+
}
|
|
27510
|
+
function passesExtraGlob(extraGlob, name, full) {
|
|
27511
|
+
extraGlob.lastIndex = 0;
|
|
27512
|
+
const nameMatch = extraGlob.test(name);
|
|
27513
|
+
extraGlob.lastIndex = 0;
|
|
27514
|
+
const fullMatch = extraGlob.test(full);
|
|
27515
|
+
extraGlob.lastIndex = 0;
|
|
27516
|
+
return nameMatch || fullMatch;
|
|
27517
|
+
}
|
|
27010
27518
|
async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
27011
27519
|
const base = ctx.cwd;
|
|
27012
27520
|
const normalized = filesInput.trim();
|
|
@@ -27017,8 +27525,9 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
27017
27525
|
const resolved = [];
|
|
27018
27526
|
for (const p of parts) {
|
|
27019
27527
|
const absPath = safeResolve(p, ctx);
|
|
27020
|
-
|
|
27021
|
-
|
|
27528
|
+
if (extraGlob && !passesExtraGlob(extraGlob, path35.basename(absPath), absPath)) continue;
|
|
27529
|
+
const stat19 = await fs30.stat(absPath).catch(() => null);
|
|
27530
|
+
if (stat19?.isFile()) {
|
|
27022
27531
|
resolved.push(absPath);
|
|
27023
27532
|
}
|
|
27024
27533
|
}
|
|
@@ -27029,26 +27538,32 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
27029
27538
|
if (rgAvailable) {
|
|
27030
27539
|
try {
|
|
27031
27540
|
const { promise } = spawnRgFind(pattern, base);
|
|
27032
|
-
|
|
27541
|
+
const files = await promise;
|
|
27542
|
+
if (extraGlob) {
|
|
27543
|
+
return files.filter((f) => passesExtraGlob(extraGlob, path35.basename(f), f));
|
|
27544
|
+
}
|
|
27545
|
+
return files;
|
|
27033
27546
|
} catch {
|
|
27034
27547
|
}
|
|
27035
27548
|
}
|
|
27036
27549
|
return await globNative(pattern, base, extraGlob);
|
|
27037
27550
|
}
|
|
27551
|
+
var rgAvailabilityCache2;
|
|
27038
27552
|
function checkRg() {
|
|
27039
|
-
|
|
27553
|
+
rgAvailabilityCache2 ??= new Promise((resolve17) => {
|
|
27040
27554
|
try {
|
|
27041
27555
|
const p = spawn14("rg", ["--version"], {
|
|
27042
27556
|
env: buildChildEnv9(),
|
|
27043
27557
|
stdio: "ignore",
|
|
27044
27558
|
windowsHide: true
|
|
27045
27559
|
});
|
|
27046
|
-
p.on("error", () =>
|
|
27047
|
-
p.on("close", (code) =>
|
|
27560
|
+
p.on("error", () => resolve17(false));
|
|
27561
|
+
p.on("close", (code) => resolve17(code === 0));
|
|
27048
27562
|
} catch {
|
|
27049
|
-
|
|
27563
|
+
resolve17(false);
|
|
27050
27564
|
}
|
|
27051
27565
|
});
|
|
27566
|
+
return rgAvailabilityCache2;
|
|
27052
27567
|
}
|
|
27053
27568
|
function spawnRgFind(pattern, base) {
|
|
27054
27569
|
const args = ["--files", "--glob", pattern, base];
|
|
@@ -27071,10 +27586,10 @@ function spawnRgFind(pattern, base) {
|
|
|
27071
27586
|
}
|
|
27072
27587
|
});
|
|
27073
27588
|
return {
|
|
27074
|
-
promise: new Promise((
|
|
27589
|
+
promise: new Promise((resolve17, reject) => {
|
|
27075
27590
|
child.on("error", reject);
|
|
27076
27591
|
child.on("close", () => {
|
|
27077
|
-
|
|
27592
|
+
resolve17(buf.split("\n").filter(Boolean));
|
|
27078
27593
|
});
|
|
27079
27594
|
})
|
|
27080
27595
|
};
|
|
@@ -27091,10 +27606,10 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
27091
27606
|
}
|
|
27092
27607
|
for (const e of entries) {
|
|
27093
27608
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
27094
|
-
const full =
|
|
27609
|
+
const full = path35.join(dir, e.name);
|
|
27095
27610
|
try {
|
|
27096
|
-
const
|
|
27097
|
-
if (
|
|
27611
|
+
const stat19 = await fs30.lstat(full);
|
|
27612
|
+
if (stat19.isSymbolicLink()) continue;
|
|
27098
27613
|
} catch {
|
|
27099
27614
|
continue;
|
|
27100
27615
|
}
|
|
@@ -27118,7 +27633,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
27118
27633
|
// src/scaffold.ts
|
|
27119
27634
|
init_util();
|
|
27120
27635
|
import * as fs31 from "node:fs/promises";
|
|
27121
|
-
import * as
|
|
27636
|
+
import * as path36 from "node:path";
|
|
27122
27637
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
|
|
27123
27638
|
var BUILT_IN_TEMPLATES = {
|
|
27124
27639
|
"npm-package": {
|
|
@@ -27269,16 +27784,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
27269
27784
|
let filesCreated = 0;
|
|
27270
27785
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
27271
27786
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
27272
|
-
const joinedPath =
|
|
27273
|
-
const root =
|
|
27274
|
-
const target =
|
|
27275
|
-
const rel =
|
|
27276
|
-
if (rel.startsWith("..") ||
|
|
27787
|
+
const joinedPath = path36.join(cwd, resolvedPath);
|
|
27788
|
+
const root = path36.resolve(ctx.projectRoot);
|
|
27789
|
+
const target = path36.resolve(joinedPath);
|
|
27790
|
+
const rel = path36.relative(root, target);
|
|
27791
|
+
if (rel.startsWith("..") || path36.isAbsolute(rel)) {
|
|
27277
27792
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
27278
27793
|
}
|
|
27279
27794
|
const fullPath = target;
|
|
27280
27795
|
if (!dryRun) {
|
|
27281
|
-
await fs31.mkdir(
|
|
27796
|
+
await fs31.mkdir(path36.dirname(fullPath), { recursive: true });
|
|
27282
27797
|
await atomicWrite4(fullPath, substituteVars(content, name, vars));
|
|
27283
27798
|
}
|
|
27284
27799
|
files.push(resolvedPath);
|
|
@@ -27309,11 +27824,12 @@ function substituteVars(content, name, vars) {
|
|
|
27309
27824
|
}
|
|
27310
27825
|
|
|
27311
27826
|
// src/search.ts
|
|
27312
|
-
import { FetchError as FetchError3, ToolValidationError as
|
|
27827
|
+
import { FetchError as FetchError3, ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
|
|
27313
27828
|
import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
|
|
27314
|
-
import { toErrorMessage as
|
|
27829
|
+
import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
|
|
27315
27830
|
var DEFAULT_NUM = 10;
|
|
27316
27831
|
var MAX_RESULTS = 50;
|
|
27832
|
+
var MAX_SNIPPET_CHARS = 300;
|
|
27317
27833
|
var TIMEOUT_MS3 = 15e3;
|
|
27318
27834
|
var CACHE_TTL_MS = 3e5;
|
|
27319
27835
|
var CACHE_MAX_ENTRIES = 200;
|
|
@@ -27321,7 +27837,7 @@ var cache = /* @__PURE__ */ new Map();
|
|
|
27321
27837
|
var searchTool = {
|
|
27322
27838
|
name: "search",
|
|
27323
27839
|
category: "Search",
|
|
27324
|
-
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.",
|
|
27840
|
+
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.",
|
|
27325
27841
|
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.",
|
|
27326
27842
|
permission: "auto",
|
|
27327
27843
|
mutating: false,
|
|
@@ -27362,7 +27878,7 @@ var searchTool = {
|
|
|
27362
27878
|
},
|
|
27363
27879
|
async *executeStream(input, _ctx, opts) {
|
|
27364
27880
|
if (!input?.query || input.query.trim() === "") {
|
|
27365
|
-
throw new
|
|
27881
|
+
throw new ToolValidationError10({
|
|
27366
27882
|
message: "search: query is required and must be a non-empty string",
|
|
27367
27883
|
field: "query"
|
|
27368
27884
|
});
|
|
@@ -27395,7 +27911,7 @@ var searchTool = {
|
|
|
27395
27911
|
query: input.query,
|
|
27396
27912
|
results: results.slice(0, num),
|
|
27397
27913
|
source: entry.source,
|
|
27398
|
-
truncated: results.length
|
|
27914
|
+
truncated: results.length > num,
|
|
27399
27915
|
cached: true
|
|
27400
27916
|
}
|
|
27401
27917
|
};
|
|
@@ -27407,41 +27923,45 @@ var searchTool = {
|
|
|
27407
27923
|
text: `Querying ${source} for "${input.query}"\u2026`,
|
|
27408
27924
|
data: { source, query: input.query, cached: false }
|
|
27409
27925
|
};
|
|
27410
|
-
let
|
|
27926
|
+
let engine;
|
|
27411
27927
|
let effectiveSource = source;
|
|
27412
27928
|
switch (source) {
|
|
27413
27929
|
case "duckduckgo":
|
|
27414
|
-
|
|
27930
|
+
engine = await duckduckgoSearch(input.query, opts.signal);
|
|
27415
27931
|
break;
|
|
27416
27932
|
case "google":
|
|
27417
|
-
|
|
27933
|
+
engine = await googleSearch(input.query, opts.signal);
|
|
27418
27934
|
break;
|
|
27419
27935
|
case "bing":
|
|
27420
|
-
|
|
27936
|
+
engine = await bingSearch(input.query, opts.signal);
|
|
27421
27937
|
break;
|
|
27422
27938
|
default:
|
|
27423
|
-
throw new
|
|
27939
|
+
throw new ToolValidationError10({
|
|
27424
27940
|
message: `search: unknown source "${source}"`,
|
|
27425
27941
|
field: "source"
|
|
27426
27942
|
});
|
|
27427
27943
|
}
|
|
27428
|
-
let ranked = rankSearchResults(
|
|
27944
|
+
let ranked = rankSearchResults(engine.results, input.query);
|
|
27945
|
+
let engineError = engine.error;
|
|
27429
27946
|
if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
|
|
27430
27947
|
yield {
|
|
27431
27948
|
type: "log",
|
|
27432
27949
|
text: `${source} returned no relevant static results; falling back to duckduckgo`,
|
|
27433
27950
|
data: { source, fallback: "duckduckgo", query: input.query }
|
|
27434
27951
|
};
|
|
27435
|
-
|
|
27436
|
-
ranked = rankSearchResults(
|
|
27952
|
+
const fallback = await duckduckgoSearch(input.query, opts.signal);
|
|
27953
|
+
ranked = rankSearchResults(fallback.results, input.query);
|
|
27954
|
+
engineError = fallback.error;
|
|
27437
27955
|
effectiveSource = "duckduckgo";
|
|
27438
27956
|
}
|
|
27439
27957
|
const finalResults = ranked.slice(0, num);
|
|
27440
|
-
|
|
27441
|
-
|
|
27958
|
+
if (!engineError) {
|
|
27959
|
+
cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
|
|
27960
|
+
pruneCacheEntries();
|
|
27961
|
+
}
|
|
27442
27962
|
yield {
|
|
27443
27963
|
type: "partial_output",
|
|
27444
|
-
text: `${finalResults.length} results from ${effectiveSource}`,
|
|
27964
|
+
text: engineError ? `search failed: ${engineError}` : `${finalResults.length} results from ${effectiveSource}`,
|
|
27445
27965
|
data: { count: finalResults.length, cached: false, source: effectiveSource }
|
|
27446
27966
|
};
|
|
27447
27967
|
yield {
|
|
@@ -27454,8 +27974,9 @@ var searchTool = {
|
|
|
27454
27974
|
snippet: r.snippet
|
|
27455
27975
|
})),
|
|
27456
27976
|
source: effectiveSource,
|
|
27457
|
-
truncated:
|
|
27458
|
-
cached: false
|
|
27977
|
+
truncated: ranked.length > num,
|
|
27978
|
+
cached: false,
|
|
27979
|
+
...engineError ? { error: engineError } : {}
|
|
27459
27980
|
}
|
|
27460
27981
|
};
|
|
27461
27982
|
}
|
|
@@ -27506,18 +28027,18 @@ function shouldFallbackToDuckDuckGo(results, query) {
|
|
|
27506
28027
|
return terms.some((term) => haystack.includes(term));
|
|
27507
28028
|
});
|
|
27508
28029
|
}
|
|
27509
|
-
async function duckduckgoSearch(query,
|
|
28030
|
+
async function duckduckgoSearch(query, signal) {
|
|
27510
28031
|
const encoded = encodeURIComponent(query);
|
|
27511
28032
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
27512
28033
|
try {
|
|
27513
28034
|
const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
|
|
27514
28035
|
const html = await response.text();
|
|
27515
|
-
return parseDuckDuckGo(html,
|
|
28036
|
+
return { results: parseDuckDuckGo(html, MAX_RESULTS) };
|
|
27516
28037
|
} catch (err) {
|
|
27517
28038
|
console.log(
|
|
27518
|
-
JSON.stringify({ level: "debug", event: "search_failed", query, error:
|
|
28039
|
+
JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage9(err) })
|
|
27519
28040
|
);
|
|
27520
|
-
return
|
|
28041
|
+
return { results: [], error: `duckduckgo unreachable: ${toErrorMessage9(err)}` };
|
|
27521
28042
|
}
|
|
27522
28043
|
}
|
|
27523
28044
|
function takeFrom(iter, max) {
|
|
@@ -27551,7 +28072,7 @@ function parseDuckDuckGo(html, num) {
|
|
|
27551
28072
|
results.push({
|
|
27552
28073
|
title: entry.title ?? "",
|
|
27553
28074
|
url: entry.url ?? "",
|
|
27554
|
-
snippet: snippetMatches[i] ?? "",
|
|
28075
|
+
snippet: capSnippet(snippetMatches[i] ?? ""),
|
|
27555
28076
|
score: 1
|
|
27556
28077
|
});
|
|
27557
28078
|
}
|
|
@@ -27576,11 +28097,15 @@ function normalizeDuckDuckGoUrl(raw) {
|
|
|
27576
28097
|
return raw;
|
|
27577
28098
|
}
|
|
27578
28099
|
}
|
|
27579
|
-
async function googleSearch(query,
|
|
28100
|
+
async function googleSearch(query, signal) {
|
|
27580
28101
|
const encoded = encodeURIComponent(query);
|
|
27581
28102
|
const url = `https://www.google.com/search?q=${encoded}&hl=en`;
|
|
27582
|
-
|
|
27583
|
-
|
|
28103
|
+
try {
|
|
28104
|
+
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
|
|
28105
|
+
return { results: parseGoogleResults(html, MAX_RESULTS) };
|
|
28106
|
+
} catch (err) {
|
|
28107
|
+
return { results: [], error: `google unreachable: ${toErrorMessage9(err)}` };
|
|
28108
|
+
}
|
|
27584
28109
|
}
|
|
27585
28110
|
function parseGoogleResults(html, num) {
|
|
27586
28111
|
const results = [];
|
|
@@ -27603,17 +28128,21 @@ function parseGoogleResults(html, num) {
|
|
|
27603
28128
|
results.push({
|
|
27604
28129
|
title: titles[i] ?? "",
|
|
27605
28130
|
url: urls[i] ?? "",
|
|
27606
|
-
snippet: snippets[i] ?? "",
|
|
28131
|
+
snippet: capSnippet(snippets[i] ?? ""),
|
|
27607
28132
|
score: 1
|
|
27608
28133
|
});
|
|
27609
28134
|
}
|
|
27610
28135
|
return results;
|
|
27611
28136
|
}
|
|
27612
|
-
async function bingSearch(query,
|
|
28137
|
+
async function bingSearch(query, signal) {
|
|
27613
28138
|
const encoded = encodeURIComponent(query);
|
|
27614
28139
|
const url = `https://www.bing.com/search?q=${encoded}`;
|
|
27615
|
-
|
|
27616
|
-
|
|
28140
|
+
try {
|
|
28141
|
+
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
|
|
28142
|
+
return { results: parseBingResults(html, MAX_RESULTS) };
|
|
28143
|
+
} catch (err) {
|
|
28144
|
+
return { results: [], error: `bing unreachable: ${toErrorMessage9(err)}` };
|
|
28145
|
+
}
|
|
27617
28146
|
}
|
|
27618
28147
|
function parseBingResults(html, num) {
|
|
27619
28148
|
const results = [];
|
|
@@ -27626,7 +28155,7 @@ function parseBingResults(html, num) {
|
|
|
27626
28155
|
const title = stripTags(expectDefined9(titleMatch[2]));
|
|
27627
28156
|
if (!href || !title) return [];
|
|
27628
28157
|
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);
|
|
27629
|
-
const snippet = snippetMatch ? stripTags(expectDefined9(snippetMatch.at(-1))) : "";
|
|
28158
|
+
const snippet = snippetMatch ? capSnippet(stripTags(expectDefined9(snippetMatch.at(-1)))) : "";
|
|
27630
28159
|
return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
|
|
27631
28160
|
}), num);
|
|
27632
28161
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -27693,17 +28222,20 @@ function anySignal(...signals) {
|
|
|
27693
28222
|
function stripTags(html) {
|
|
27694
28223
|
return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
|
|
27695
28224
|
}
|
|
28225
|
+
function capSnippet(snippet) {
|
|
28226
|
+
return snippet.length > MAX_SNIPPET_CHARS ? `${snippet.slice(0, MAX_SNIPPET_CHARS - 1)}\u2026` : snippet;
|
|
28227
|
+
}
|
|
27696
28228
|
function decodeHtmlEntities(text) {
|
|
27697
28229
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
27698
28230
|
}
|
|
27699
28231
|
|
|
27700
28232
|
// src/set-working-dir.ts
|
|
27701
28233
|
import * as fs32 from "node:fs/promises";
|
|
27702
|
-
import { toErrorMessage as
|
|
28234
|
+
import { toErrorMessage as toErrorMessage10 } from "@wrongstack/core/utils";
|
|
27703
28235
|
var setWorkingDirTool = {
|
|
27704
28236
|
name: "set_working_dir",
|
|
27705
28237
|
category: "Context",
|
|
27706
|
-
description: "Change the current working directory for
|
|
28238
|
+
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.",
|
|
27707
28239
|
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.",
|
|
27708
28240
|
permission: "confirm",
|
|
27709
28241
|
mutating: true,
|
|
@@ -27733,19 +28265,23 @@ var setWorkingDirTool = {
|
|
|
27733
28265
|
} catch (err) {
|
|
27734
28266
|
return {
|
|
27735
28267
|
current: ctx.workingDir,
|
|
27736
|
-
error:
|
|
28268
|
+
error: toErrorMessage10(err)
|
|
27737
28269
|
};
|
|
27738
28270
|
}
|
|
28271
|
+
let isDirectory = false;
|
|
27739
28272
|
try {
|
|
27740
|
-
await fs32.
|
|
28273
|
+
isDirectory = (await fs32.stat(resolved)).isDirectory();
|
|
27741
28274
|
} catch {
|
|
28275
|
+
isDirectory = false;
|
|
28276
|
+
}
|
|
28277
|
+
if (!isDirectory) {
|
|
27742
28278
|
try {
|
|
27743
28279
|
ctx.setWorkingDir(previous);
|
|
27744
28280
|
} catch {
|
|
27745
28281
|
}
|
|
27746
28282
|
return {
|
|
27747
28283
|
current: ctx.workingDir,
|
|
27748
|
-
error: `Directory does not exist: ${resolved}`
|
|
28284
|
+
error: `Directory does not exist (or is not a directory): ${resolved}`
|
|
27749
28285
|
};
|
|
27750
28286
|
}
|
|
27751
28287
|
return {
|
|
@@ -28200,6 +28736,7 @@ var taskTool = {
|
|
|
28200
28736
|
inProgress: 0
|
|
28201
28737
|
};
|
|
28202
28738
|
}
|
|
28739
|
+
ctx.meta["task.path.resolved"] = taskPath;
|
|
28203
28740
|
if (todosToReplace) {
|
|
28204
28741
|
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
28205
28742
|
signal: AbortSignal.timeout(3e4)
|
|
@@ -28224,6 +28761,7 @@ var taskTool = {
|
|
|
28224
28761
|
formatted = formatPlan2(updated);
|
|
28225
28762
|
return updated;
|
|
28226
28763
|
});
|
|
28764
|
+
ctx.meta["plan.path.resolved"] = planPath;
|
|
28227
28765
|
} catch (err) {
|
|
28228
28766
|
return {
|
|
28229
28767
|
ok: false,
|
|
@@ -28267,7 +28805,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
|
|
|
28267
28805
|
init_spawn_stream();
|
|
28268
28806
|
init_util();
|
|
28269
28807
|
init_legacy_bridge();
|
|
28270
|
-
import * as
|
|
28808
|
+
import * as path37 from "node:path";
|
|
28271
28809
|
var testTool = {
|
|
28272
28810
|
name: "test",
|
|
28273
28811
|
category: "Code Quality",
|
|
@@ -28370,11 +28908,11 @@ var testTool = {
|
|
|
28370
28908
|
}
|
|
28371
28909
|
};
|
|
28372
28910
|
async function detectRunner(cwd) {
|
|
28373
|
-
const { stat:
|
|
28911
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
28374
28912
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
28375
28913
|
for (const f of candidates) {
|
|
28376
28914
|
try {
|
|
28377
|
-
await
|
|
28915
|
+
await stat19(path37.join(cwd, f));
|
|
28378
28916
|
if (f.includes("vitest")) return "vitest";
|
|
28379
28917
|
if (f.includes("jest")) return "jest";
|
|
28380
28918
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -28752,7 +29290,7 @@ var toolUseTool = {
|
|
|
28752
29290
|
// src/tree.ts
|
|
28753
29291
|
init_util();
|
|
28754
29292
|
import * as fs33 from "node:fs/promises";
|
|
28755
|
-
import * as
|
|
29293
|
+
import * as path38 from "node:path";
|
|
28756
29294
|
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
28757
29295
|
var DEFAULT_IGNORE5 = /* @__PURE__ */ new Set([
|
|
28758
29296
|
...DEFAULT_WALK_IGNORE_DIRS4,
|
|
@@ -28772,6 +29310,7 @@ var treeTool = {
|
|
|
28772
29310
|
mutating: false,
|
|
28773
29311
|
capabilities: ["fs.read"],
|
|
28774
29312
|
icon: "tree",
|
|
29313
|
+
maxOutputBytes: 262144,
|
|
28775
29314
|
timeoutMs: 15e3,
|
|
28776
29315
|
inputSchema: {
|
|
28777
29316
|
type: "object",
|
|
@@ -28920,17 +29459,15 @@ async function walkDir(dir, depth, opts) {
|
|
|
28920
29459
|
if (opts.exclude.has(e.name)) return false;
|
|
28921
29460
|
return true;
|
|
28922
29461
|
});
|
|
28923
|
-
|
|
28924
|
-
|
|
28925
|
-
|
|
28926
|
-
|
|
28927
|
-
|
|
28928
|
-
|
|
28929
|
-
|
|
28930
|
-
|
|
28931
|
-
|
|
28932
|
-
opts.onProgress?.();
|
|
28933
|
-
}
|
|
29462
|
+
let dirCount = 0;
|
|
29463
|
+
let fileCount = 0;
|
|
29464
|
+
for (const e of filtered) {
|
|
29465
|
+
if (e.isDirectory()) dirCount++;
|
|
29466
|
+
else if (e.isFile()) fileCount++;
|
|
29467
|
+
}
|
|
29468
|
+
opts.totalDirs.value += dirCount;
|
|
29469
|
+
opts.totalFiles.value += fileCount;
|
|
29470
|
+
opts.onProgress?.();
|
|
28934
29471
|
const items = filtered.sort((a, b) => {
|
|
28935
29472
|
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
28936
29473
|
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
@@ -28958,7 +29495,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
28958
29495
|
opts.retention.outputBytes += lineBytes;
|
|
28959
29496
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
28960
29497
|
const childPrefix = opts.prefix + connector;
|
|
28961
|
-
await walkDir(
|
|
29498
|
+
await walkDir(path38.join(dir, entry.name), depth + 1, {
|
|
28962
29499
|
...opts,
|
|
28963
29500
|
prefix: childPrefix,
|
|
28964
29501
|
isLast
|
|
@@ -28971,7 +29508,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
28971
29508
|
init_spawn_stream();
|
|
28972
29509
|
init_util();
|
|
28973
29510
|
init_legacy_bridge();
|
|
28974
|
-
import * as
|
|
29511
|
+
import * as path39 from "node:path";
|
|
28975
29512
|
var typecheckTool = {
|
|
28976
29513
|
name: "typecheck",
|
|
28977
29514
|
category: "Code Quality",
|
|
@@ -28993,11 +29530,7 @@ var typecheckTool = {
|
|
|
28993
29530
|
},
|
|
28994
29531
|
all: {
|
|
28995
29532
|
type: "boolean",
|
|
28996
|
-
description: "Type-check all
|
|
28997
|
-
},
|
|
28998
|
-
json: {
|
|
28999
|
-
type: "boolean",
|
|
29000
|
-
description: "Emit JSON output from tsc (default: false)"
|
|
29533
|
+
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)"
|
|
29001
29534
|
}
|
|
29002
29535
|
}
|
|
29003
29536
|
},
|
|
@@ -29033,29 +29566,42 @@ var typecheckTool = {
|
|
|
29033
29566
|
};
|
|
29034
29567
|
return;
|
|
29035
29568
|
}
|
|
29036
|
-
let
|
|
29569
|
+
let cmd;
|
|
29570
|
+
let cmdArgs;
|
|
29037
29571
|
let project;
|
|
29038
29572
|
if (input.all) {
|
|
29039
|
-
args = ["--noEmit"];
|
|
29040
29573
|
project = "workspace";
|
|
29574
|
+
const tscArgs = ["--noEmit"];
|
|
29575
|
+
if (input.strict) tscArgs.push("--strict");
|
|
29576
|
+
const manager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
29577
|
+
if (manager === "pnpm") {
|
|
29578
|
+
cmd = "pnpm";
|
|
29579
|
+
cmdArgs = ["-r", "--no-bail", "exec", "tsc", ...tscArgs];
|
|
29580
|
+
} else {
|
|
29581
|
+
cmd = "npx";
|
|
29582
|
+
cmdArgs = ["tsc", ...tscArgs];
|
|
29583
|
+
}
|
|
29041
29584
|
} else {
|
|
29042
29585
|
const tsconfig = input.project ? safeResolve(input.project, ctx) : await findTsConfig(cwd);
|
|
29043
|
-
|
|
29044
|
-
if (input.strict)
|
|
29045
|
-
if (tsconfig)
|
|
29586
|
+
const tscArgs = ["--noEmit"];
|
|
29587
|
+
if (input.strict) tscArgs.push("--strict");
|
|
29588
|
+
if (tsconfig) tscArgs.push("--project", tsconfig);
|
|
29046
29589
|
project = tsconfig ?? "default";
|
|
29590
|
+
cmd = "npx";
|
|
29591
|
+
cmdArgs = ["tsc", ...tscArgs];
|
|
29047
29592
|
}
|
|
29048
|
-
|
|
29049
|
-
yield { type: "log", text: `tsc ${args.join(" ")}`, data: { project } };
|
|
29593
|
+
yield { type: "log", text: `${cmd} ${cmdArgs.join(" ")}`, data: { project } };
|
|
29050
29594
|
const result = yield* spawnStream({
|
|
29051
|
-
cmd
|
|
29052
|
-
args:
|
|
29595
|
+
cmd,
|
|
29596
|
+
args: cmdArgs,
|
|
29053
29597
|
cwd,
|
|
29054
29598
|
signal: opts.signal,
|
|
29055
29599
|
maxBytes: 2e5
|
|
29056
29600
|
});
|
|
29057
|
-
const
|
|
29058
|
-
|
|
29601
|
+
const combined = `${result.stdout}
|
|
29602
|
+
${result.stderr}`;
|
|
29603
|
+
const errors = [...combined.matchAll(/^.*\berror TS\d+:/gm)].length;
|
|
29604
|
+
const warnings = [...combined.matchAll(/^.*\bwarning TS\d+:/gm)].length;
|
|
29059
29605
|
yield {
|
|
29060
29606
|
type: "final",
|
|
29061
29607
|
output: {
|
|
@@ -29070,12 +29616,12 @@ var typecheckTool = {
|
|
|
29070
29616
|
}
|
|
29071
29617
|
};
|
|
29072
29618
|
async function findTsConfig(cwd) {
|
|
29073
|
-
const { stat:
|
|
29619
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
29074
29620
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
29075
29621
|
for (const f of candidates) {
|
|
29076
29622
|
try {
|
|
29077
|
-
const s = await
|
|
29078
|
-
if (s.isFile()) return
|
|
29623
|
+
const s = await stat19(path39.join(cwd, f));
|
|
29624
|
+
if (s.isFile()) return path39.join(cwd, f);
|
|
29079
29625
|
} catch {
|
|
29080
29626
|
}
|
|
29081
29627
|
}
|
|
@@ -29084,21 +29630,32 @@ async function findTsConfig(cwd) {
|
|
|
29084
29630
|
|
|
29085
29631
|
// src/write.ts
|
|
29086
29632
|
import * as fs34 from "node:fs/promises";
|
|
29087
|
-
import { ToolValidationError as
|
|
29088
|
-
import {
|
|
29633
|
+
import { ToolValidationError as ToolValidationError11 } from "@wrongstack/core/types";
|
|
29634
|
+
import {
|
|
29635
|
+
atomicWrite as atomicWrite5,
|
|
29636
|
+
detectNewlineStyle as detectNewlineStyle3,
|
|
29637
|
+
normalizeToLf as normalizeToLf3,
|
|
29638
|
+
toStyle as toStyle3,
|
|
29639
|
+
unifiedDiff as unifiedDiff3
|
|
29640
|
+
} from "@wrongstack/core/utils";
|
|
29089
29641
|
init_util();
|
|
29642
|
+
var MAX_DIFF_BYTES3 = 262144;
|
|
29090
29643
|
var writeTool = {
|
|
29091
29644
|
name: "write",
|
|
29092
29645
|
category: "Filesystem",
|
|
29093
29646
|
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.",
|
|
29094
|
-
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-
|
|
29647
|
+
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.",
|
|
29095
29648
|
selection: {
|
|
29096
29649
|
doNotUseWhen: "making a precise change to part of an existing file.",
|
|
29097
29650
|
useInstead: ["edit"]
|
|
29098
29651
|
},
|
|
29099
29652
|
permission: "confirm",
|
|
29653
|
+
// WS-046: gives permission decisions something to key on — the file being
|
|
29654
|
+
// written, so trust rules can scope by path.
|
|
29655
|
+
subjectKey: "path",
|
|
29100
29656
|
mutating: true,
|
|
29101
29657
|
timeoutMs: 5e3,
|
|
29658
|
+
maxOutputBytes: 262144,
|
|
29102
29659
|
capabilities: ["fs.write"],
|
|
29103
29660
|
icon: "file",
|
|
29104
29661
|
inputSchema: {
|
|
@@ -29134,13 +29691,13 @@ async function writeFile6(input, ctx, signal) {
|
|
|
29134
29691
|
}
|
|
29135
29692
|
async function prepareWrite(input, ctx) {
|
|
29136
29693
|
if (!input?.path) {
|
|
29137
|
-
throw new
|
|
29694
|
+
throw new ToolValidationError11({
|
|
29138
29695
|
message: "write: path is required",
|
|
29139
29696
|
field: "path"
|
|
29140
29697
|
});
|
|
29141
29698
|
}
|
|
29142
29699
|
if (input.content === void 0) {
|
|
29143
|
-
throw new
|
|
29700
|
+
throw new ToolValidationError11({
|
|
29144
29701
|
message: "write: content is required",
|
|
29145
29702
|
field: "content"
|
|
29146
29703
|
});
|
|
@@ -29149,12 +29706,12 @@ async function prepareWrite(input, ctx) {
|
|
|
29149
29706
|
let existed = false;
|
|
29150
29707
|
let prev = "";
|
|
29151
29708
|
try {
|
|
29152
|
-
const
|
|
29153
|
-
existed =
|
|
29709
|
+
const stat19 = await fs34.stat(absPath);
|
|
29710
|
+
existed = stat19.isFile();
|
|
29154
29711
|
if (existed) {
|
|
29155
29712
|
if (!ctx.hasRead(absPath)) {
|
|
29156
29713
|
prev = await fs34.readFile(absPath, "utf8");
|
|
29157
|
-
ctx.recordRead(absPath,
|
|
29714
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "write", sha256hex(prev));
|
|
29158
29715
|
} else {
|
|
29159
29716
|
prev = await fs34.readFile(absPath, "utf8");
|
|
29160
29717
|
}
|
|
@@ -29167,31 +29724,42 @@ async function prepareWrite(input, ctx) {
|
|
|
29167
29724
|
return { absPath, existed, prev };
|
|
29168
29725
|
}
|
|
29169
29726
|
async function finishWrite(input, ctx, prepared, signal) {
|
|
29727
|
+
const content = prepared.existed ? toStyle3(normalizeToLf3(input.content), detectNewlineStyle3(prepared.prev)) : input.content;
|
|
29170
29728
|
signal?.throwIfAborted();
|
|
29171
|
-
await atomicWrite5(prepared.absPath,
|
|
29172
|
-
const
|
|
29173
|
-
+ (new file, ${
|
|
29174
|
-
const
|
|
29175
|
-
|
|
29729
|
+
await atomicWrite5(prepared.absPath, content);
|
|
29730
|
+
const rawDiff = prepared.existed ? unifiedDiff3(prepared.prev, content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
29731
|
+
+ (new file, ${content.split("\n").length} lines)`;
|
|
29732
|
+
const { text: diff, truncated: diffTruncated } = truncateDiffPayload(rawDiff, MAX_DIFF_BYTES3);
|
|
29733
|
+
const stat19 = await fs34.stat(prepared.absPath);
|
|
29734
|
+
ctx.recordRead(prepared.absPath, stat19.mtimeMs, "write", sha256hex(content));
|
|
29176
29735
|
ctx.session.recordFileChange({
|
|
29177
29736
|
path: prepared.absPath,
|
|
29178
29737
|
action: prepared.existed ? "modified" : "created",
|
|
29179
29738
|
before: prepared.existed ? prepared.prev : null,
|
|
29180
|
-
after:
|
|
29739
|
+
after: content
|
|
29181
29740
|
});
|
|
29182
29741
|
const syntax = await checkSyntax(
|
|
29183
29742
|
prepared.absPath,
|
|
29184
|
-
|
|
29743
|
+
content,
|
|
29185
29744
|
prepared.existed ? prepared.prev : void 0
|
|
29186
29745
|
).catch(() => void 0);
|
|
29187
29746
|
const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
|
|
29747
|
+
const notes = [];
|
|
29748
|
+
if (diffTruncated) {
|
|
29749
|
+
notes.push("Diff truncated to the 256 KiB output budget \u2014 the full write is on disk.");
|
|
29750
|
+
}
|
|
29751
|
+
if (hasSyntaxErrors) {
|
|
29752
|
+
notes.push(
|
|
29753
|
+
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.`
|
|
29754
|
+
);
|
|
29755
|
+
}
|
|
29188
29756
|
return {
|
|
29189
29757
|
path: prepared.absPath,
|
|
29190
|
-
bytes_written: Buffer.byteLength(
|
|
29758
|
+
bytes_written: Buffer.byteLength(content, "utf8"),
|
|
29191
29759
|
created: !prepared.existed,
|
|
29192
29760
|
diff,
|
|
29193
29761
|
syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
|
|
29194
|
-
note:
|
|
29762
|
+
note: notes.length > 0 ? notes.join("\n") : void 0
|
|
29195
29763
|
};
|
|
29196
29764
|
}
|
|
29197
29765
|
|