@wrongstack/tools 0.305.1 → 0.306.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_shell-pick.d.ts +4 -5
- package/dist/_util.d.ts +22 -5
- package/dist/audit.d.ts +0 -1
- package/dist/audit.js +135 -46
- package/dist/bash.js +61 -37
- package/dist/browser/index.js +29 -9
- package/dist/browser/types.d.ts +7 -1
- package/dist/builtin.js +1279 -726
- package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
- package/dist/codebase-index/index.js +223 -152
- 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 +1342 -761
- 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 +1279 -726
- package/dist/plan.js +76 -3
- package/dist/process-registry.d.ts +8 -2
- package/dist/process-registry.js +28 -13
- package/dist/ps-slash.js +22 -12
- package/dist/read.js +10 -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/skill.d.ts +6 -0
- package/dist/skill.js +9 -10
- package/dist/task.js +66 -2
- package/dist/test.js +28 -13
- package/dist/todo.js +64 -2
- package/dist/tool-icons.js +4 -2
- package/dist/tool-summary.d.ts +1 -1
- package/dist/tool-summary.js +76 -1
- package/dist/tool-tier.js +1279 -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/index.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
|
});
|
|
@@ -521,11 +525,15 @@ var init_process_registry = __esm({
|
|
|
521
525
|
return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
|
|
522
526
|
}
|
|
523
527
|
_canSignalProcessGroup(p) {
|
|
524
|
-
return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
|
|
528
|
+
return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
|
|
525
529
|
}
|
|
526
530
|
_killChildDirect(p, signal) {
|
|
527
531
|
try {
|
|
528
|
-
p.child
|
|
532
|
+
if (p.child) {
|
|
533
|
+
p.child.kill(signal);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
|
|
529
537
|
} catch {
|
|
530
538
|
}
|
|
531
539
|
}
|
|
@@ -723,15 +731,15 @@ var init_process_registry = __esm({
|
|
|
723
731
|
this._pruneStale(pid);
|
|
724
732
|
const p = this.processes.get(pid);
|
|
725
733
|
if (!p) return false;
|
|
726
|
-
if (p.killed) return true;
|
|
734
|
+
if (p.killed && opts.force !== true) return true;
|
|
727
735
|
if (p.protected && opts.includeProtected !== true) return false;
|
|
728
736
|
if (opts.preserveBackground && p.background) return false;
|
|
729
737
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
730
738
|
const isWin5 = os.platform() === "win32";
|
|
731
739
|
if (isWin5) {
|
|
732
|
-
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
740
|
+
const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
|
|
733
741
|
const directFallback = () => {
|
|
734
|
-
if (p.child.exitCode === null) {
|
|
742
|
+
if (p.child && p.child.exitCode === null) {
|
|
735
743
|
try {
|
|
736
744
|
p.child.kill("SIGKILL");
|
|
737
745
|
} catch {
|
|
@@ -743,10 +751,7 @@ var init_process_registry = __esm({
|
|
|
743
751
|
onSettled: directFallback
|
|
744
752
|
})) {
|
|
745
753
|
} else {
|
|
746
|
-
|
|
747
|
-
p.child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
748
|
-
} catch {
|
|
749
|
-
}
|
|
754
|
+
this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
|
|
750
755
|
}
|
|
751
756
|
p.killed = true;
|
|
752
757
|
return true;
|
|
@@ -757,7 +762,7 @@ var init_process_registry = __esm({
|
|
|
757
762
|
} else {
|
|
758
763
|
this._killPosix(p, "SIGTERM");
|
|
759
764
|
const timer = setTimeout(() => {
|
|
760
|
-
if (this.processes.has(pid) && !p.child
|
|
765
|
+
if (this.processes.has(pid) && !p.child?.killed) {
|
|
761
766
|
this._killPosix(p, "SIGKILL");
|
|
762
767
|
}
|
|
763
768
|
}, graceMs);
|
|
@@ -812,6 +817,16 @@ var init_process_registry = __esm({
|
|
|
812
817
|
* before reusing a PID, but we want to clean up before that becomes a risk.
|
|
813
818
|
*/
|
|
814
819
|
_isStaleEntry(entry) {
|
|
820
|
+
if (entry.child === null) {
|
|
821
|
+
if (Date.now() - entry.startedAt <= 6e4) return false;
|
|
822
|
+
if (os.platform() === "win32") return false;
|
|
823
|
+
try {
|
|
824
|
+
process.kill(entry.pid, 0);
|
|
825
|
+
return false;
|
|
826
|
+
} catch (err) {
|
|
827
|
+
return err.code !== "EPERM";
|
|
828
|
+
}
|
|
829
|
+
}
|
|
815
830
|
return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
|
|
816
831
|
}
|
|
817
832
|
/**
|
|
@@ -1013,8 +1028,8 @@ async function* spawnStream(opts) {
|
|
|
1013
1028
|
try {
|
|
1014
1029
|
for (; ; ) {
|
|
1015
1030
|
while (queue.length === 0) {
|
|
1016
|
-
await new Promise((
|
|
1017
|
-
waiter =
|
|
1031
|
+
await new Promise((resolve18) => {
|
|
1032
|
+
waiter = resolve18;
|
|
1018
1033
|
});
|
|
1019
1034
|
}
|
|
1020
1035
|
const chunk = queue.shift();
|
|
@@ -1102,19 +1117,48 @@ import * as Core from "@wrongstack/core/utils";
|
|
|
1102
1117
|
function sha256hex(content) {
|
|
1103
1118
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1104
1119
|
}
|
|
1105
|
-
async function detectPackageManager(cwd) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1120
|
+
async function detectPackageManager(cwd, stopAt) {
|
|
1121
|
+
let dir = path3.resolve(cwd);
|
|
1122
|
+
const stop = stopAt ? path3.resolve(stopAt) : dir;
|
|
1123
|
+
for (; ; ) {
|
|
1124
|
+
const found = await detectPackageManagerInDir(dir);
|
|
1125
|
+
if (found) return found;
|
|
1126
|
+
if (dir === stop) break;
|
|
1127
|
+
const parent = path3.dirname(dir);
|
|
1128
|
+
const relParent = path3.relative(stop, parent);
|
|
1129
|
+
if (parent === dir || relParent.startsWith("..") || path3.isAbsolute(relParent)) break;
|
|
1130
|
+
dir = parent;
|
|
1111
1131
|
}
|
|
1132
|
+
return "npm";
|
|
1133
|
+
}
|
|
1134
|
+
async function detectPackageManagerInDir(dir) {
|
|
1135
|
+
const fs36 = await import("node:fs/promises");
|
|
1112
1136
|
try {
|
|
1113
|
-
await
|
|
1114
|
-
|
|
1137
|
+
const raw = await fs36.readFile(path3.join(dir, "package.json"), "utf8");
|
|
1138
|
+
const declared = JSON.parse(raw).packageManager;
|
|
1139
|
+
if (typeof declared === "string") {
|
|
1140
|
+
const name = declared.split("@")[0] ?? "";
|
|
1141
|
+
if (name === "pnpm" || name === "yarn") return name;
|
|
1142
|
+
if (name === "npm" || name === "bun") return "npm";
|
|
1143
|
+
}
|
|
1115
1144
|
} catch {
|
|
1116
1145
|
}
|
|
1117
|
-
|
|
1146
|
+
const lockfiles = [
|
|
1147
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
1148
|
+
["yarn.lock", "yarn"],
|
|
1149
|
+
["bun.lockb", "npm"],
|
|
1150
|
+
["bun.lock", "npm"],
|
|
1151
|
+
["package-lock.json", "npm"],
|
|
1152
|
+
["npm-shrinkwrap.json", "npm"]
|
|
1153
|
+
];
|
|
1154
|
+
for (const [file, manager] of lockfiles) {
|
|
1155
|
+
try {
|
|
1156
|
+
await fs36.stat(`${dir}/${file}`);
|
|
1157
|
+
return manager;
|
|
1158
|
+
} catch {
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return null;
|
|
1118
1162
|
}
|
|
1119
1163
|
function resolvePath(input, ctx) {
|
|
1120
1164
|
return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
@@ -1173,6 +1217,20 @@ async function safeResolveReal(input, ctx) {
|
|
|
1173
1217
|
const abs = safeResolve(input, ctx);
|
|
1174
1218
|
return await resolveRealInsideRoot(abs, ctx);
|
|
1175
1219
|
}
|
|
1220
|
+
function truncateDiffPayload(diff, maxBytes) {
|
|
1221
|
+
const total = Buffer.byteLength(diff, "utf8");
|
|
1222
|
+
if (total <= maxBytes) return { text: diff, truncated: false };
|
|
1223
|
+
const MARKER_RESERVE = 96;
|
|
1224
|
+
let head = takeHeadBytes(diff, Math.max(0, maxBytes - MARKER_RESERVE));
|
|
1225
|
+
const nl = head.lastIndexOf("\n");
|
|
1226
|
+
if (nl > 0) head = head.slice(0, nl);
|
|
1227
|
+
const kept = Buffer.byteLength(head, "utf8");
|
|
1228
|
+
return {
|
|
1229
|
+
text: `${head}
|
|
1230
|
+
\u2026[diff truncated: ${total - kept} of ${total} bytes omitted]`,
|
|
1231
|
+
truncated: true
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1176
1234
|
function truncateMiddle(s, max) {
|
|
1177
1235
|
if (Buffer.byteLength(s, "utf8") <= max) return s;
|
|
1178
1236
|
const half = Math.floor(max / 2);
|
|
@@ -2735,8 +2793,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
|
|
|
2735
2793
|
collectFileEvidence(directory, fullPath, entry.name, profiles, state);
|
|
2736
2794
|
}
|
|
2737
2795
|
}
|
|
2738
|
-
function collectFileEvidence(directory, fullPath,
|
|
2739
|
-
const lower =
|
|
2796
|
+
function collectFileEvidence(directory, fullPath, basename15, profiles, state) {
|
|
2797
|
+
const lower = basename15.toLowerCase();
|
|
2740
2798
|
const extension = path4.extname(lower);
|
|
2741
2799
|
for (const profile of profiles) {
|
|
2742
2800
|
const detector = profile.detectors.find(
|
|
@@ -2747,7 +2805,7 @@ function collectFileEvidence(directory, fullPath, basename14, profiles, state) {
|
|
|
2747
2805
|
candidate.evidence.push({
|
|
2748
2806
|
kind: detector.kind,
|
|
2749
2807
|
path: fullPath,
|
|
2750
|
-
value:
|
|
2808
|
+
value: basename15,
|
|
2751
2809
|
weight: detector.weight
|
|
2752
2810
|
});
|
|
2753
2811
|
if (detector.kind === "manifest" || detector.kind === "config") {
|
|
@@ -2875,8 +2933,8 @@ function normalizeLimits(input) {
|
|
|
2875
2933
|
async function canonicalDirectory(input) {
|
|
2876
2934
|
const resolved = path4.resolve(input);
|
|
2877
2935
|
const real = await fs2.realpath(resolved);
|
|
2878
|
-
const
|
|
2879
|
-
if (!
|
|
2936
|
+
const stat20 = await fs2.stat(real);
|
|
2937
|
+
if (!stat20.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
|
|
2880
2938
|
return real;
|
|
2881
2939
|
}
|
|
2882
2940
|
async function canonicalInside(input, root, label) {
|
|
@@ -3775,8 +3833,8 @@ async function executeInternal(options, startedAt) {
|
|
|
3775
3833
|
const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
|
|
3776
3834
|
if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
|
|
3777
3835
|
const safeTarget = await assertContainedFile(target, options.projectRoot);
|
|
3778
|
-
const
|
|
3779
|
-
if (
|
|
3836
|
+
const stat20 = await fs4.stat(safeTarget);
|
|
3837
|
+
if (stat20.size > MAX_INTERNAL_SOURCE_BYTES) {
|
|
3780
3838
|
return unavailableResult(
|
|
3781
3839
|
options,
|
|
3782
3840
|
`Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
|
|
@@ -4063,8 +4121,8 @@ async function snapshotPaths(paths) {
|
|
|
4063
4121
|
const existing = [];
|
|
4064
4122
|
for (const candidate of paths) {
|
|
4065
4123
|
try {
|
|
4066
|
-
const
|
|
4067
|
-
if (!
|
|
4124
|
+
const stat20 = await fs4.stat(candidate);
|
|
4125
|
+
if (!stat20.isFile()) continue;
|
|
4068
4126
|
existing.push(candidate);
|
|
4069
4127
|
} catch {
|
|
4070
4128
|
}
|
|
@@ -4075,15 +4133,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
|
|
|
4075
4133
|
const beforeSet = new Set(before);
|
|
4076
4134
|
const afterSet = new Set(after);
|
|
4077
4135
|
const changed = /* @__PURE__ */ new Set();
|
|
4078
|
-
for (const
|
|
4079
|
-
if (!beforeSet.has(
|
|
4136
|
+
for (const path41 of after) {
|
|
4137
|
+
if (!beforeSet.has(path41)) changed.add(path41);
|
|
4080
4138
|
}
|
|
4081
|
-
for (const
|
|
4082
|
-
if (!afterSet.has(
|
|
4139
|
+
for (const path41 of before) {
|
|
4140
|
+
if (!afterSet.has(path41)) changed.add(path41);
|
|
4083
4141
|
}
|
|
4084
4142
|
if (beforeSizes && afterSizes) {
|
|
4085
|
-
for (const
|
|
4086
|
-
if (beforeSizes.get(
|
|
4143
|
+
for (const path41 of after) {
|
|
4144
|
+
if (beforeSizes.get(path41) !== afterSizes.get(path41)) changed.add(path41);
|
|
4087
4145
|
}
|
|
4088
4146
|
}
|
|
4089
4147
|
return [...changed].sort();
|
|
@@ -4092,8 +4150,8 @@ async function snapshotSizes(paths) {
|
|
|
4092
4150
|
const sizes = /* @__PURE__ */ new Map();
|
|
4093
4151
|
for (const candidate of paths) {
|
|
4094
4152
|
try {
|
|
4095
|
-
const
|
|
4096
|
-
if (
|
|
4153
|
+
const stat20 = await fs4.stat(candidate);
|
|
4154
|
+
if (stat20.isFile()) sizes.set(candidate, stat20.size);
|
|
4097
4155
|
} catch {
|
|
4098
4156
|
}
|
|
4099
4157
|
}
|
|
@@ -5551,7 +5609,7 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
5551
5609
|
}
|
|
5552
5610
|
const goBinary = resolveWin32Command("go");
|
|
5553
5611
|
const goResult = await new Promise(
|
|
5554
|
-
(
|
|
5612
|
+
(resolve18, reject) => {
|
|
5555
5613
|
let settled = false;
|
|
5556
5614
|
const proc = spawn5(goBinary, ["run", scriptPath], {
|
|
5557
5615
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -5580,7 +5638,7 @@ async function syncGoParse(filePath, content, lang) {
|
|
|
5580
5638
|
if (settled) return;
|
|
5581
5639
|
settled = true;
|
|
5582
5640
|
clearTimeout(timer);
|
|
5583
|
-
|
|
5641
|
+
resolve18({ code: code2, stdout: stdout2 });
|
|
5584
5642
|
});
|
|
5585
5643
|
}
|
|
5586
5644
|
);
|
|
@@ -6245,7 +6303,7 @@ async function resolvePython() {
|
|
|
6245
6303
|
return null;
|
|
6246
6304
|
}
|
|
6247
6305
|
function commandIsAvailable(command) {
|
|
6248
|
-
return new Promise((
|
|
6306
|
+
return new Promise((resolve18) => {
|
|
6249
6307
|
let settled = false;
|
|
6250
6308
|
const proc = spawn6(command, ["--version"], {
|
|
6251
6309
|
stdio: "ignore",
|
|
@@ -6255,7 +6313,7 @@ function commandIsAvailable(command) {
|
|
|
6255
6313
|
if (settled) return;
|
|
6256
6314
|
settled = true;
|
|
6257
6315
|
clearTimeout(timer);
|
|
6258
|
-
|
|
6316
|
+
resolve18(available);
|
|
6259
6317
|
};
|
|
6260
6318
|
const timer = setTimeout(() => {
|
|
6261
6319
|
proc.kill("SIGKILL");
|
|
@@ -6267,7 +6325,7 @@ function commandIsAvailable(command) {
|
|
|
6267
6325
|
});
|
|
6268
6326
|
}
|
|
6269
6327
|
function spawnPyParser(pyBinary, scriptPath, filePath, content) {
|
|
6270
|
-
return new Promise((
|
|
6328
|
+
return new Promise((resolve18, reject) => {
|
|
6271
6329
|
let settled = false;
|
|
6272
6330
|
const proc = spawn6(pyBinary, [scriptPath, filePath], {
|
|
6273
6331
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -6296,7 +6354,7 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
|
|
|
6296
6354
|
if (settled) return;
|
|
6297
6355
|
settled = true;
|
|
6298
6356
|
clearTimeout(timer);
|
|
6299
|
-
|
|
6357
|
+
resolve18({ code, stdout });
|
|
6300
6358
|
});
|
|
6301
6359
|
});
|
|
6302
6360
|
}
|
|
@@ -6705,9 +6763,9 @@ function parseSymbols6(opts) {
|
|
|
6705
6763
|
function regexParse2(opts) {
|
|
6706
6764
|
const { file, content, lang } = opts;
|
|
6707
6765
|
const symbols = [];
|
|
6708
|
-
const
|
|
6709
|
-
const isPackageJson =
|
|
6710
|
-
const isTsconfig =
|
|
6766
|
+
const basename15 = path22.basename(file).toLowerCase();
|
|
6767
|
+
const isPackageJson = basename15 === "package.json";
|
|
6768
|
+
const isTsconfig = basename15 === "tsconfig.json" || basename15 === "tsconfig.build.json";
|
|
6711
6769
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
6712
6770
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
6713
6771
|
const lines = content.split("\n");
|
|
@@ -7989,11 +8047,18 @@ function ensureSessionShell(opts = {}) {
|
|
|
7989
8047
|
init_spawn_stream();
|
|
7990
8048
|
init_util();
|
|
7991
8049
|
init_legacy_bridge();
|
|
8050
|
+
var SEVERITY_RANK = {
|
|
8051
|
+
info: 0,
|
|
8052
|
+
low: 1,
|
|
8053
|
+
moderate: 2,
|
|
8054
|
+
high: 3,
|
|
8055
|
+
critical: 4
|
|
8056
|
+
};
|
|
7992
8057
|
var auditTool = {
|
|
7993
8058
|
name: "audit",
|
|
7994
8059
|
category: "Package Management",
|
|
7995
8060
|
description: "Run a security audit against project dependencies (using pnpm/npm audit). Reports known vulnerabilities with severity.",
|
|
7996
|
-
usageHint: "CRITICAL SECURITY TOOL:\n\n- Run regularly and especially before any release.\n- Use `level` to focus on high/critical issues.\n- `
|
|
8061
|
+
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.",
|
|
7997
8062
|
permission: "confirm",
|
|
7998
8063
|
mutating: false,
|
|
7999
8064
|
capabilities: ["shell.restricted"],
|
|
@@ -8008,8 +8073,10 @@ var auditTool = {
|
|
|
8008
8073
|
enum: ["low", "moderate", "high", "critical"],
|
|
8009
8074
|
description: "Minimum severity level to report"
|
|
8010
8075
|
},
|
|
8011
|
-
fix: {
|
|
8012
|
-
|
|
8076
|
+
fix: {
|
|
8077
|
+
type: "boolean",
|
|
8078
|
+
description: "Deprecated and rejected \u2014 this tool is read-only and never modifies dependencies. Use `install` (or `language_package`) to remediate vulnerabilities."
|
|
8079
|
+
}
|
|
8013
8080
|
}
|
|
8014
8081
|
},
|
|
8015
8082
|
async execute(input, ctx, opts) {
|
|
@@ -8023,6 +8090,11 @@ var auditTool = {
|
|
|
8023
8090
|
return final;
|
|
8024
8091
|
},
|
|
8025
8092
|
async *executeStream(input, ctx, opts) {
|
|
8093
|
+
if (input.fix === true) {
|
|
8094
|
+
throw new Error(
|
|
8095
|
+
"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)."
|
|
8096
|
+
);
|
|
8097
|
+
}
|
|
8026
8098
|
const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
|
|
8027
8099
|
const bridge = await tryLegacyPackageOperation("package-audit", {
|
|
8028
8100
|
cwd,
|
|
@@ -8052,13 +8124,11 @@ var auditTool = {
|
|
|
8052
8124
|
};
|
|
8053
8125
|
return;
|
|
8054
8126
|
}
|
|
8055
|
-
const manager = await detectPackageManager(cwd);
|
|
8127
|
+
const manager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
8056
8128
|
yield { type: "log", text: `Auditing with ${manager}\u2026`, data: { manager } };
|
|
8057
8129
|
const args = ["audit", "--json"];
|
|
8058
|
-
if (input.
|
|
8059
|
-
|
|
8060
|
-
const pkgs = Array.isArray(input.packages) ? input.packages : input.packages.split(",");
|
|
8061
|
-
args.push(...pkgs.map((p) => p.trim()));
|
|
8130
|
+
if (input.level && (manager === "npm" || manager === "pnpm")) {
|
|
8131
|
+
args.push(`--audit-level=${input.level}`);
|
|
8062
8132
|
}
|
|
8063
8133
|
const result = yield* spawnStream({
|
|
8064
8134
|
cmd: manager,
|
|
@@ -8067,10 +8137,16 @@ var auditTool = {
|
|
|
8067
8137
|
signal: opts.signal,
|
|
8068
8138
|
maxBytes: 1e5
|
|
8069
8139
|
});
|
|
8070
|
-
yield {
|
|
8140
|
+
yield {
|
|
8141
|
+
type: "final",
|
|
8142
|
+
output: parseAuditOutput(result.stdout, result.exitCode, {
|
|
8143
|
+
level: input.level,
|
|
8144
|
+
spawnTruncated: result.truncated
|
|
8145
|
+
})
|
|
8146
|
+
};
|
|
8071
8147
|
}
|
|
8072
8148
|
};
|
|
8073
|
-
function parseAuditOutput(json2, exitCode) {
|
|
8149
|
+
function parseAuditOutput(json2, exitCode, opts = {}) {
|
|
8074
8150
|
if (!json2) {
|
|
8075
8151
|
return {
|
|
8076
8152
|
exit_code: exitCode,
|
|
@@ -8081,18 +8157,14 @@ function parseAuditOutput(json2, exitCode) {
|
|
|
8081
8157
|
truncated: false
|
|
8082
8158
|
};
|
|
8083
8159
|
}
|
|
8160
|
+
const cappedOutput = normalizeCommandOutput(json2);
|
|
8161
|
+
const truncated = opts.spawnTruncated === true || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
|
|
8084
8162
|
try {
|
|
8085
8163
|
const data = JSON.parse(json2);
|
|
8086
|
-
|
|
8087
|
-
const
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
advisories.push({
|
|
8091
|
-
severity: adv.severity ?? "unknown",
|
|
8092
|
-
package: adv.module_name ?? id,
|
|
8093
|
-
title: adv.title ?? "Unknown vulnerability",
|
|
8094
|
-
url: adv.url ?? ""
|
|
8095
|
-
});
|
|
8164
|
+
let advisories = extractAdvisories(data);
|
|
8165
|
+
const minRank = opts.level ? SEVERITY_RANK[opts.level] ?? 0 : 0;
|
|
8166
|
+
if (minRank > 0) {
|
|
8167
|
+
advisories = advisories.filter((a) => (SEVERITY_RANK[a.severity] ?? 0) >= minRank);
|
|
8096
8168
|
}
|
|
8097
8169
|
const total = advisories.length;
|
|
8098
8170
|
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`;
|
|
@@ -8101,8 +8173,8 @@ function parseAuditOutput(json2, exitCode) {
|
|
|
8101
8173
|
vulnerabilities: advisories,
|
|
8102
8174
|
total,
|
|
8103
8175
|
summary,
|
|
8104
|
-
output:
|
|
8105
|
-
truncated
|
|
8176
|
+
output: cappedOutput,
|
|
8177
|
+
truncated
|
|
8106
8178
|
};
|
|
8107
8179
|
} catch {
|
|
8108
8180
|
return {
|
|
@@ -8110,15 +8182,47 @@ function parseAuditOutput(json2, exitCode) {
|
|
|
8110
8182
|
vulnerabilities: [],
|
|
8111
8183
|
total: 0,
|
|
8112
8184
|
summary: "Could not parse audit output",
|
|
8113
|
-
output:
|
|
8114
|
-
truncated
|
|
8185
|
+
output: cappedOutput,
|
|
8186
|
+
truncated
|
|
8115
8187
|
};
|
|
8116
8188
|
}
|
|
8117
8189
|
}
|
|
8190
|
+
function extractAdvisories(data) {
|
|
8191
|
+
const advisories = [];
|
|
8192
|
+
const ads = data["advisories"];
|
|
8193
|
+
if (ads && typeof ads === "object") {
|
|
8194
|
+
for (const [id, value] of Object.entries(ads)) {
|
|
8195
|
+
const adv = value ?? {};
|
|
8196
|
+
advisories.push({
|
|
8197
|
+
severity: typeof adv["severity"] === "string" ? adv["severity"] : "unknown",
|
|
8198
|
+
package: typeof adv["module_name"] === "string" ? adv["module_name"] : id,
|
|
8199
|
+
title: typeof adv["title"] === "string" ? adv["title"] : "Unknown vulnerability",
|
|
8200
|
+
url: typeof adv["url"] === "string" ? adv["url"] : ""
|
|
8201
|
+
});
|
|
8202
|
+
}
|
|
8203
|
+
return advisories;
|
|
8204
|
+
}
|
|
8205
|
+
const vulns = data["vulnerabilities"];
|
|
8206
|
+
if (vulns && typeof vulns === "object") {
|
|
8207
|
+
for (const [pkg, value] of Object.entries(vulns)) {
|
|
8208
|
+
const vuln = value ?? {};
|
|
8209
|
+
const via = Array.isArray(vuln["via"]) ? vuln["via"] : [];
|
|
8210
|
+
const detail = via.find((v) => !!v && typeof v === "object");
|
|
8211
|
+
advisories.push({
|
|
8212
|
+
severity: typeof vuln["severity"] === "string" ? vuln["severity"] : "unknown",
|
|
8213
|
+
package: pkg,
|
|
8214
|
+
title: detail && typeof detail["title"] === "string" ? detail["title"] : "Unknown vulnerability",
|
|
8215
|
+
url: detail && typeof detail["url"] === "string" ? detail["url"] : ""
|
|
8216
|
+
});
|
|
8217
|
+
}
|
|
8218
|
+
}
|
|
8219
|
+
return advisories;
|
|
8220
|
+
}
|
|
8118
8221
|
|
|
8119
8222
|
// src/bash.ts
|
|
8120
8223
|
import { spawn as spawn3 } from "node:child_process";
|
|
8121
8224
|
import * as os4 from "node:os";
|
|
8225
|
+
import { StringDecoder } from "node:string_decoder";
|
|
8122
8226
|
import {
|
|
8123
8227
|
emitProcessCompleted as emitProcessCompleted2,
|
|
8124
8228
|
emitProcessOutput as emitProcessOutput2,
|
|
@@ -8399,7 +8503,6 @@ var PersistentProcessRegistry = class {
|
|
|
8399
8503
|
try {
|
|
8400
8504
|
const data = await readRegistryFile(this.registryPath);
|
|
8401
8505
|
data.instances.set(String(entry.pid), entry);
|
|
8402
|
-
const child = null;
|
|
8403
8506
|
this.baseRegistry.register({
|
|
8404
8507
|
pid: entry.pid,
|
|
8405
8508
|
name: entry.name,
|
|
@@ -8407,7 +8510,7 @@ var PersistentProcessRegistry = class {
|
|
|
8407
8510
|
startedAt: entry.startedAt,
|
|
8408
8511
|
sessionId: entry.sessionId,
|
|
8409
8512
|
protected: entry.protected,
|
|
8410
|
-
child
|
|
8513
|
+
child: null
|
|
8411
8514
|
});
|
|
8412
8515
|
await writeRegistryFile(this.registryPath, data);
|
|
8413
8516
|
} finally {
|
|
@@ -8973,7 +9076,7 @@ function looksLikePowerShell(command) {
|
|
|
8973
9076
|
return true;
|
|
8974
9077
|
}
|
|
8975
9078
|
if (PS_VERB_RE.test(trimmed)) return true;
|
|
8976
|
-
if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps
|
|
9079
|
+
if (/(?:^|[\s;&|])(gci|gi|gp|gcm|gps)\b/i.test(trimmed)) {
|
|
8977
9080
|
return true;
|
|
8978
9081
|
}
|
|
8979
9082
|
if (looksLikePowerShellExtended(command)) return true;
|
|
@@ -9065,7 +9168,7 @@ var bashTool = {
|
|
|
9065
9168
|
name: "bash",
|
|
9066
9169
|
category: "Shell",
|
|
9067
9170
|
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.",
|
|
9068
|
-
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.",
|
|
9171
|
+
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.",
|
|
9069
9172
|
selection: {
|
|
9070
9173
|
doNotUseWhen: "the command is allowlisted and does not require pipes, redirection, or shell expansion.",
|
|
9071
9174
|
useInstead: ["exec"]
|
|
@@ -9079,7 +9182,14 @@ var bashTool = {
|
|
|
9079
9182
|
// explicitly removes the implicit cross-tool aliasing.
|
|
9080
9183
|
subjectKey: "command",
|
|
9081
9184
|
capabilities: ["shell.arbitrary"],
|
|
9082
|
-
|
|
9185
|
+
// Executor-level abort ceiling. Must sit ABOVE the per-call `timeout_ms`
|
|
9186
|
+
// ceiling (600_000): the tool's own timer tree-kills and returns a
|
|
9187
|
+
// structured `timed_out: true` result, while the executor's
|
|
9188
|
+
// AbortSignal.timeout is a blunt abort. The old value (300_000) meant any
|
|
9189
|
+
// timeout_ms > 5min was silently cut short by the executor. The 10s margin
|
|
9190
|
+
// covers the kill/teardown window. (The executor additionally clamps to
|
|
9191
|
+
// config `tools.maxToolTimeoutMs`.)
|
|
9192
|
+
timeoutMs: 61e4,
|
|
9083
9193
|
maxOutputBytes: MAX_OUTPUT,
|
|
9084
9194
|
estimatedDurationMs: 3e4,
|
|
9085
9195
|
inputSchema: {
|
|
@@ -9091,7 +9201,7 @@ var bashTool = {
|
|
|
9091
9201
|
},
|
|
9092
9202
|
timeout_ms: {
|
|
9093
9203
|
type: "integer",
|
|
9094
|
-
description: "Optional timeout for this specific command in milliseconds."
|
|
9204
|
+
description: "Optional timeout for this specific command in milliseconds (default 300000, max 600000)."
|
|
9095
9205
|
},
|
|
9096
9206
|
background: {
|
|
9097
9207
|
type: "boolean",
|
|
@@ -9142,16 +9252,7 @@ var bashTool = {
|
|
|
9142
9252
|
return;
|
|
9143
9253
|
}
|
|
9144
9254
|
const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
|
|
9145
|
-
|
|
9146
|
-
console.warn(JSON.stringify({
|
|
9147
|
-
level: "warn",
|
|
9148
|
-
event: "bash.pipe_to_shell_detected",
|
|
9149
|
-
message: "Detected pipe-to-shell pattern. Consider reviewing the full command before confirming.",
|
|
9150
|
-
command_prefix: input.command.slice(0, 100),
|
|
9151
|
-
// Log first 100 chars for review
|
|
9152
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9153
|
-
}));
|
|
9154
|
-
}
|
|
9255
|
+
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." : "";
|
|
9155
9256
|
const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS2, 6e5));
|
|
9156
9257
|
const isWin5 = os4.platform() === "win32";
|
|
9157
9258
|
let plan;
|
|
@@ -9185,11 +9286,12 @@ var bashTool = {
|
|
|
9185
9286
|
const shell = plan.bin;
|
|
9186
9287
|
const args = plan.useStdin ? [...plan.argv] : [...plan.argv, input.command];
|
|
9187
9288
|
const env = buildChildEnv2(ctx.session?.id);
|
|
9289
|
+
const spawnCwd = ctx.workingDir ?? ctx.projectRoot;
|
|
9188
9290
|
const detached = !isWin5;
|
|
9189
9291
|
const startedAt = Date.now();
|
|
9190
9292
|
if (input.background) {
|
|
9191
9293
|
const child2 = spawn3(shell, args, {
|
|
9192
|
-
cwd:
|
|
9294
|
+
cwd: spawnCwd,
|
|
9193
9295
|
env,
|
|
9194
9296
|
// PowerShell takes the script on stdin (no argv quoting); cmd.exe
|
|
9195
9297
|
// and POSIX shells ignore stdin when given the command inline.
|
|
@@ -9220,7 +9322,7 @@ var bashTool = {
|
|
|
9220
9322
|
parentPid: process.pid,
|
|
9221
9323
|
command: redactCommand(`${shell} ${args.join(" ")}`),
|
|
9222
9324
|
args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
|
|
9223
|
-
cwd:
|
|
9325
|
+
cwd: spawnCwd,
|
|
9224
9326
|
background: true,
|
|
9225
9327
|
startedAt: new Date(startedAt).toISOString()
|
|
9226
9328
|
});
|
|
@@ -9264,7 +9366,9 @@ var bashTool = {
|
|
|
9264
9366
|
yield {
|
|
9265
9367
|
type: "final",
|
|
9266
9368
|
output: {
|
|
9267
|
-
output
|
|
9369
|
+
// Background runs have no captured output; the pipe-to-shell caution
|
|
9370
|
+
// (when present) is the only thing worth surfacing.
|
|
9371
|
+
output: pipeToShellNote.trim(),
|
|
9268
9372
|
exit_code: null,
|
|
9269
9373
|
timed_out: false,
|
|
9270
9374
|
pid: pid2
|
|
@@ -9281,7 +9385,7 @@ var bashTool = {
|
|
|
9281
9385
|
return;
|
|
9282
9386
|
}
|
|
9283
9387
|
const child = spawn3(shell, args, {
|
|
9284
|
-
cwd:
|
|
9388
|
+
cwd: spawnCwd,
|
|
9285
9389
|
env,
|
|
9286
9390
|
// PowerShell takes the script on stdin (no argv quoting); cmd.exe
|
|
9287
9391
|
// and POSIX shells ignore stdin when given the command inline.
|
|
@@ -9306,7 +9410,7 @@ var bashTool = {
|
|
|
9306
9410
|
parentPid: process.pid,
|
|
9307
9411
|
command: redactCommand(`${shell} ${args.join(" ")}`),
|
|
9308
9412
|
args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
|
|
9309
|
-
cwd:
|
|
9413
|
+
cwd: spawnCwd,
|
|
9310
9414
|
background: false,
|
|
9311
9415
|
startedAt: new Date(startedAt).toISOString()
|
|
9312
9416
|
});
|
|
@@ -9397,10 +9501,10 @@ var bashTool = {
|
|
|
9397
9501
|
queue.push(c);
|
|
9398
9502
|
}
|
|
9399
9503
|
};
|
|
9400
|
-
const next = () => new Promise((
|
|
9504
|
+
const next = () => new Promise((resolve18) => {
|
|
9401
9505
|
const c = queue.shift();
|
|
9402
|
-
if (c)
|
|
9403
|
-
else resolveNext =
|
|
9506
|
+
if (c) resolve18(c);
|
|
9507
|
+
else resolveNext = resolve18;
|
|
9404
9508
|
});
|
|
9405
9509
|
let lastFlush = Date.now();
|
|
9406
9510
|
const flush = () => {
|
|
@@ -9425,8 +9529,10 @@ var bashTool = {
|
|
|
9425
9529
|
child.stderr?.resume();
|
|
9426
9530
|
}
|
|
9427
9531
|
};
|
|
9532
|
+
const stdoutDecoder = new StringDecoder("utf8");
|
|
9533
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
9428
9534
|
const onData = (chunk, stream) => {
|
|
9429
|
-
const text =
|
|
9535
|
+
const text = (stream === "stdout" ? stdoutDecoder : stderrDecoder).write(chunk);
|
|
9430
9536
|
if (stream === "stdout") stdoutBytes += chunk.byteLength;
|
|
9431
9537
|
else stderrBytes += chunk.byteLength;
|
|
9432
9538
|
emitProcessOutput2({ pid, stream, chunk });
|
|
@@ -9453,6 +9559,12 @@ var bashTool = {
|
|
|
9453
9559
|
if (typeof pid === "number") registry.unregister(pid);
|
|
9454
9560
|
registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
|
|
9455
9561
|
completeForeground(timedOut ? 124 : code ?? (signal ? 1 : 0), signal ?? void 0);
|
|
9562
|
+
const tail = stdoutDecoder.end() + stderrDecoder.end();
|
|
9563
|
+
if (tail) {
|
|
9564
|
+
if (buf.length < MAX_OUTPUT) buf += tail.slice(0, MAX_OUTPUT - buf.length);
|
|
9565
|
+
spool.write(tail);
|
|
9566
|
+
pending2 += tail;
|
|
9567
|
+
}
|
|
9456
9568
|
push({ kind: "end", code });
|
|
9457
9569
|
});
|
|
9458
9570
|
try {
|
|
@@ -9472,7 +9584,7 @@ var bashTool = {
|
|
|
9472
9584
|
output: {
|
|
9473
9585
|
output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
|
|
9474
9586
|
|
|
9475
|
-
${hint}` : ""),
|
|
9587
|
+
${hint}` : "") + pipeToShellNote,
|
|
9476
9588
|
exit_code: c.code,
|
|
9477
9589
|
timed_out: timedOut
|
|
9478
9590
|
}
|
|
@@ -9534,7 +9646,7 @@ ${hint}` : ""),
|
|
|
9534
9646
|
if (!sessionId) return;
|
|
9535
9647
|
for (const entry of registry.bySession(sessionId)) {
|
|
9536
9648
|
if (entry.name !== "bash") continue;
|
|
9537
|
-
if (entry.child.exitCode !== null) continue;
|
|
9649
|
+
if (entry.child && entry.child.exitCode !== null) continue;
|
|
9538
9650
|
if (entry.background) continue;
|
|
9539
9651
|
if (entry.protected) continue;
|
|
9540
9652
|
registry.kill(entry.pid, { force: true });
|
|
@@ -9769,8 +9881,8 @@ function sweepOldArtifacts(root) {
|
|
|
9769
9881
|
for (const name of names) {
|
|
9770
9882
|
const target = path10.join(dir, name);
|
|
9771
9883
|
try {
|
|
9772
|
-
const
|
|
9773
|
-
if (
|
|
9884
|
+
const stat20 = await fs7.stat(target);
|
|
9885
|
+
if (stat20.isFile() && stat20.mtimeMs < cutoff) {
|
|
9774
9886
|
await fs7.rm(target, { force: true });
|
|
9775
9887
|
removed++;
|
|
9776
9888
|
}
|
|
@@ -9798,14 +9910,14 @@ var BrowserArtifactStore = class {
|
|
|
9798
9910
|
}
|
|
9799
9911
|
async record(id, sessionId, kind, target, mimeType) {
|
|
9800
9912
|
await fs7.chmod(target, 384).catch(() => void 0);
|
|
9801
|
-
const [
|
|
9913
|
+
const [stat20, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
|
|
9802
9914
|
const artifact = {
|
|
9803
9915
|
id,
|
|
9804
9916
|
kind,
|
|
9805
9917
|
sensitivity: "sensitive",
|
|
9806
9918
|
path: target,
|
|
9807
9919
|
mimeType,
|
|
9808
|
-
sizeBytes:
|
|
9920
|
+
sizeBytes: stat20.size,
|
|
9809
9921
|
sha256,
|
|
9810
9922
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9811
9923
|
};
|
|
@@ -9845,11 +9957,11 @@ var BrowserArtifactStore = class {
|
|
|
9845
9957
|
};
|
|
9846
9958
|
async function hashFile(target) {
|
|
9847
9959
|
const hash = createHash3("sha256");
|
|
9848
|
-
await new Promise((
|
|
9960
|
+
await new Promise((resolve18, reject) => {
|
|
9849
9961
|
const stream = createReadStream(target);
|
|
9850
9962
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
9851
9963
|
stream.once("error", reject);
|
|
9852
|
-
stream.once("end",
|
|
9964
|
+
stream.once("end", resolve18);
|
|
9853
9965
|
});
|
|
9854
9966
|
return hash.digest("hex");
|
|
9855
9967
|
}
|
|
@@ -10004,7 +10116,7 @@ var BrowserNetworkGuardProxy = class {
|
|
|
10004
10116
|
async start() {
|
|
10005
10117
|
if (this.url) return this.url;
|
|
10006
10118
|
if (this.startPromise) return this.startPromise;
|
|
10007
|
-
this.startPromise = new Promise((
|
|
10119
|
+
this.startPromise = new Promise((resolve18, reject) => {
|
|
10008
10120
|
const onError = (error) => {
|
|
10009
10121
|
this.server.off("listening", onListening);
|
|
10010
10122
|
reject(error);
|
|
@@ -10017,7 +10129,7 @@ var BrowserNetworkGuardProxy = class {
|
|
|
10017
10129
|
return;
|
|
10018
10130
|
}
|
|
10019
10131
|
this.url = `http://127.0.0.1:${address.port}`;
|
|
10020
|
-
|
|
10132
|
+
resolve18(this.url);
|
|
10021
10133
|
};
|
|
10022
10134
|
this.server.once("error", onError);
|
|
10023
10135
|
this.server.once("listening", onListening);
|
|
@@ -10032,7 +10144,7 @@ var BrowserNetworkGuardProxy = class {
|
|
|
10032
10144
|
for (const socket of this.sockets) socket.destroy();
|
|
10033
10145
|
this.sockets.clear();
|
|
10034
10146
|
if (!this.server.listening) return;
|
|
10035
|
-
await new Promise((
|
|
10147
|
+
await new Promise((resolve18) => this.server.close(() => resolve18()));
|
|
10036
10148
|
}
|
|
10037
10149
|
async forwardHttp(request2, response) {
|
|
10038
10150
|
try {
|
|
@@ -10062,8 +10174,8 @@ var BrowserNetworkGuardProxy = class {
|
|
|
10062
10174
|
upstream.on("error", () => writeProxyError(response, 502, "Bad Gateway"));
|
|
10063
10175
|
request2.on("aborted", () => upstream.destroy());
|
|
10064
10176
|
request2.pipe(upstream);
|
|
10065
|
-
} catch {
|
|
10066
|
-
writeProxyError(response, 403,
|
|
10177
|
+
} catch (error) {
|
|
10178
|
+
writeProxyError(response, 403, policyBlockMessage(error));
|
|
10067
10179
|
}
|
|
10068
10180
|
}
|
|
10069
10181
|
async forwardConnect(request2, client, head) {
|
|
@@ -10087,8 +10199,8 @@ var BrowserNetworkGuardProxy = class {
|
|
|
10087
10199
|
pipeDuplexPair(upstream, client);
|
|
10088
10200
|
});
|
|
10089
10201
|
client.once("close", () => upstream.destroy());
|
|
10090
|
-
} catch {
|
|
10091
|
-
client.end(
|
|
10202
|
+
} catch (error) {
|
|
10203
|
+
client.end(rawForbiddenResponse(policyBlockMessage(error)));
|
|
10092
10204
|
}
|
|
10093
10205
|
}
|
|
10094
10206
|
async forwardUpgrade(request2, client, head) {
|
|
@@ -10129,9 +10241,9 @@ var BrowserNetworkGuardProxy = class {
|
|
|
10129
10241
|
});
|
|
10130
10242
|
client.once("close", () => upstream?.destroy());
|
|
10131
10243
|
upstream.end();
|
|
10132
|
-
} catch {
|
|
10244
|
+
} catch (error) {
|
|
10133
10245
|
upstream?.destroy();
|
|
10134
|
-
client.end(
|
|
10246
|
+
client.end(rawForbiddenResponse(policyBlockMessage(error)));
|
|
10135
10247
|
}
|
|
10136
10248
|
}
|
|
10137
10249
|
resolve(rawUrl) {
|
|
@@ -10171,6 +10283,18 @@ function writeProxyError(response, status, message) {
|
|
|
10171
10283
|
response.writeHead(status, { "content-type": "text/plain", connection: "close" });
|
|
10172
10284
|
response.end(message);
|
|
10173
10285
|
}
|
|
10286
|
+
function policyBlockMessage(error) {
|
|
10287
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
10288
|
+
return reason ? `Blocked by browser network policy: ${reason}` : "Blocked by browser network policy";
|
|
10289
|
+
}
|
|
10290
|
+
function rawForbiddenResponse(message) {
|
|
10291
|
+
return `HTTP/1.1 403 Forbidden\r
|
|
10292
|
+
Content-Type: text/plain\r
|
|
10293
|
+
Content-Length: ${Buffer.byteLength(message)}\r
|
|
10294
|
+
Connection: close\r
|
|
10295
|
+
\r
|
|
10296
|
+
` + message;
|
|
10297
|
+
}
|
|
10174
10298
|
|
|
10175
10299
|
// src/browser/manager.ts
|
|
10176
10300
|
var DEFAULT_OPERATION_TIMEOUT_MS = 3e4;
|
|
@@ -10398,13 +10522,21 @@ var BrowserSessionManager = class {
|
|
|
10398
10522
|
if (relative13.startsWith("..") || path11.isAbsolute(relative13)) {
|
|
10399
10523
|
throw new Error("browser: upload files must stay inside the project root");
|
|
10400
10524
|
}
|
|
10401
|
-
|
|
10525
|
+
let realFile;
|
|
10526
|
+
try {
|
|
10527
|
+
realFile = await fs8.realpath(absolute);
|
|
10528
|
+
} catch (error) {
|
|
10529
|
+
if (error?.code === "ENOENT") {
|
|
10530
|
+
throw new Error(`browser: upload file not found: ${file}`);
|
|
10531
|
+
}
|
|
10532
|
+
throw error;
|
|
10533
|
+
}
|
|
10402
10534
|
const realRelative = path11.relative(realRoot, realFile);
|
|
10403
10535
|
if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
|
|
10404
10536
|
throw new Error("browser: upload files must not escape the project root through a symlink");
|
|
10405
10537
|
}
|
|
10406
|
-
const
|
|
10407
|
-
if (!
|
|
10538
|
+
const stat20 = await fs8.stat(realFile);
|
|
10539
|
+
if (!stat20.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
|
|
10408
10540
|
resolved.push(realFile);
|
|
10409
10541
|
}
|
|
10410
10542
|
await this.runPageOperation(
|
|
@@ -10585,7 +10717,7 @@ function pushBounded(target, value, limit) {
|
|
|
10585
10717
|
}
|
|
10586
10718
|
async function abortable(signal, operation, onAbort) {
|
|
10587
10719
|
signal.throwIfAborted();
|
|
10588
|
-
return new Promise((
|
|
10720
|
+
return new Promise((resolve18, reject) => {
|
|
10589
10721
|
let settled = false;
|
|
10590
10722
|
let aborting = false;
|
|
10591
10723
|
const finish = (fn) => {
|
|
@@ -10603,7 +10735,7 @@ async function abortable(signal, operation, onAbort) {
|
|
|
10603
10735
|
signal.addEventListener("abort", abort, { once: true });
|
|
10604
10736
|
operation().then(
|
|
10605
10737
|
(value) => {
|
|
10606
|
-
if (!aborting) finish(() =>
|
|
10738
|
+
if (!aborting) finish(() => resolve18(value));
|
|
10607
10739
|
},
|
|
10608
10740
|
(err) => {
|
|
10609
10741
|
if (!aborting) finish(() => reject(err));
|
|
@@ -10663,7 +10795,7 @@ var sessionIdSchema = {
|
|
|
10663
10795
|
};
|
|
10664
10796
|
var browserOpenTool = {
|
|
10665
10797
|
name: "browser_open",
|
|
10666
|
-
description: "Open an isolated first-party Playwright browser session, optionally navigating to a URL.",
|
|
10798
|
+
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.",
|
|
10667
10799
|
usageHint: "browser_open({ url?, width?, height?, trace? })",
|
|
10668
10800
|
permission: "confirm",
|
|
10669
10801
|
mutating: true,
|
|
@@ -10716,7 +10848,7 @@ var browserStatusTool = {
|
|
|
10716
10848
|
};
|
|
10717
10849
|
var browserNavigateTool = {
|
|
10718
10850
|
name: "browser_navigate",
|
|
10719
|
-
description: "Navigate an owned browser session to an approved http(s) URL.",
|
|
10851
|
+
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.",
|
|
10720
10852
|
usageHint: "browser_navigate({ sessionId, url })",
|
|
10721
10853
|
permission: "confirm",
|
|
10722
10854
|
mutating: true,
|
|
@@ -11060,7 +11192,7 @@ async function shutdownBrowserTools() {
|
|
|
11060
11192
|
import { spawn as spawn4 } from "node:child_process";
|
|
11061
11193
|
import * as fs13 from "node:fs";
|
|
11062
11194
|
import * as net3 from "node:net";
|
|
11063
|
-
import { StringDecoder } from "node:string_decoder";
|
|
11195
|
+
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
11064
11196
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
11065
11197
|
import { checkUnixSocketPath } from "@wrongstack/core/utils";
|
|
11066
11198
|
|
|
@@ -14104,12 +14236,12 @@ function projectIndexServerBuildId(entrypoint) {
|
|
|
14104
14236
|
const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
|
|
14105
14237
|
const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path17.resolve(cleanHref);
|
|
14106
14238
|
try {
|
|
14107
|
-
const
|
|
14108
|
-
if (buildIdCache?.file === file && buildIdCache.mtimeMs ===
|
|
14239
|
+
const stat20 = fs12.statSync(file);
|
|
14240
|
+
if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat20.mtimeMs && buildIdCache.size === stat20.size) {
|
|
14109
14241
|
return buildIdCache.buildId;
|
|
14110
14242
|
}
|
|
14111
14243
|
const buildId = createHash4("sha256").update(fs12.readFileSync(file)).digest("hex").slice(0, 24);
|
|
14112
|
-
buildIdCache = { file, mtimeMs:
|
|
14244
|
+
buildIdCache = { file, mtimeMs: stat20.mtimeMs, size: stat20.size, buildId };
|
|
14113
14245
|
return buildId;
|
|
14114
14246
|
} catch {
|
|
14115
14247
|
return `unreadable:${path17.basename(file)}`;
|
|
@@ -14251,8 +14383,8 @@ function isProjectIndexServerHealth(value) {
|
|
|
14251
14383
|
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";
|
|
14252
14384
|
}
|
|
14253
14385
|
function delay(ms) {
|
|
14254
|
-
return new Promise((
|
|
14255
|
-
const timer = setTimeout(
|
|
14386
|
+
return new Promise((resolve18) => {
|
|
14387
|
+
const timer = setTimeout(resolve18, ms);
|
|
14256
14388
|
timer.unref?.();
|
|
14257
14389
|
});
|
|
14258
14390
|
}
|
|
@@ -14456,7 +14588,7 @@ var ProjectServerConnection = class {
|
|
|
14456
14588
|
return Promise.reject(new Error("codebase-index server connection is not available"));
|
|
14457
14589
|
}
|
|
14458
14590
|
const id = this.nextId++;
|
|
14459
|
-
return new Promise((
|
|
14591
|
+
return new Promise((resolve18, reject) => {
|
|
14460
14592
|
const timer = setTimeout(() => {
|
|
14461
14593
|
const entry = this.pending.get(id);
|
|
14462
14594
|
if (!entry) return;
|
|
@@ -14479,7 +14611,7 @@ var ProjectServerConnection = class {
|
|
|
14479
14611
|
entry.reject(cancellationError(signal));
|
|
14480
14612
|
} : void 0;
|
|
14481
14613
|
this.pending.set(id, {
|
|
14482
|
-
resolve:
|
|
14614
|
+
resolve: resolve18,
|
|
14483
14615
|
reject,
|
|
14484
14616
|
timer,
|
|
14485
14617
|
signal,
|
|
@@ -14548,7 +14680,7 @@ var ProjectServerConnection = class {
|
|
|
14548
14680
|
this.binaryBuffer = [];
|
|
14549
14681
|
this.useBinary = false;
|
|
14550
14682
|
this.textDecoder = null;
|
|
14551
|
-
return new Promise((
|
|
14683
|
+
return new Promise((resolve18, reject) => {
|
|
14552
14684
|
const socket = net3.createConnection(this.endpoint);
|
|
14553
14685
|
this.socket = socket;
|
|
14554
14686
|
const timer = setTimeout(() => {
|
|
@@ -14560,7 +14692,7 @@ var ProjectServerConnection = class {
|
|
|
14560
14692
|
clearTimeout(timer);
|
|
14561
14693
|
this.connectResolve = null;
|
|
14562
14694
|
this.connectReject = null;
|
|
14563
|
-
|
|
14695
|
+
resolve18();
|
|
14564
14696
|
};
|
|
14565
14697
|
const finishReject = (error) => {
|
|
14566
14698
|
clearTimeout(timer);
|
|
@@ -14583,7 +14715,7 @@ var ProjectServerConnection = class {
|
|
|
14583
14715
|
this.onBinaryData(socket, chunk);
|
|
14584
14716
|
return;
|
|
14585
14717
|
}
|
|
14586
|
-
if (!this.textDecoder) this.textDecoder = new
|
|
14718
|
+
if (!this.textDecoder) this.textDecoder = new StringDecoder2("utf8");
|
|
14587
14719
|
this.buffer += this.textDecoder.write(chunk);
|
|
14588
14720
|
while (true) {
|
|
14589
14721
|
const newline = this.buffer.indexOf("\n");
|
|
@@ -15608,9 +15740,9 @@ var ParserWorkerPool = class {
|
|
|
15608
15740
|
for (let i = 0; i < files.length; i++) {
|
|
15609
15741
|
chunks[i % workerCount].push(files[i]);
|
|
15610
15742
|
}
|
|
15611
|
-
return new Promise((
|
|
15743
|
+
return new Promise((resolve18, reject) => {
|
|
15612
15744
|
this.pending.set(batchId, {
|
|
15613
|
-
resolve:
|
|
15745
|
+
resolve: resolve18,
|
|
15614
15746
|
reject,
|
|
15615
15747
|
accumulated: [],
|
|
15616
15748
|
expectedWorkers: workerCount,
|
|
@@ -15641,10 +15773,10 @@ var ParserWorkerPool = class {
|
|
|
15641
15773
|
await Promise.allSettled(
|
|
15642
15774
|
workers.map(
|
|
15643
15775
|
(w) => Promise.race([
|
|
15644
|
-
new Promise((
|
|
15645
|
-
w.once("exit", () =>
|
|
15776
|
+
new Promise((resolve18) => {
|
|
15777
|
+
w.once("exit", () => resolve18());
|
|
15646
15778
|
}),
|
|
15647
|
-
new Promise((
|
|
15779
|
+
new Promise((resolve18) => setTimeout(() => resolve18(), 2e3))
|
|
15648
15780
|
]).then(() => {
|
|
15649
15781
|
if (!w.threadId) return;
|
|
15650
15782
|
return w.terminate().catch(() => {
|
|
@@ -15708,7 +15840,7 @@ function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
|
|
|
15708
15840
|
return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
|
|
15709
15841
|
}
|
|
15710
15842
|
function yieldEventLoop() {
|
|
15711
|
-
return new Promise((
|
|
15843
|
+
return new Promise((resolve18) => setImmediate(resolve18));
|
|
15712
15844
|
}
|
|
15713
15845
|
function throwIfAborted(signal) {
|
|
15714
15846
|
if (!signal?.aborted) return;
|
|
@@ -15736,7 +15868,7 @@ function normalizeComparablePath(value) {
|
|
|
15736
15868
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
15737
15869
|
}
|
|
15738
15870
|
function gitOutput(projectRoot, args) {
|
|
15739
|
-
return new Promise((
|
|
15871
|
+
return new Promise((resolve18, reject) => {
|
|
15740
15872
|
execFile(
|
|
15741
15873
|
"git",
|
|
15742
15874
|
["-C", projectRoot, ...args],
|
|
@@ -15747,7 +15879,7 @@ function gitOutput(projectRoot, args) {
|
|
|
15747
15879
|
},
|
|
15748
15880
|
(error, stdout) => {
|
|
15749
15881
|
if (error) reject(error);
|
|
15750
|
-
else
|
|
15882
|
+
else resolve18(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
|
|
15751
15883
|
}
|
|
15752
15884
|
);
|
|
15753
15885
|
});
|
|
@@ -15979,9 +16111,9 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15979
16111
|
const statReadParse = await Promise.allSettled(
|
|
15980
16112
|
batchFiles.map(
|
|
15981
16113
|
async (file) => {
|
|
15982
|
-
let
|
|
16114
|
+
let stat20;
|
|
15983
16115
|
try {
|
|
15984
|
-
|
|
16116
|
+
stat20 = await fs18.stat(file, statOpts);
|
|
15985
16117
|
} catch (e) {
|
|
15986
16118
|
if (isAbortError(e)) throw e;
|
|
15987
16119
|
return {
|
|
@@ -15993,21 +16125,21 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15993
16125
|
missing: isMissingPathError(e)
|
|
15994
16126
|
};
|
|
15995
16127
|
}
|
|
15996
|
-
if (!
|
|
16128
|
+
if (!stat20.isFile()) return { file, stat: stat20, lang: "", parsed: null };
|
|
15997
16129
|
const lang = detectLang(file);
|
|
15998
|
-
if (!lang) return { file, stat:
|
|
15999
|
-
if (
|
|
16130
|
+
if (!lang) return { file, stat: stat20, lang: "", parsed: null };
|
|
16131
|
+
if (stat20.size > MAX_INDEX_FILE_BYTES) {
|
|
16000
16132
|
return {
|
|
16001
16133
|
file,
|
|
16002
|
-
stat:
|
|
16134
|
+
stat: stat20,
|
|
16003
16135
|
lang,
|
|
16004
16136
|
parsed: null,
|
|
16005
|
-
error: `file too large (${
|
|
16137
|
+
error: `file too large (${stat20.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
16006
16138
|
};
|
|
16007
16139
|
}
|
|
16008
16140
|
const meta = existingMeta.get(file);
|
|
16009
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
16010
|
-
return { file, stat:
|
|
16141
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat20.mtimeMs)) {
|
|
16142
|
+
return { file, stat: stat20, lang, parsed: null, skippedMeta: meta };
|
|
16011
16143
|
}
|
|
16012
16144
|
let content;
|
|
16013
16145
|
try {
|
|
@@ -16016,7 +16148,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16016
16148
|
if (isAbortError(e)) throw e;
|
|
16017
16149
|
return {
|
|
16018
16150
|
file,
|
|
16019
|
-
stat:
|
|
16151
|
+
stat: stat20,
|
|
16020
16152
|
lang,
|
|
16021
16153
|
parsed: null,
|
|
16022
16154
|
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
@@ -16026,15 +16158,15 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16026
16158
|
if (!force && meta && meta.contentHash && contentHash === meta.contentHash) {
|
|
16027
16159
|
return {
|
|
16028
16160
|
file,
|
|
16029
|
-
stat:
|
|
16161
|
+
stat: stat20,
|
|
16030
16162
|
lang,
|
|
16031
16163
|
parsed: null,
|
|
16032
16164
|
content,
|
|
16033
16165
|
contentHash,
|
|
16034
|
-
skippedMeta: { ...meta, mtimeMs: Math.floor(
|
|
16166
|
+
skippedMeta: { ...meta, mtimeMs: Math.floor(stat20.mtimeMs) }
|
|
16035
16167
|
};
|
|
16036
16168
|
}
|
|
16037
|
-
return { file, stat:
|
|
16169
|
+
return { file, stat: stat20, lang, parsed: null, content, contentHash };
|
|
16038
16170
|
}
|
|
16039
16171
|
)
|
|
16040
16172
|
);
|
|
@@ -16113,7 +16245,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16113
16245
|
filesFailed++;
|
|
16114
16246
|
continue;
|
|
16115
16247
|
}
|
|
16116
|
-
const { stat:
|
|
16248
|
+
const { stat: stat20, lang, parsed } = result;
|
|
16117
16249
|
if (result.skippedMeta) {
|
|
16118
16250
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
16119
16251
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -16137,7 +16269,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16137
16269
|
store.upsertFile({
|
|
16138
16270
|
file,
|
|
16139
16271
|
lang,
|
|
16140
|
-
mtimeMs: Math.floor(
|
|
16272
|
+
mtimeMs: Math.floor(stat20.mtimeMs),
|
|
16141
16273
|
symbolCount: 0,
|
|
16142
16274
|
lastIndexed: Date.now(),
|
|
16143
16275
|
contentHash: result.contentHash ?? ""
|
|
@@ -16151,7 +16283,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16151
16283
|
store.replaceEmptyFile({
|
|
16152
16284
|
file,
|
|
16153
16285
|
lang,
|
|
16154
|
-
mtimeMs: Math.floor(
|
|
16286
|
+
mtimeMs: Math.floor(stat20.mtimeMs),
|
|
16155
16287
|
symbolCount: 0,
|
|
16156
16288
|
lastIndexed: Date.now(),
|
|
16157
16289
|
contentHash: result.contentHash ?? ""
|
|
@@ -16165,7 +16297,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16165
16297
|
lang,
|
|
16166
16298
|
symbols: parsed.symbols,
|
|
16167
16299
|
refs: parsed.refs ?? [],
|
|
16168
|
-
mtimeMs: Math.floor(
|
|
16300
|
+
mtimeMs: Math.floor(stat20.mtimeMs),
|
|
16169
16301
|
symbolCount: parsed.symbols.length,
|
|
16170
16302
|
contentHash: result.contentHash ?? ""
|
|
16171
16303
|
});
|
|
@@ -16516,7 +16648,7 @@ function callIndexOp(op, args, opts) {
|
|
|
16516
16648
|
opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
|
|
16517
16649
|
);
|
|
16518
16650
|
}
|
|
16519
|
-
return new Promise((
|
|
16651
|
+
return new Promise((resolve18, reject) => {
|
|
16520
16652
|
const id = nextRpcId++;
|
|
16521
16653
|
const timer = setTimeout(() => {
|
|
16522
16654
|
pending.delete(id);
|
|
@@ -16538,7 +16670,7 @@ function callIndexOp(op, args, opts) {
|
|
|
16538
16670
|
pending.set(id, {
|
|
16539
16671
|
resolve: (v) => {
|
|
16540
16672
|
cleanup();
|
|
16541
|
-
|
|
16673
|
+
resolve18(v);
|
|
16542
16674
|
},
|
|
16543
16675
|
reject: (e) => {
|
|
16544
16676
|
cleanup();
|
|
@@ -16806,6 +16938,160 @@ function ensureCodebaseIndexServer(options) {
|
|
|
16806
16938
|
}
|
|
16807
16939
|
|
|
16808
16940
|
// src/codebase-index/codebase-index-tool.ts
|
|
16941
|
+
import { ToolValidationError } from "@wrongstack/core/types";
|
|
16942
|
+
|
|
16943
|
+
// src/codebase-index/codebase-search-tool.ts
|
|
16944
|
+
import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
|
|
16945
|
+
var INDEXABLE_LANG_IDS = [
|
|
16946
|
+
"ts",
|
|
16947
|
+
"tsx",
|
|
16948
|
+
"js",
|
|
16949
|
+
"jsx",
|
|
16950
|
+
"go",
|
|
16951
|
+
"py",
|
|
16952
|
+
"rs",
|
|
16953
|
+
"json",
|
|
16954
|
+
"yaml"
|
|
16955
|
+
];
|
|
16956
|
+
var codebaseSearchTool = {
|
|
16957
|
+
name: "codebase-search",
|
|
16958
|
+
category: "Project",
|
|
16959
|
+
icon: "index",
|
|
16960
|
+
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).",
|
|
16961
|
+
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.",
|
|
16962
|
+
permission: "auto",
|
|
16963
|
+
mutating: false,
|
|
16964
|
+
capabilities: ["fs.read"],
|
|
16965
|
+
// The index host has its own 30s read watchdog. Leave enough headroom for
|
|
16966
|
+
// worker teardown and structured timeout reporting.
|
|
16967
|
+
timeoutMs: 35e3,
|
|
16968
|
+
inputSchema: {
|
|
16969
|
+
type: "object",
|
|
16970
|
+
properties: {
|
|
16971
|
+
query: {
|
|
16972
|
+
type: "string",
|
|
16973
|
+
description: "Search query \u2014 searches symbol names, signatures, and doc comments"
|
|
16974
|
+
},
|
|
16975
|
+
kind: {
|
|
16976
|
+
type: "string",
|
|
16977
|
+
enum: [
|
|
16978
|
+
"class",
|
|
16979
|
+
"interface",
|
|
16980
|
+
"enum",
|
|
16981
|
+
"type",
|
|
16982
|
+
"function",
|
|
16983
|
+
"method",
|
|
16984
|
+
"var",
|
|
16985
|
+
"const",
|
|
16986
|
+
"let",
|
|
16987
|
+
"property",
|
|
16988
|
+
"parameter",
|
|
16989
|
+
"namespace",
|
|
16990
|
+
"object",
|
|
16991
|
+
"literal",
|
|
16992
|
+
"schema",
|
|
16993
|
+
"struct",
|
|
16994
|
+
"trait",
|
|
16995
|
+
"impl",
|
|
16996
|
+
"static",
|
|
16997
|
+
"mod"
|
|
16998
|
+
],
|
|
16999
|
+
description: "Filter by indexed symbol kind"
|
|
17000
|
+
},
|
|
17001
|
+
lang: {
|
|
17002
|
+
type: "string",
|
|
17003
|
+
enum: [...INDEXABLE_LANG_IDS],
|
|
17004
|
+
description: "Filter by indexed language"
|
|
17005
|
+
},
|
|
17006
|
+
lspKind: {
|
|
17007
|
+
type: "integer",
|
|
17008
|
+
description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
|
|
17009
|
+
},
|
|
17010
|
+
file: {
|
|
17011
|
+
type: "string",
|
|
17012
|
+
description: "Filter to files matching this path substring"
|
|
17013
|
+
},
|
|
17014
|
+
limit: {
|
|
17015
|
+
type: "integer",
|
|
17016
|
+
description: "Maximum results to return (default 20, max 100)",
|
|
17017
|
+
minimum: 1,
|
|
17018
|
+
maximum: 100
|
|
17019
|
+
},
|
|
17020
|
+
preferLsp: {
|
|
17021
|
+
type: "boolean",
|
|
17022
|
+
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."
|
|
17023
|
+
}
|
|
17024
|
+
},
|
|
17025
|
+
required: ["query"]
|
|
17026
|
+
},
|
|
17027
|
+
async execute(input, ctx, execOpts) {
|
|
17028
|
+
const state = getIndexState();
|
|
17029
|
+
if (state.indexing && !state.ready) {
|
|
17030
|
+
return {
|
|
17031
|
+
results: [],
|
|
17032
|
+
total: 0,
|
|
17033
|
+
query: input.query,
|
|
17034
|
+
indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
|
|
17035
|
+
};
|
|
17036
|
+
}
|
|
17037
|
+
if (state.lastError) {
|
|
17038
|
+
const circuit = state.circuit;
|
|
17039
|
+
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.";
|
|
17040
|
+
return {
|
|
17041
|
+
results: [],
|
|
17042
|
+
total: 0,
|
|
17043
|
+
query: input.query,
|
|
17044
|
+
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
17045
|
+
};
|
|
17046
|
+
}
|
|
17047
|
+
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
|
|
17048
|
+
let searched;
|
|
17049
|
+
try {
|
|
17050
|
+
searched = await searchCodebaseIndex(
|
|
17051
|
+
{
|
|
17052
|
+
projectRoot: ctx.projectRoot,
|
|
17053
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
17054
|
+
query: input.query,
|
|
17055
|
+
kind: input.kind?.toLowerCase(),
|
|
17056
|
+
lang: input.lang?.toLowerCase(),
|
|
17057
|
+
file: input.file,
|
|
17058
|
+
lspKind: input.lspKind,
|
|
17059
|
+
limit
|
|
17060
|
+
},
|
|
17061
|
+
{ signal: execOpts?.signal }
|
|
17062
|
+
);
|
|
17063
|
+
} catch (err) {
|
|
17064
|
+
if (execOpts?.signal?.aborted) throw err;
|
|
17065
|
+
return {
|
|
17066
|
+
results: [],
|
|
17067
|
+
total: 0,
|
|
17068
|
+
query: input.query,
|
|
17069
|
+
indexStatus: `Index query failed: ${toErrorMessage3(err)}. Fall back to grep/glob for this lookup.`
|
|
17070
|
+
};
|
|
17071
|
+
}
|
|
17072
|
+
const { results, total } = searched;
|
|
17073
|
+
let hasPersistedIndex = state.ready || total > 0;
|
|
17074
|
+
if (!hasPersistedIndex) {
|
|
17075
|
+
try {
|
|
17076
|
+
const stats = await codebaseIndexStats(
|
|
17077
|
+
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
17078
|
+
{ signal: execOpts?.signal }
|
|
17079
|
+
);
|
|
17080
|
+
hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
|
|
17081
|
+
} catch {
|
|
17082
|
+
}
|
|
17083
|
+
}
|
|
17084
|
+
return {
|
|
17085
|
+
results,
|
|
17086
|
+
total,
|
|
17087
|
+
query: input.query,
|
|
17088
|
+
...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
|
|
17089
|
+
};
|
|
17090
|
+
}
|
|
17091
|
+
};
|
|
17092
|
+
|
|
17093
|
+
// src/codebase-index/codebase-index-tool.ts
|
|
17094
|
+
var MAX_REPORTED_ERRORS = 20;
|
|
16809
17095
|
var codebaseIndexTool = {
|
|
16810
17096
|
name: "codebase-index",
|
|
16811
17097
|
category: "Project",
|
|
@@ -16831,12 +17117,23 @@ var codebaseIndexTool = {
|
|
|
16831
17117
|
},
|
|
16832
17118
|
langs: {
|
|
16833
17119
|
type: "array",
|
|
16834
|
-
items: { type: "string" },
|
|
16835
|
-
description:
|
|
17120
|
+
items: { type: "string", enum: [...INDEXABLE_LANG_IDS] },
|
|
17121
|
+
description: `Limit reindex to specific languages: ${INDEXABLE_LANG_IDS.join(", ")}`
|
|
16836
17122
|
}
|
|
16837
17123
|
}
|
|
16838
17124
|
},
|
|
16839
17125
|
async execute(input, ctx, execOpts) {
|
|
17126
|
+
if (input.langs) {
|
|
17127
|
+
const unknown = input.langs.filter(
|
|
17128
|
+
(lang) => !INDEXABLE_LANG_IDS.includes(lang)
|
|
17129
|
+
);
|
|
17130
|
+
if (unknown.length > 0) {
|
|
17131
|
+
throw new ToolValidationError({
|
|
17132
|
+
message: `codebase-index: unknown lang(s) ${unknown.map((l) => `"${l}"`).join(", ")}. Valid ids: ${INDEXABLE_LANG_IDS.join(", ")}.`,
|
|
17133
|
+
field: "langs"
|
|
17134
|
+
});
|
|
17135
|
+
}
|
|
17136
|
+
}
|
|
16840
17137
|
if (isIndexing()) {
|
|
16841
17138
|
return {
|
|
16842
17139
|
filesIndexed: 0,
|
|
@@ -16858,23 +17155,32 @@ var codebaseIndexTool = {
|
|
|
16858
17155
|
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.`
|
|
16859
17156
|
};
|
|
16860
17157
|
}
|
|
16861
|
-
|
|
17158
|
+
const result = await runStartupIndex({
|
|
16862
17159
|
projectRoot: ctx.projectRoot,
|
|
16863
17160
|
force: input.force ?? false,
|
|
16864
17161
|
langs: input.langs,
|
|
16865
17162
|
indexDir: codebaseIndexDirOverride(ctx),
|
|
16866
17163
|
signal: execOpts?.signal
|
|
16867
17164
|
});
|
|
17165
|
+
if (result.errors.length > MAX_REPORTED_ERRORS) {
|
|
17166
|
+
const hidden = result.errors.length - MAX_REPORTED_ERRORS;
|
|
17167
|
+
return {
|
|
17168
|
+
...result,
|
|
17169
|
+
errors: [...result.errors.slice(0, MAX_REPORTED_ERRORS), `+${hidden} more`]
|
|
17170
|
+
};
|
|
17171
|
+
}
|
|
17172
|
+
return result;
|
|
16868
17173
|
}
|
|
16869
17174
|
};
|
|
16870
17175
|
|
|
16871
17176
|
// src/codebase-index/codebase-incoming-calls-tool.ts
|
|
17177
|
+
import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
|
|
16872
17178
|
var codebaseIncomingCallsTool = {
|
|
16873
17179
|
name: "codebase-incoming-calls",
|
|
16874
17180
|
category: "Project",
|
|
16875
17181
|
icon: "index",
|
|
16876
|
-
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.
|
|
16877
|
-
usageHint: 'CALL THIS BEFORE REFACTORING OR CHANGING ANY FUNCTION:\n\n-
|
|
17182
|
+
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.",
|
|
17183
|
+
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.',
|
|
16878
17184
|
permission: "auto",
|
|
16879
17185
|
mutating: false,
|
|
16880
17186
|
capabilities: ["fs.read"],
|
|
@@ -16926,16 +17232,27 @@ var codebaseIncomingCallsTool = {
|
|
|
16926
17232
|
}
|
|
16927
17233
|
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
|
|
16928
17234
|
const transitive = input.transitive === true;
|
|
16929
|
-
|
|
16930
|
-
|
|
16931
|
-
|
|
16932
|
-
|
|
17235
|
+
let serviced;
|
|
17236
|
+
try {
|
|
17237
|
+
serviced = await incomingCallsService2(
|
|
17238
|
+
{
|
|
17239
|
+
projectRoot: ctx.projectRoot,
|
|
17240
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
17241
|
+
symbol: input.symbol,
|
|
17242
|
+
file: input.file,
|
|
17243
|
+
limit,
|
|
17244
|
+
transitive
|
|
17245
|
+
}
|
|
17246
|
+
);
|
|
17247
|
+
} catch (err) {
|
|
17248
|
+
return {
|
|
16933
17249
|
symbol: input.symbol,
|
|
16934
|
-
|
|
16935
|
-
|
|
16936
|
-
|
|
16937
|
-
}
|
|
16938
|
-
|
|
17250
|
+
calls: [],
|
|
17251
|
+
total: 0,
|
|
17252
|
+
indexStatus: `Index query failed: ${toErrorMessage4(err)}. Fall back to grep for this lookup.`
|
|
17253
|
+
};
|
|
17254
|
+
}
|
|
17255
|
+
const { calls, symbolFound, ambiguous, totalMatches } = serviced;
|
|
16939
17256
|
if (!symbolFound) {
|
|
16940
17257
|
let hasPersistedIndex = state.ready;
|
|
16941
17258
|
if (!hasPersistedIndex) {
|
|
@@ -16980,12 +17297,13 @@ var codebaseIncomingCallsTool = {
|
|
|
16980
17297
|
};
|
|
16981
17298
|
|
|
16982
17299
|
// src/codebase-index/codebase-outgoing-calls-tool.ts
|
|
17300
|
+
import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
|
|
16983
17301
|
var codebaseOutgoingCallsTool = {
|
|
16984
17302
|
name: "codebase-outgoing-calls",
|
|
16985
17303
|
category: "Project",
|
|
16986
17304
|
icon: "index",
|
|
16987
17305
|
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.",
|
|
16988
|
-
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.',
|
|
17306
|
+
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.',
|
|
16989
17307
|
permission: "auto",
|
|
16990
17308
|
mutating: false,
|
|
16991
17309
|
capabilities: ["fs.read"],
|
|
@@ -17037,16 +17355,27 @@ var codebaseOutgoingCallsTool = {
|
|
|
17037
17355
|
}
|
|
17038
17356
|
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
|
|
17039
17357
|
const transitive = input.transitive === true;
|
|
17040
|
-
|
|
17041
|
-
|
|
17042
|
-
|
|
17043
|
-
|
|
17358
|
+
let serviced;
|
|
17359
|
+
try {
|
|
17360
|
+
serviced = await outgoingCallsService2(
|
|
17361
|
+
{
|
|
17362
|
+
projectRoot: ctx.projectRoot,
|
|
17363
|
+
indexDir: codebaseIndexDirOverride(ctx),
|
|
17364
|
+
symbol: input.symbol,
|
|
17365
|
+
file: input.file,
|
|
17366
|
+
limit,
|
|
17367
|
+
transitive
|
|
17368
|
+
}
|
|
17369
|
+
);
|
|
17370
|
+
} catch (err) {
|
|
17371
|
+
return {
|
|
17044
17372
|
symbol: input.symbol,
|
|
17045
|
-
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
}
|
|
17049
|
-
|
|
17373
|
+
calls: [],
|
|
17374
|
+
total: 0,
|
|
17375
|
+
indexStatus: `Index query failed: ${toErrorMessage5(err)}. Fall back to grep for this lookup.`
|
|
17376
|
+
};
|
|
17377
|
+
}
|
|
17378
|
+
const { calls, symbolFound, unresolvedCount, totalMatches } = serviced;
|
|
17050
17379
|
if (!symbolFound) {
|
|
17051
17380
|
let hasPersistedIndex = state.ready;
|
|
17052
17381
|
if (!hasPersistedIndex) {
|
|
@@ -17090,132 +17419,6 @@ var codebaseOutgoingCallsTool = {
|
|
|
17090
17419
|
}
|
|
17091
17420
|
};
|
|
17092
17421
|
|
|
17093
|
-
// src/codebase-index/codebase-search-tool.ts
|
|
17094
|
-
var codebaseSearchTool = {
|
|
17095
|
-
name: "codebase-search",
|
|
17096
|
-
category: "Project",
|
|
17097
|
-
icon: "index",
|
|
17098
|
-
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).",
|
|
17099
|
-
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.",
|
|
17100
|
-
permission: "auto",
|
|
17101
|
-
mutating: false,
|
|
17102
|
-
capabilities: ["fs.read"],
|
|
17103
|
-
// The index host has its own 30s read watchdog. Leave enough headroom for
|
|
17104
|
-
// worker teardown and structured timeout reporting.
|
|
17105
|
-
timeoutMs: 35e3,
|
|
17106
|
-
inputSchema: {
|
|
17107
|
-
type: "object",
|
|
17108
|
-
properties: {
|
|
17109
|
-
query: {
|
|
17110
|
-
type: "string",
|
|
17111
|
-
description: "Search query \u2014 searches symbol names, signatures, and doc comments"
|
|
17112
|
-
},
|
|
17113
|
-
kind: {
|
|
17114
|
-
type: "string",
|
|
17115
|
-
enum: [
|
|
17116
|
-
"class",
|
|
17117
|
-
"interface",
|
|
17118
|
-
"enum",
|
|
17119
|
-
"type",
|
|
17120
|
-
"function",
|
|
17121
|
-
"method",
|
|
17122
|
-
"var",
|
|
17123
|
-
"const",
|
|
17124
|
-
"let",
|
|
17125
|
-
"property",
|
|
17126
|
-
"parameter",
|
|
17127
|
-
"namespace",
|
|
17128
|
-
"object",
|
|
17129
|
-
"literal",
|
|
17130
|
-
"schema",
|
|
17131
|
-
"struct",
|
|
17132
|
-
"trait",
|
|
17133
|
-
"impl",
|
|
17134
|
-
"static",
|
|
17135
|
-
"mod"
|
|
17136
|
-
],
|
|
17137
|
-
description: "Filter by indexed symbol kind"
|
|
17138
|
-
},
|
|
17139
|
-
lang: {
|
|
17140
|
-
type: "string",
|
|
17141
|
-
enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
|
|
17142
|
-
description: "Filter by indexed language"
|
|
17143
|
-
},
|
|
17144
|
-
lspKind: {
|
|
17145
|
-
type: "integer",
|
|
17146
|
-
description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
|
|
17147
|
-
},
|
|
17148
|
-
file: {
|
|
17149
|
-
type: "string",
|
|
17150
|
-
description: "Filter to files matching this path substring"
|
|
17151
|
-
},
|
|
17152
|
-
limit: {
|
|
17153
|
-
type: "integer",
|
|
17154
|
-
description: "Maximum results to return (default 20, max 100)",
|
|
17155
|
-
minimum: 1,
|
|
17156
|
-
maximum: 100
|
|
17157
|
-
},
|
|
17158
|
-
preferLsp: {
|
|
17159
|
-
type: "boolean",
|
|
17160
|
-
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."
|
|
17161
|
-
}
|
|
17162
|
-
},
|
|
17163
|
-
required: ["query"]
|
|
17164
|
-
},
|
|
17165
|
-
async execute(input, ctx, execOpts) {
|
|
17166
|
-
const state = getIndexState();
|
|
17167
|
-
if (state.indexing && !state.ready) {
|
|
17168
|
-
return {
|
|
17169
|
-
results: [],
|
|
17170
|
-
total: 0,
|
|
17171
|
-
query: input.query,
|
|
17172
|
-
indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
|
|
17173
|
-
};
|
|
17174
|
-
}
|
|
17175
|
-
if (state.lastError) {
|
|
17176
|
-
const circuit = state.circuit;
|
|
17177
|
-
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.";
|
|
17178
|
-
return {
|
|
17179
|
-
results: [],
|
|
17180
|
-
total: 0,
|
|
17181
|
-
query: input.query,
|
|
17182
|
-
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
17183
|
-
};
|
|
17184
|
-
}
|
|
17185
|
-
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
|
|
17186
|
-
const { results, total } = await searchCodebaseIndex(
|
|
17187
|
-
{
|
|
17188
|
-
projectRoot: ctx.projectRoot,
|
|
17189
|
-
indexDir: codebaseIndexDirOverride(ctx),
|
|
17190
|
-
query: input.query,
|
|
17191
|
-
kind: input.kind?.toLowerCase(),
|
|
17192
|
-
lang: input.lang?.toLowerCase(),
|
|
17193
|
-
file: input.file,
|
|
17194
|
-
lspKind: input.lspKind,
|
|
17195
|
-
limit
|
|
17196
|
-
},
|
|
17197
|
-
{ signal: execOpts?.signal }
|
|
17198
|
-
);
|
|
17199
|
-
let hasPersistedIndex = state.ready || total > 0;
|
|
17200
|
-
if (!hasPersistedIndex) {
|
|
17201
|
-
try {
|
|
17202
|
-
const stats = await codebaseIndexStats(
|
|
17203
|
-
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
17204
|
-
{ signal: execOpts?.signal }
|
|
17205
|
-
);
|
|
17206
|
-
hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
|
|
17207
|
-
} catch {
|
|
17208
|
-
}
|
|
17209
|
-
}
|
|
17210
|
-
return {
|
|
17211
|
-
results,
|
|
17212
|
-
total,
|
|
17213
|
-
query: input.query,
|
|
17214
|
-
...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
|
|
17215
|
-
};
|
|
17216
|
-
}
|
|
17217
|
-
};
|
|
17218
|
-
|
|
17219
17422
|
// src/codebase-index/codebase-stats-tool.ts
|
|
17220
17423
|
var codebaseStatsTool = {
|
|
17221
17424
|
name: "codebase-stats",
|
|
@@ -17309,9 +17512,9 @@ import * as path25 from "node:path";
|
|
|
17309
17512
|
var deadCodeScanTool = {
|
|
17310
17513
|
name: "dead-code-scan",
|
|
17311
17514
|
category: "Project",
|
|
17312
|
-
icon: "
|
|
17515
|
+
icon: "index",
|
|
17313
17516
|
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).",
|
|
17314
|
-
usageHint:
|
|
17517
|
+
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).",
|
|
17315
17518
|
permission: "auto",
|
|
17316
17519
|
mutating: false,
|
|
17317
17520
|
capabilities: ["fs.read"],
|
|
@@ -18074,12 +18277,14 @@ import { statSync as statSync3 } from "node:fs";
|
|
|
18074
18277
|
import * as fs22 from "node:fs/promises";
|
|
18075
18278
|
import * as path27 from "node:path";
|
|
18076
18279
|
import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
|
|
18280
|
+
import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
|
|
18077
18281
|
var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
|
|
18282
|
+
var MAX_GIT_DIFF_CHARS = 1e5;
|
|
18078
18283
|
var diffTool = {
|
|
18079
18284
|
name: "diff",
|
|
18080
18285
|
category: "Filesystem",
|
|
18081
18286
|
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.",
|
|
18082
|
-
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`
|
|
18287
|
+
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).',
|
|
18083
18288
|
permission: "auto",
|
|
18084
18289
|
mutating: false,
|
|
18085
18290
|
maxOutputBytes: 262144,
|
|
@@ -18112,11 +18317,12 @@ var diffTool = {
|
|
|
18112
18317
|
mode: {
|
|
18113
18318
|
type: "string",
|
|
18114
18319
|
enum: ["unified", "side-by-side", "stat"],
|
|
18115
|
-
description: 'Output format. "unified" is default
|
|
18320
|
+
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.'
|
|
18116
18321
|
},
|
|
18117
18322
|
context: {
|
|
18118
18323
|
type: "integer",
|
|
18119
|
-
|
|
18324
|
+
minimum: 0,
|
|
18325
|
+
description: "Number of context lines for git unified diffs (default: 3, passed as -U<n>). Ignored by the `files`-only dump path."
|
|
18120
18326
|
}
|
|
18121
18327
|
}
|
|
18122
18328
|
},
|
|
@@ -18129,16 +18335,31 @@ var diffTool = {
|
|
|
18129
18335
|
};
|
|
18130
18336
|
async function gitDiff(input, ctx, signal) {
|
|
18131
18337
|
if (input.a?.startsWith("-")) {
|
|
18132
|
-
throw new
|
|
18338
|
+
throw new ToolValidationError2({
|
|
18339
|
+
message: `diff: unsafe ref "${input.a}" \u2014 refs may not begin with '-' (flag injection)`,
|
|
18340
|
+
field: "a"
|
|
18341
|
+
});
|
|
18133
18342
|
}
|
|
18134
18343
|
if (input.b?.startsWith("-")) {
|
|
18135
|
-
throw new
|
|
18344
|
+
throw new ToolValidationError2({
|
|
18345
|
+
message: `diff: unsafe ref "${input.b}" \u2014 refs may not begin with '-' (flag injection)`,
|
|
18346
|
+
field: "b"
|
|
18347
|
+
});
|
|
18136
18348
|
}
|
|
18349
|
+
const requestedMode = input.mode ?? "unified";
|
|
18350
|
+
const statMode = requestedMode === "stat";
|
|
18351
|
+
const effectiveMode = statMode ? "stat" : "unified";
|
|
18352
|
+
const sideBySideNote = requestedMode === "side-by-side" ? "side-by-side output is not supported; a unified diff was produced instead." : void 0;
|
|
18137
18353
|
const gitDir = findGitDir(ctx.cwd);
|
|
18138
18354
|
if (!gitDir) {
|
|
18139
|
-
return { diff: "", files: [], truncated: false, mode:
|
|
18355
|
+
return { diff: "", files: [], truncated: false, mode: effectiveMode };
|
|
18140
18356
|
}
|
|
18141
18357
|
const args = ["diff", "--no-color"];
|
|
18358
|
+
if (statMode) args.push("--stat");
|
|
18359
|
+
if (!statMode && input.context !== void 0) {
|
|
18360
|
+
const contextLines = Math.max(0, Math.floor(input.context));
|
|
18361
|
+
if (Number.isFinite(contextLines)) args.push(`-U${contextLines}`);
|
|
18362
|
+
}
|
|
18142
18363
|
if (input.staged) args.push("--staged");
|
|
18143
18364
|
if (input.a) args.push(input.a);
|
|
18144
18365
|
if (input.b) args.push(input.b);
|
|
@@ -18147,19 +18368,30 @@ async function gitDiff(input, ctx, signal) {
|
|
|
18147
18368
|
args.push("--", ...files.map((f) => f.trim()));
|
|
18148
18369
|
}
|
|
18149
18370
|
const result = await runGit(args, gitDir, signal);
|
|
18371
|
+
let diff = result.stdout;
|
|
18372
|
+
let truncated = false;
|
|
18373
|
+
if (diff.length > MAX_GIT_DIFF_CHARS) {
|
|
18374
|
+
let clipped = diff.slice(0, MAX_GIT_DIFF_CHARS);
|
|
18375
|
+
const nl = clipped.lastIndexOf("\n");
|
|
18376
|
+
if (nl > 0) clipped = clipped.slice(0, nl);
|
|
18377
|
+
diff = `${clipped}
|
|
18378
|
+
\u2026[git diff truncated: ${result.stdout.length - clipped.length} of ${result.stdout.length} characters omitted]`;
|
|
18379
|
+
truncated = true;
|
|
18380
|
+
}
|
|
18150
18381
|
return {
|
|
18151
|
-
diff
|
|
18382
|
+
diff,
|
|
18152
18383
|
files: [],
|
|
18153
|
-
truncated
|
|
18154
|
-
mode:
|
|
18384
|
+
truncated,
|
|
18385
|
+
mode: effectiveMode,
|
|
18386
|
+
note: sideBySideNote
|
|
18155
18387
|
};
|
|
18156
18388
|
}
|
|
18157
18389
|
function findGitDir(cwd) {
|
|
18158
18390
|
let dir = cwd;
|
|
18159
18391
|
for (let i = 0; i < 20; i++) {
|
|
18160
18392
|
try {
|
|
18161
|
-
const
|
|
18162
|
-
if (
|
|
18393
|
+
const stat20 = statSync3(path27.join(dir, ".git"));
|
|
18394
|
+
if (stat20.isDirectory()) return dir;
|
|
18163
18395
|
} catch {
|
|
18164
18396
|
}
|
|
18165
18397
|
const parent = path27.dirname(dir);
|
|
@@ -18169,7 +18401,7 @@ function findGitDir(cwd) {
|
|
|
18169
18401
|
return null;
|
|
18170
18402
|
}
|
|
18171
18403
|
function runGit(args, cwd, signal) {
|
|
18172
|
-
return new Promise((
|
|
18404
|
+
return new Promise((resolve18) => {
|
|
18173
18405
|
let stdout = "";
|
|
18174
18406
|
let stderr = "";
|
|
18175
18407
|
const child = spawn7("git", args, {
|
|
@@ -18185,8 +18417,8 @@ function runGit(args, cwd, signal) {
|
|
|
18185
18417
|
child.stderr?.on("data", (c) => {
|
|
18186
18418
|
stderr += c.toString();
|
|
18187
18419
|
});
|
|
18188
|
-
child.on("close", (code) =>
|
|
18189
|
-
child.on("error", (e) =>
|
|
18420
|
+
child.on("close", (code) => resolve18({ stdout, stderr, exitCode: code ?? 0 }));
|
|
18421
|
+
child.on("error", (e) => resolve18({ stdout: "", stderr: e.message, exitCode: 1 }));
|
|
18190
18422
|
});
|
|
18191
18423
|
}
|
|
18192
18424
|
async function fileDiff(input, ctx, _signal) {
|
|
@@ -18197,19 +18429,19 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
18197
18429
|
diff: "No files specified",
|
|
18198
18430
|
files: [],
|
|
18199
18431
|
truncated: false,
|
|
18200
|
-
mode:
|
|
18432
|
+
mode: "dump"
|
|
18201
18433
|
};
|
|
18202
18434
|
}
|
|
18203
18435
|
const results = [];
|
|
18204
18436
|
let truncated = false;
|
|
18205
18437
|
for (const file of files) {
|
|
18206
|
-
const absPath =
|
|
18207
|
-
const
|
|
18208
|
-
if (!
|
|
18209
|
-
if (
|
|
18438
|
+
const absPath = await safeResolveReal(file, ctx);
|
|
18439
|
+
const stat20 = await fs22.stat(absPath).catch(() => null);
|
|
18440
|
+
if (!stat20?.isFile()) continue;
|
|
18441
|
+
if (stat20.size > MAX_FILE_DUMP_BYTES) {
|
|
18210
18442
|
truncated = true;
|
|
18211
18443
|
results.push(
|
|
18212
|
-
`--- ${file} (skipped: ${
|
|
18444
|
+
`--- ${file} (skipped: ${stat20.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
|
|
18213
18445
|
);
|
|
18214
18446
|
continue;
|
|
18215
18447
|
}
|
|
@@ -18221,7 +18453,10 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
18221
18453
|
diff: results.join("\n\n"),
|
|
18222
18454
|
files,
|
|
18223
18455
|
truncated,
|
|
18224
|
-
mode:
|
|
18456
|
+
// Honest mode: this path always produces a line-numbered dump — it never
|
|
18457
|
+
// honors `mode`, so it must not echo the requested value back.
|
|
18458
|
+
mode: "dump",
|
|
18459
|
+
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
|
|
18225
18460
|
};
|
|
18226
18461
|
}
|
|
18227
18462
|
function formatWithLineNumbers(file, lines) {
|
|
@@ -18234,11 +18469,12 @@ ${numbered}`;
|
|
|
18234
18469
|
// src/document.ts
|
|
18235
18470
|
init_util();
|
|
18236
18471
|
import * as fs23 from "node:fs/promises";
|
|
18472
|
+
import * as path28 from "node:path";
|
|
18237
18473
|
var documentTool = {
|
|
18238
18474
|
name: "document",
|
|
18239
18475
|
category: "Project",
|
|
18240
|
-
description: "DEPRECATED \u2014
|
|
18241
|
-
usageHint: "Deprecated:
|
|
18476
|
+
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.",
|
|
18477
|
+
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).",
|
|
18242
18478
|
permission: "auto",
|
|
18243
18479
|
mutating: false,
|
|
18244
18480
|
timeoutMs: 3e4,
|
|
@@ -18274,7 +18510,11 @@ var documentTool = {
|
|
|
18274
18510
|
const results = [];
|
|
18275
18511
|
let filesProcessed = 0;
|
|
18276
18512
|
let itemsDocumented = 0;
|
|
18277
|
-
const fileList = input.files ? await resolveFiles(
|
|
18513
|
+
const fileList = input.files ? await resolveFiles(
|
|
18514
|
+
Array.isArray(input.files) ? input.files.join(",") : input.files,
|
|
18515
|
+
cwd,
|
|
18516
|
+
ctx
|
|
18517
|
+
) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
18278
18518
|
for (const absPath of fileList) {
|
|
18279
18519
|
try {
|
|
18280
18520
|
const content = await fs23.readFile(absPath, "utf8");
|
|
@@ -18307,14 +18547,21 @@ var documentTool = {
|
|
|
18307
18547
|
};
|
|
18308
18548
|
}
|
|
18309
18549
|
};
|
|
18310
|
-
async function resolveFiles(filesInput, cwd) {
|
|
18311
|
-
const files =
|
|
18550
|
+
async function resolveFiles(filesInput, cwd, ctx) {
|
|
18551
|
+
const files = filesInput.split(",");
|
|
18312
18552
|
const resolved = [];
|
|
18313
18553
|
for (const f of files) {
|
|
18314
|
-
const
|
|
18554
|
+
const entry = f.trim();
|
|
18555
|
+
if (!entry) continue;
|
|
18556
|
+
let absPath;
|
|
18557
|
+
try {
|
|
18558
|
+
absPath = ensureInsideRoot(path28.resolve(cwd, entry), ctx);
|
|
18559
|
+
} catch {
|
|
18560
|
+
continue;
|
|
18561
|
+
}
|
|
18315
18562
|
try {
|
|
18316
|
-
const
|
|
18317
|
-
if (
|
|
18563
|
+
const stat20 = await fs23.stat(absPath);
|
|
18564
|
+
if (stat20.isFile()) resolved.push(absPath);
|
|
18318
18565
|
} catch {
|
|
18319
18566
|
}
|
|
18320
18567
|
}
|
|
@@ -18384,7 +18631,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
|
|
|
18384
18631
|
// src/e2e.ts
|
|
18385
18632
|
init_util();
|
|
18386
18633
|
import { open, readdir as readdir7 } from "node:fs/promises";
|
|
18387
|
-
import * as
|
|
18634
|
+
import * as path29 from "node:path";
|
|
18388
18635
|
async function readBoundedText(filePath, maxBytes) {
|
|
18389
18636
|
let handle;
|
|
18390
18637
|
try {
|
|
@@ -18434,8 +18681,8 @@ var MAX_PACKAGE_BYTES = 512 * 1024;
|
|
|
18434
18681
|
var MAX_CONFIG_BYTES = 512 * 1024;
|
|
18435
18682
|
var MAX_SPEC_SAMPLES = 20;
|
|
18436
18683
|
function relativePath(root, target) {
|
|
18437
|
-
const value =
|
|
18438
|
-
return value.split(
|
|
18684
|
+
const value = path29.relative(root, target) || ".";
|
|
18685
|
+
return value.split(path29.sep).join("/");
|
|
18439
18686
|
}
|
|
18440
18687
|
function escapeRegExp(value) {
|
|
18441
18688
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -18507,7 +18754,7 @@ async function scanWorkspace(root, maxDepth, signal) {
|
|
|
18507
18754
|
}
|
|
18508
18755
|
for (const entry of entries) {
|
|
18509
18756
|
signal.throwIfAborted();
|
|
18510
|
-
const absolutePath =
|
|
18757
|
+
const absolutePath = path29.join(current.directory, entry.name);
|
|
18511
18758
|
if (entry.isFile()) {
|
|
18512
18759
|
if (entry.name === "package.json") result.packageFiles.push(absolutePath);
|
|
18513
18760
|
const framework = CONFIG_NAMES.get(entry.name);
|
|
@@ -18568,9 +18815,9 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
|
|
|
18568
18815
|
if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
|
|
18569
18816
|
if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
|
|
18570
18817
|
if (directory === scanRoot) break;
|
|
18571
|
-
const parent =
|
|
18572
|
-
const relativeParent =
|
|
18573
|
-
if (parent === directory || relativeParent.startsWith("..") ||
|
|
18818
|
+
const parent = path29.dirname(directory);
|
|
18819
|
+
const relativeParent = path29.relative(scanRoot, parent);
|
|
18820
|
+
if (parent === directory || relativeParent.startsWith("..") || path29.isAbsolute(relativeParent)) {
|
|
18574
18821
|
break;
|
|
18575
18822
|
}
|
|
18576
18823
|
directory = parent;
|
|
@@ -18615,7 +18862,7 @@ function isSpec(framework, filename) {
|
|
|
18615
18862
|
return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
|
|
18616
18863
|
}
|
|
18617
18864
|
async function collectSpecs(root, framework, testDirectory, signal) {
|
|
18618
|
-
const roots = testDirectory ? [
|
|
18865
|
+
const roots = testDirectory ? [path29.resolve(root, testDirectory)] : framework === "cypress" ? [path29.join(root, "cypress", "e2e"), path29.join(root, "cypress", "integration")] : [path29.join(root, "tests")];
|
|
18619
18866
|
const samples = [];
|
|
18620
18867
|
let count = 0;
|
|
18621
18868
|
let scanned = 0;
|
|
@@ -18634,7 +18881,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
|
|
|
18634
18881
|
}
|
|
18635
18882
|
for (const entry of entries) {
|
|
18636
18883
|
signal.throwIfAborted();
|
|
18637
|
-
const target =
|
|
18884
|
+
const target = path29.join(directory, entry.name);
|
|
18638
18885
|
if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
|
|
18639
18886
|
else if (entry.isFile() && isSpec(framework, entry.name)) {
|
|
18640
18887
|
count += 1;
|
|
@@ -18654,7 +18901,7 @@ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
|
|
|
18654
18901
|
const found = packagesByDirectory.get(directory);
|
|
18655
18902
|
if (found) return found;
|
|
18656
18903
|
if (directory === scanRoot) return void 0;
|
|
18657
|
-
const parent =
|
|
18904
|
+
const parent = path29.dirname(directory);
|
|
18658
18905
|
if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
|
|
18659
18906
|
directory = parent;
|
|
18660
18907
|
}
|
|
@@ -18665,13 +18912,13 @@ async function discoverE2EProjects(root, options) {
|
|
|
18665
18912
|
const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
|
|
18666
18913
|
(info) => Boolean(info)
|
|
18667
18914
|
);
|
|
18668
|
-
const packagesByDirectory = new Map(packages.map((info) => [
|
|
18915
|
+
const packagesByDirectory = new Map(packages.map((info) => [path29.dirname(info.path), info]));
|
|
18669
18916
|
const candidates = /* @__PURE__ */ new Map();
|
|
18670
18917
|
for (const config of scan.configs) {
|
|
18671
18918
|
if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
|
|
18672
18919
|
continue;
|
|
18673
18920
|
}
|
|
18674
|
-
const projectRoot =
|
|
18921
|
+
const projectRoot = path29.dirname(config.absolutePath);
|
|
18675
18922
|
candidates.set(`${config.framework}:${projectRoot}`, {
|
|
18676
18923
|
framework: config.framework,
|
|
18677
18924
|
root: projectRoot,
|
|
@@ -18679,7 +18926,7 @@ async function discoverE2EProjects(root, options) {
|
|
|
18679
18926
|
});
|
|
18680
18927
|
}
|
|
18681
18928
|
for (const info of packages) {
|
|
18682
|
-
const projectRoot =
|
|
18929
|
+
const projectRoot = path29.dirname(info.path);
|
|
18683
18930
|
for (const framework of frameworkFromPackage(info)) {
|
|
18684
18931
|
if (options.framework && options.framework !== "all" && framework !== options.framework)
|
|
18685
18932
|
continue;
|
|
@@ -18695,10 +18942,10 @@ async function discoverE2EProjects(root, options) {
|
|
|
18695
18942
|
const scripts = matchingScripts(info, candidate.framework);
|
|
18696
18943
|
const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
|
|
18697
18944
|
const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
|
|
18698
|
-
const resolvedTestDirectory = testDirectory ?
|
|
18699
|
-
const relativeTestDirectory = resolvedTestDirectory ?
|
|
18945
|
+
const resolvedTestDirectory = testDirectory ? path29.resolve(candidate.root, testDirectory) : void 0;
|
|
18946
|
+
const relativeTestDirectory = resolvedTestDirectory ? path29.relative(root, resolvedTestDirectory) : void 0;
|
|
18700
18947
|
const unsafeTestDirectory = Boolean(
|
|
18701
|
-
relativeTestDirectory && (relativeTestDirectory.startsWith("..") ||
|
|
18948
|
+
relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path29.isAbsolute(relativeTestDirectory))
|
|
18702
18949
|
);
|
|
18703
18950
|
const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
|
|
18704
18951
|
const warnings = [];
|
|
@@ -18806,7 +19053,7 @@ import {
|
|
|
18806
19053
|
toStyle,
|
|
18807
19054
|
unifiedDiff
|
|
18808
19055
|
} from "@wrongstack/core/utils";
|
|
18809
|
-
import { ToolValidationError } from "@wrongstack/core/types";
|
|
19056
|
+
import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
|
|
18810
19057
|
|
|
18811
19058
|
// src/_edit-match.ts
|
|
18812
19059
|
var TIER_LABEL = {
|
|
@@ -19034,7 +19281,7 @@ function prefixSimilarity(a, b) {
|
|
|
19034
19281
|
}
|
|
19035
19282
|
|
|
19036
19283
|
// src/_syntax-check.ts
|
|
19037
|
-
import * as
|
|
19284
|
+
import * as path30 from "node:path";
|
|
19038
19285
|
var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
19039
19286
|
var MAX_CHECK_CHARS = 15e5;
|
|
19040
19287
|
var MAX_ERRORS = 5;
|
|
@@ -19058,15 +19305,15 @@ async function checkSyntax(filePath, content, previousContent) {
|
|
|
19058
19305
|
return { errors, preExisting };
|
|
19059
19306
|
}
|
|
19060
19307
|
function isJsoncFile(filePath) {
|
|
19061
|
-
const base =
|
|
19308
|
+
const base = path30.basename(filePath).toLowerCase();
|
|
19062
19309
|
if (base.endsWith(".jsonc")) return true;
|
|
19063
19310
|
if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
|
|
19064
|
-
const dir =
|
|
19311
|
+
const dir = path30.basename(path30.dirname(filePath)).toLowerCase();
|
|
19065
19312
|
return dir === ".vscode";
|
|
19066
19313
|
}
|
|
19067
19314
|
async function parseErrors(filePath, content) {
|
|
19068
19315
|
if (content.length > MAX_CHECK_CHARS) return void 0;
|
|
19069
|
-
const ext =
|
|
19316
|
+
const ext = path30.extname(filePath).toLowerCase();
|
|
19070
19317
|
if (ext === ".json" || ext === ".jsonc") {
|
|
19071
19318
|
try {
|
|
19072
19319
|
JSON.parse(content);
|
|
@@ -19093,7 +19340,7 @@ async function parseErrors(filePath, content) {
|
|
|
19093
19340
|
ts2.ScriptKind.JSX
|
|
19094
19341
|
);
|
|
19095
19342
|
const sourceFile = ts2.createSourceFile(
|
|
19096
|
-
|
|
19343
|
+
path30.basename(filePath),
|
|
19097
19344
|
content,
|
|
19098
19345
|
ts2.ScriptTarget.Latest,
|
|
19099
19346
|
/* setParentNodes */
|
|
@@ -19120,6 +19367,7 @@ function formatDiag(ts2, diag, content, sourceFile) {
|
|
|
19120
19367
|
|
|
19121
19368
|
// src/edit.ts
|
|
19122
19369
|
init_util();
|
|
19370
|
+
var MAX_DIFF_BYTES = 262144;
|
|
19123
19371
|
var editTool = {
|
|
19124
19372
|
name: "edit",
|
|
19125
19373
|
category: "Filesystem",
|
|
@@ -19130,46 +19378,62 @@ var editTool = {
|
|
|
19130
19378
|
useInstead: ["write", "patch"]
|
|
19131
19379
|
},
|
|
19132
19380
|
permission: "confirm",
|
|
19381
|
+
// WS-046: gives permission decisions something to key on — the file being
|
|
19382
|
+
// edited, so trust rules can scope by path.
|
|
19383
|
+
subjectKey: "path",
|
|
19133
19384
|
mutating: true,
|
|
19134
19385
|
capabilities: ["fs.write"],
|
|
19135
19386
|
icon: "edit",
|
|
19136
19387
|
timeoutMs: 5e3,
|
|
19388
|
+
maxOutputBytes: 262144,
|
|
19137
19389
|
inputSchema: {
|
|
19138
19390
|
type: "object",
|
|
19139
19391
|
properties: {
|
|
19140
|
-
path: {
|
|
19141
|
-
|
|
19142
|
-
|
|
19143
|
-
|
|
19392
|
+
path: {
|
|
19393
|
+
type: "string",
|
|
19394
|
+
description: "Path to the file to edit \u2014 relative to the project root, or absolute inside it."
|
|
19395
|
+
},
|
|
19396
|
+
old_string: {
|
|
19397
|
+
type: "string",
|
|
19398
|
+
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."
|
|
19399
|
+
},
|
|
19400
|
+
new_string: {
|
|
19401
|
+
type: "string",
|
|
19402
|
+
description: "The exact replacement text (may be empty to delete `old_string`)."
|
|
19403
|
+
},
|
|
19404
|
+
replace_all: {
|
|
19405
|
+
type: "boolean",
|
|
19406
|
+
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."
|
|
19407
|
+
}
|
|
19144
19408
|
},
|
|
19145
19409
|
required: ["path", "old_string", "new_string"]
|
|
19146
19410
|
},
|
|
19147
19411
|
async execute(input, ctx, opts) {
|
|
19148
19412
|
if (!input?.path) {
|
|
19149
|
-
throw new
|
|
19413
|
+
throw new ToolValidationError3({ message: "edit: path is required", field: "path" });
|
|
19150
19414
|
}
|
|
19151
19415
|
if (input.old_string === void 0) {
|
|
19152
|
-
throw new
|
|
19416
|
+
throw new ToolValidationError3({
|
|
19153
19417
|
message: "edit: old_string is required",
|
|
19154
19418
|
field: "old_string"
|
|
19155
19419
|
});
|
|
19156
19420
|
}
|
|
19157
19421
|
if (input.new_string === void 0) {
|
|
19158
|
-
throw new
|
|
19422
|
+
throw new ToolValidationError3({
|
|
19159
19423
|
message: "edit: new_string is required",
|
|
19160
19424
|
field: "new_string"
|
|
19161
19425
|
});
|
|
19162
19426
|
}
|
|
19163
19427
|
if (input.old_string === "") {
|
|
19164
|
-
throw new
|
|
19428
|
+
throw new ToolValidationError3({
|
|
19165
19429
|
message: "edit: old_string cannot be empty",
|
|
19166
19430
|
field: "old_string"
|
|
19167
19431
|
});
|
|
19168
19432
|
}
|
|
19169
19433
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
19170
|
-
const
|
|
19434
|
+
const stat20 = await fs24.stat(absPath).catch((err) => {
|
|
19171
19435
|
if (err.code === "ENOENT") {
|
|
19172
|
-
throw new
|
|
19436
|
+
throw new ToolValidationError3({
|
|
19173
19437
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
19174
19438
|
field: "path",
|
|
19175
19439
|
context: { exists: false }
|
|
@@ -19177,8 +19441,8 @@ var editTool = {
|
|
|
19177
19441
|
}
|
|
19178
19442
|
throw err;
|
|
19179
19443
|
});
|
|
19180
|
-
if (!
|
|
19181
|
-
throw new
|
|
19444
|
+
if (!stat20.isFile()) {
|
|
19445
|
+
throw new ToolValidationError3({
|
|
19182
19446
|
message: `edit: "${input.path}" is not a regular file`,
|
|
19183
19447
|
field: "path"
|
|
19184
19448
|
});
|
|
@@ -19191,7 +19455,7 @@ var editTool = {
|
|
|
19191
19455
|
const lastReadHash = ctx.lastReadHash?.(absPath);
|
|
19192
19456
|
if (lastReadHash !== void 0) {
|
|
19193
19457
|
if (lastReadHash !== originalHash) {
|
|
19194
|
-
throw new
|
|
19458
|
+
throw new ToolValidationError3({
|
|
19195
19459
|
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
19196
19460
|
field: "path",
|
|
19197
19461
|
context: { reason: "external_modification" }
|
|
@@ -19200,15 +19464,15 @@ var editTool = {
|
|
|
19200
19464
|
} else {
|
|
19201
19465
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
19202
19466
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
19203
|
-
throw new
|
|
19467
|
+
throw new ToolValidationError3({
|
|
19204
19468
|
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
19205
19469
|
field: "path",
|
|
19206
19470
|
context: { reason: "external_modification" }
|
|
19207
19471
|
});
|
|
19208
19472
|
}
|
|
19209
19473
|
}
|
|
19210
|
-
if (autoRead && updated.mtimeMs >
|
|
19211
|
-
throw new
|
|
19474
|
+
if (autoRead && updated.mtimeMs > stat20.mtimeMs + mtimeTolerance) {
|
|
19475
|
+
throw new ToolValidationError3({
|
|
19212
19476
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
19213
19477
|
field: "path",
|
|
19214
19478
|
context: { reason: "auto_read_race" }
|
|
@@ -19220,6 +19484,9 @@ var editTool = {
|
|
|
19220
19484
|
const oldLf = normalizeToLf(input.old_string);
|
|
19221
19485
|
const newLf = normalizeToLf(input.new_string);
|
|
19222
19486
|
if (oldLf === newLf) {
|
|
19487
|
+
if (!fileLf.includes(oldLf)) {
|
|
19488
|
+
throw noMatchError(input.path, fileLf, oldLf);
|
|
19489
|
+
}
|
|
19223
19490
|
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
|
|
19224
19491
|
return {
|
|
19225
19492
|
path: absPath,
|
|
@@ -19233,26 +19500,20 @@ var editTool = {
|
|
|
19233
19500
|
const ladder = findLadderMatches(fileLf, oldLf);
|
|
19234
19501
|
if (!ladder) {
|
|
19235
19502
|
opts?.signal?.throwIfAborted();
|
|
19236
|
-
|
|
19237
|
-
throw new ToolValidationError({
|
|
19238
|
-
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
|
|
19239
|
-
${hint.snippet}
|
|
19240
|
-
Compare this against your old_string and retry with the file's actual text.` : ""}`,
|
|
19241
|
-
field: "old_string"
|
|
19242
|
-
});
|
|
19503
|
+
throw noMatchError(input.path, fileLf, oldLf);
|
|
19243
19504
|
}
|
|
19244
19505
|
const { tier, matches } = ladder;
|
|
19245
19506
|
const count = matches.length;
|
|
19246
19507
|
if (ladder.ambiguous) {
|
|
19247
19508
|
const lines = matches.map((m) => m.startLine);
|
|
19248
|
-
throw new
|
|
19509
|
+
throw new ToolValidationError3({
|
|
19249
19510
|
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.`,
|
|
19250
19511
|
field: "old_string",
|
|
19251
19512
|
context: { occurrences: count, matchTier: tier }
|
|
19252
19513
|
});
|
|
19253
19514
|
}
|
|
19254
19515
|
if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
|
|
19255
|
-
throw new
|
|
19516
|
+
throw new ToolValidationError3({
|
|
19256
19517
|
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.`,
|
|
19257
19518
|
field: "old_string",
|
|
19258
19519
|
context: { matchTier: tier }
|
|
@@ -19260,7 +19521,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
19260
19521
|
}
|
|
19261
19522
|
if (count > 1 && !input.replace_all) {
|
|
19262
19523
|
const lines = matches.map((m) => m.startLine);
|
|
19263
|
-
throw new
|
|
19524
|
+
throw new ToolValidationError3({
|
|
19264
19525
|
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.`,
|
|
19265
19526
|
field: "old_string",
|
|
19266
19527
|
context: { occurrences: count, matchTier: tier }
|
|
@@ -19301,16 +19562,20 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
19301
19562
|
after: newFile
|
|
19302
19563
|
});
|
|
19303
19564
|
opts?.signal?.throwIfAborted();
|
|
19304
|
-
const diff =
|
|
19305
|
-
|
|
19306
|
-
|
|
19307
|
-
|
|
19565
|
+
const { text: diff, truncated: diffTruncated } = truncateDiffPayload(
|
|
19566
|
+
unifiedDiff(original, newFile, {
|
|
19567
|
+
fromFile: input.path,
|
|
19568
|
+
toFile: input.path
|
|
19569
|
+
}),
|
|
19570
|
+
MAX_DIFF_BYTES
|
|
19571
|
+
);
|
|
19572
|
+
const diffNote = diffTruncated ? "Diff truncated to the 256 KiB output budget \u2014 the full edit is on disk." : void 0;
|
|
19308
19573
|
const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
|
|
19309
19574
|
let syntaxNote;
|
|
19310
19575
|
if (syntax && syntax.errors.length > 0) {
|
|
19311
19576
|
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.`;
|
|
19312
19577
|
}
|
|
19313
|
-
const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
|
|
19578
|
+
const notes = [autoReadNote, tierNote, diffNote, syntaxNote].filter(Boolean);
|
|
19314
19579
|
return {
|
|
19315
19580
|
path: absPath,
|
|
19316
19581
|
replacements: input.replace_all ? count : 1,
|
|
@@ -19321,6 +19586,15 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
19321
19586
|
};
|
|
19322
19587
|
}
|
|
19323
19588
|
};
|
|
19589
|
+
function noMatchError(inputPath, fileLf, oldLf) {
|
|
19590
|
+
const hint = nearestMatchHint(fileLf, oldLf);
|
|
19591
|
+
return new ToolValidationError3({
|
|
19592
|
+
message: `edit: no match for old_string in "${inputPath}".${hint ? ` Nearest match near line ${hint.line}:
|
|
19593
|
+
${hint.snippet}
|
|
19594
|
+
Compare this against your old_string and retry with the file's actual text.` : ""}`,
|
|
19595
|
+
field: "old_string"
|
|
19596
|
+
});
|
|
19597
|
+
}
|
|
19324
19598
|
|
|
19325
19599
|
// src/exec.ts
|
|
19326
19600
|
import { spawn as spawn8 } from "node:child_process";
|
|
@@ -19329,14 +19603,14 @@ import {
|
|
|
19329
19603
|
emitProcessOutput as emitProcessOutput3,
|
|
19330
19604
|
emitProcessStarted as emitProcessStarted3
|
|
19331
19605
|
} from "@wrongstack/core/observability";
|
|
19332
|
-
import { toErrorMessage as
|
|
19606
|
+
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils/error";
|
|
19333
19607
|
init_output_spool();
|
|
19334
19608
|
init_util();
|
|
19335
19609
|
init_win32_resolve();
|
|
19336
19610
|
|
|
19337
19611
|
// src/exec-kill-guard.ts
|
|
19338
19612
|
import * as os8 from "node:os";
|
|
19339
|
-
import * as
|
|
19613
|
+
import * as path31 from "node:path";
|
|
19340
19614
|
var isWin3 = os8.platform() === "win32";
|
|
19341
19615
|
async function checkExecKillCommand(cmd, args) {
|
|
19342
19616
|
if (!cmd) return { blocked: false };
|
|
@@ -19549,7 +19823,7 @@ async function checkKillTarget(target) {
|
|
|
19549
19823
|
reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
|
|
19550
19824
|
};
|
|
19551
19825
|
}
|
|
19552
|
-
const currentImage =
|
|
19826
|
+
const currentImage = path31.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
|
|
19553
19827
|
const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
|
|
19554
19828
|
if (targetsNodeRuntime && currentImage === "node") {
|
|
19555
19829
|
return {
|
|
@@ -20200,6 +20474,7 @@ function getExecAllowlist() {
|
|
|
20200
20474
|
var MAX_ARGS = 20;
|
|
20201
20475
|
var MAX_OUTPUT2 = 2e5;
|
|
20202
20476
|
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
20477
|
+
var MAX_TIMEOUT_MS = 6e5;
|
|
20203
20478
|
var BLOCKED_ARG_PATTERNS = {
|
|
20204
20479
|
python: [],
|
|
20205
20480
|
// git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
|
|
@@ -20325,8 +20600,8 @@ var SAFE_DANGER = { level: "safe", reasons: [] };
|
|
|
20325
20600
|
var execTool = {
|
|
20326
20601
|
name: "exec",
|
|
20327
20602
|
category: "Shell",
|
|
20328
|
-
description: "Execute a
|
|
20329
|
-
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).\
|
|
20603
|
+
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.",
|
|
20604
|
+
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.",
|
|
20330
20605
|
selection: {
|
|
20331
20606
|
doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
|
|
20332
20607
|
useInstead: ["bash"]
|
|
@@ -20342,7 +20617,13 @@ var execTool = {
|
|
|
20342
20617
|
subjectKey: "command",
|
|
20343
20618
|
mutating: true,
|
|
20344
20619
|
riskTier: "standard",
|
|
20345
|
-
|
|
20620
|
+
// Executor-level abort ceiling. Must sit ABOVE the per-call timeout ceiling
|
|
20621
|
+
// (MAX_TIMEOUT_MS): the tool's own timer resolves with exit 124 + registry
|
|
20622
|
+
// tree-kill; the executor's AbortSignal.timeout is a blunt abort that would
|
|
20623
|
+
// otherwise fire first and discard the structured timeout result. The 10s
|
|
20624
|
+
// margin covers the kill/teardown window. (The executor additionally clamps
|
|
20625
|
+
// to config `tools.maxToolTimeoutMs`.)
|
|
20626
|
+
timeoutMs: MAX_TIMEOUT_MS + 1e4,
|
|
20346
20627
|
capabilities: ["shell.restricted"],
|
|
20347
20628
|
icon: "terminal",
|
|
20348
20629
|
inputSchema: {
|
|
@@ -20363,7 +20644,7 @@ var execTool = {
|
|
|
20363
20644
|
},
|
|
20364
20645
|
timeout: {
|
|
20365
20646
|
type: "integer",
|
|
20366
|
-
description: "Per-command timeout in milliseconds."
|
|
20647
|
+
description: "Per-command timeout in milliseconds (default 30000, max 600000)."
|
|
20367
20648
|
}
|
|
20368
20649
|
},
|
|
20369
20650
|
required: ["command"]
|
|
@@ -20407,7 +20688,7 @@ var execTool = {
|
|
|
20407
20688
|
};
|
|
20408
20689
|
}
|
|
20409
20690
|
const args = (input.args ?? []).slice(0, MAX_ARGS);
|
|
20410
|
-
const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS3,
|
|
20691
|
+
const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS3, MAX_TIMEOUT_MS));
|
|
20411
20692
|
const danger = detectDanger(cmd, args, dangerBypass);
|
|
20412
20693
|
const killCheck = await checkExecKillCommand(cmd, args);
|
|
20413
20694
|
if (killCheck.blocked) {
|
|
@@ -20435,15 +20716,16 @@ var execTool = {
|
|
|
20435
20716
|
danger
|
|
20436
20717
|
};
|
|
20437
20718
|
}
|
|
20719
|
+
const defaultCwd = ctx.workingDir ?? ctx.cwd;
|
|
20438
20720
|
let cwd;
|
|
20439
20721
|
try {
|
|
20440
|
-
cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(
|
|
20722
|
+
cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(defaultCwd, ctx);
|
|
20441
20723
|
} catch {
|
|
20442
20724
|
return {
|
|
20443
20725
|
command: cmd,
|
|
20444
20726
|
args,
|
|
20445
20727
|
stdout: "",
|
|
20446
|
-
stderr: `cwd "${input.cwd ??
|
|
20728
|
+
stderr: `cwd "${input.cwd ?? defaultCwd}" resolves outside project root`,
|
|
20447
20729
|
exitCode: 1,
|
|
20448
20730
|
truncated: false,
|
|
20449
20731
|
allowed: false,
|
|
@@ -20455,7 +20737,7 @@ var execTool = {
|
|
|
20455
20737
|
}
|
|
20456
20738
|
};
|
|
20457
20739
|
function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
20458
|
-
return new Promise((
|
|
20740
|
+
return new Promise((resolve18) => {
|
|
20459
20741
|
let stdout = "";
|
|
20460
20742
|
let stderr = "";
|
|
20461
20743
|
let killed = false;
|
|
@@ -20463,7 +20745,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
20463
20745
|
const finish = (result) => {
|
|
20464
20746
|
if (resolvedOnce.value) return;
|
|
20465
20747
|
resolvedOnce.value = true;
|
|
20466
|
-
|
|
20748
|
+
resolve18(result);
|
|
20467
20749
|
};
|
|
20468
20750
|
const startedAt = Date.now();
|
|
20469
20751
|
let stdoutBytes = 0;
|
|
@@ -20515,7 +20797,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
20515
20797
|
command: cmd,
|
|
20516
20798
|
args,
|
|
20517
20799
|
stdout: "",
|
|
20518
|
-
stderr: `spawn failed: ${
|
|
20800
|
+
stderr: `spawn failed: ${toErrorMessage6(err)}`,
|
|
20519
20801
|
exitCode: 1,
|
|
20520
20802
|
truncated: false,
|
|
20521
20803
|
allowed: true,
|
|
@@ -20618,7 +20900,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
20618
20900
|
}
|
|
20619
20901
|
|
|
20620
20902
|
// src/fetch.ts
|
|
20621
|
-
import { FetchError as FetchError2, ToolError, ToolValidationError as
|
|
20903
|
+
import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
|
|
20622
20904
|
import TurndownService from "turndown";
|
|
20623
20905
|
|
|
20624
20906
|
// src/_fetch-guard.ts
|
|
@@ -20628,7 +20910,7 @@ import {
|
|
|
20628
20910
|
isPrivateIPv4 as isPrivateIPv42,
|
|
20629
20911
|
isPrivateIPv6 as isPrivateIPv62
|
|
20630
20912
|
} from "@wrongstack/core/utils";
|
|
20631
|
-
import { FetchError, ToolValidationError as
|
|
20913
|
+
import { FetchError, ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
20632
20914
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
20633
20915
|
var nativeGlobalFetch = globalThis.fetch;
|
|
20634
20916
|
var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
|
|
@@ -20699,13 +20981,13 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
20699
20981
|
for (; ; ) {
|
|
20700
20982
|
const parsed = new URL(currentUrl);
|
|
20701
20983
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
20702
|
-
throw new
|
|
20984
|
+
throw new ToolValidationError4({
|
|
20703
20985
|
message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
|
|
20704
20986
|
field: "url"
|
|
20705
20987
|
});
|
|
20706
20988
|
}
|
|
20707
20989
|
if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
20708
|
-
throw new
|
|
20990
|
+
throw new ToolValidationError4({
|
|
20709
20991
|
message: "fetch: redirect to http:// blocked (HTTPS required by default)",
|
|
20710
20992
|
field: "url"
|
|
20711
20993
|
});
|
|
@@ -20721,6 +21003,10 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
20721
21003
|
if (res.status < 300 || res.status > 399) {
|
|
20722
21004
|
return res;
|
|
20723
21005
|
}
|
|
21006
|
+
try {
|
|
21007
|
+
await res.body?.cancel();
|
|
21008
|
+
} catch {
|
|
21009
|
+
}
|
|
20724
21010
|
redirectCount++;
|
|
20725
21011
|
if (redirectCount > maxRedirects) {
|
|
20726
21012
|
throw new FetchError({
|
|
@@ -20744,7 +21030,7 @@ async function assertNotPrivate(hostname4) {
|
|
|
20744
21030
|
if (ALLOW_PRIVATE) return;
|
|
20745
21031
|
const host = hostname4.startsWith("[") && hostname4.endsWith("]") ? hostname4.slice(1, -1) : hostname4;
|
|
20746
21032
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
20747
|
-
throw new
|
|
21033
|
+
throw new ToolValidationError4({
|
|
20748
21034
|
message: "fetch: blocked localhost target",
|
|
20749
21035
|
field: "url"
|
|
20750
21036
|
});
|
|
@@ -20752,14 +21038,14 @@ async function assertNotPrivate(hostname4) {
|
|
|
20752
21038
|
const ipVersion = net4.isIP(host);
|
|
20753
21039
|
if (ipVersion === 4) {
|
|
20754
21040
|
if (isPrivateIPv42(host)) {
|
|
20755
|
-
throw new
|
|
21041
|
+
throw new ToolValidationError4({
|
|
20756
21042
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
20757
21043
|
field: "url"
|
|
20758
21044
|
});
|
|
20759
21045
|
}
|
|
20760
21046
|
} else if (ipVersion === 6) {
|
|
20761
21047
|
if (isPrivateIPv62(host)) {
|
|
20762
|
-
throw new
|
|
21048
|
+
throw new ToolValidationError4({
|
|
20763
21049
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
20764
21050
|
field: "url"
|
|
20765
21051
|
});
|
|
@@ -20770,14 +21056,14 @@ async function assertNotPrivate(hostname4) {
|
|
|
20770
21056
|
for (const r of records) {
|
|
20771
21057
|
const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
|
|
20772
21058
|
if (bad) {
|
|
20773
|
-
throw new
|
|
21059
|
+
throw new ToolValidationError4({
|
|
20774
21060
|
message: `fetch: resolved to private address ${r.address}`,
|
|
20775
21061
|
field: "url"
|
|
20776
21062
|
});
|
|
20777
21063
|
}
|
|
20778
21064
|
}
|
|
20779
21065
|
} catch (err) {
|
|
20780
|
-
if (err instanceof
|
|
21066
|
+
if (err instanceof ToolValidationError4) throw err;
|
|
20781
21067
|
}
|
|
20782
21068
|
}
|
|
20783
21069
|
}
|
|
@@ -20794,6 +21080,8 @@ TD.addRule("stripDangerousElements", {
|
|
|
20794
21080
|
filter: ["script", "style", "noscript"],
|
|
20795
21081
|
replacement: () => ""
|
|
20796
21082
|
});
|
|
21083
|
+
var PRUNED_BOILERPLATE_TAGS = /* @__PURE__ */ new Set(["nav", "header", "footer", "aside", "svg", "iframe"]);
|
|
21084
|
+
TD.remove((node) => PRUNED_BOILERPLATE_TAGS.has(node.nodeName.toLowerCase()));
|
|
20797
21085
|
var MAX_BYTES = 131072;
|
|
20798
21086
|
var TIMEOUT_MS = 2e4;
|
|
20799
21087
|
var combineSignals = (signals) => AbortSignal.any(signals);
|
|
@@ -20823,7 +21111,7 @@ var fetchTool = {
|
|
|
20823
21111
|
format: {
|
|
20824
21112
|
type: "string",
|
|
20825
21113
|
enum: ["markdown", "text", "raw"],
|
|
20826
|
-
description: 'Output format. "markdown" is recommended for HTML pages.'
|
|
21114
|
+
description: 'Output format. "markdown" is recommended for HTML pages; for non-HTML content types it falls back to plain text (JSON is pretty-printed).'
|
|
20827
21115
|
}
|
|
20828
21116
|
},
|
|
20829
21117
|
required: ["url"]
|
|
@@ -20852,20 +21140,26 @@ var fetchTool = {
|
|
|
20852
21140
|
},
|
|
20853
21141
|
async *executeStream(input, ctx, opts) {
|
|
20854
21142
|
if (!input?.url) {
|
|
20855
|
-
throw new
|
|
21143
|
+
throw new ToolValidationError5({
|
|
20856
21144
|
message: "fetch: url is required",
|
|
20857
21145
|
field: "url"
|
|
20858
21146
|
});
|
|
20859
21147
|
}
|
|
20860
21148
|
const u = new URL(input.url);
|
|
21149
|
+
if (u.username || u.password) {
|
|
21150
|
+
throw new ToolValidationError5({
|
|
21151
|
+
message: "fetch: URLs with embedded credentials (user:pass@host) are not allowed",
|
|
21152
|
+
field: "url"
|
|
21153
|
+
});
|
|
21154
|
+
}
|
|
20861
21155
|
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
20862
|
-
throw new
|
|
21156
|
+
throw new ToolValidationError5({
|
|
20863
21157
|
message: `fetch: unsupported protocol "${u.protocol}"`,
|
|
20864
21158
|
field: "url"
|
|
20865
21159
|
});
|
|
20866
21160
|
}
|
|
20867
21161
|
if (u.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
20868
|
-
throw new
|
|
21162
|
+
throw new ToolValidationError5({
|
|
20869
21163
|
message: "fetch: http:// blocked (HTTPS required by default)",
|
|
20870
21164
|
field: "url"
|
|
20871
21165
|
});
|
|
@@ -21055,8 +21349,9 @@ var formatTool = {
|
|
|
21055
21349
|
type: "final",
|
|
21056
21350
|
output: {
|
|
21057
21351
|
fixer: bridge.language,
|
|
21058
|
-
|
|
21059
|
-
|
|
21352
|
+
// Language-bridge runs don't report per-file counts.
|
|
21353
|
+
files_checked: void 0,
|
|
21354
|
+
files_changed: void 0,
|
|
21060
21355
|
output: normalizeCommandOutput(run.output || run.error || ""),
|
|
21061
21356
|
truncated: run.truncated
|
|
21062
21357
|
}
|
|
@@ -21083,11 +21378,14 @@ var formatTool = {
|
|
|
21083
21378
|
text: `Running ${detected}\u2026`,
|
|
21084
21379
|
data: { fixer: detected, check: !!input.check }
|
|
21085
21380
|
};
|
|
21086
|
-
const
|
|
21087
|
-
|
|
21088
|
-
if (
|
|
21089
|
-
|
|
21090
|
-
args.push(
|
|
21381
|
+
const fileList = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()) : [];
|
|
21382
|
+
let args;
|
|
21383
|
+
if (detected === "prettier") {
|
|
21384
|
+
args = [input.check ? "--check" : "--write"];
|
|
21385
|
+
args.push(...fileList.length > 0 ? fileList : ["."]);
|
|
21386
|
+
} else {
|
|
21387
|
+
args = ["format", input.check ? "--check" : "--write"];
|
|
21388
|
+
if (fileList.length > 0) args.push("--", ...fileList);
|
|
21091
21389
|
}
|
|
21092
21390
|
const result = yield* spawnStream({
|
|
21093
21391
|
cmd: detected,
|
|
@@ -21096,39 +21394,70 @@ var formatTool = {
|
|
|
21096
21394
|
signal: opts.signal,
|
|
21097
21395
|
maxBytes: 1e5
|
|
21098
21396
|
});
|
|
21099
|
-
const
|
|
21397
|
+
const combinedOut = `${result.stdout}
|
|
21398
|
+
${result.stderr}`;
|
|
21399
|
+
const counts = parseFormatterCounts(detected, combinedOut);
|
|
21100
21400
|
yield {
|
|
21101
21401
|
type: "final",
|
|
21102
21402
|
output: {
|
|
21103
21403
|
fixer: detected,
|
|
21104
|
-
files_checked:
|
|
21105
|
-
files_changed: changed,
|
|
21404
|
+
files_checked: counts.checked,
|
|
21405
|
+
files_changed: counts.changed,
|
|
21106
21406
|
output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
|
|
21107
21407
|
truncated: result.truncated
|
|
21108
21408
|
}
|
|
21109
21409
|
};
|
|
21110
21410
|
}
|
|
21111
21411
|
};
|
|
21412
|
+
function parseFormatterCounts(fixer, output) {
|
|
21413
|
+
if (fixer !== "biome") return { checked: void 0, changed: void 0 };
|
|
21414
|
+
const checkedMatch = /\b(?:Checked|Formatted)\s+(\d+)\s+files?\b/i.exec(output);
|
|
21415
|
+
const changedMatch = /\bFixed\s+(\d+)\s+files?\b/i.exec(output);
|
|
21416
|
+
return {
|
|
21417
|
+
checked: checkedMatch?.[1] !== void 0 ? Number(checkedMatch[1]) : void 0,
|
|
21418
|
+
changed: changedMatch?.[1] !== void 0 ? Number(changedMatch[1]) : void 0
|
|
21419
|
+
};
|
|
21420
|
+
}
|
|
21112
21421
|
async function detectFixer(cwd) {
|
|
21113
|
-
const
|
|
21114
|
-
|
|
21115
|
-
await stat19(`${cwd}/biome.json`);
|
|
21116
|
-
return "biome";
|
|
21117
|
-
} catch {
|
|
21422
|
+
const fs36 = await import("node:fs/promises");
|
|
21423
|
+
const exists = async (file) => {
|
|
21118
21424
|
try {
|
|
21119
|
-
await
|
|
21120
|
-
return
|
|
21425
|
+
await fs36.stat(`${cwd}/${file}`);
|
|
21426
|
+
return true;
|
|
21121
21427
|
} catch {
|
|
21122
|
-
return
|
|
21428
|
+
return false;
|
|
21123
21429
|
}
|
|
21430
|
+
};
|
|
21431
|
+
if (await exists("biome.json") || await exists("biome.jsonc")) return "biome";
|
|
21432
|
+
const PRETTIER_CONFIGS = [
|
|
21433
|
+
".prettierrc",
|
|
21434
|
+
".prettierrc.json",
|
|
21435
|
+
".prettierrc.yml",
|
|
21436
|
+
".prettierrc.yaml",
|
|
21437
|
+
".prettierrc.js",
|
|
21438
|
+
".prettierrc.cjs",
|
|
21439
|
+
".prettierrc.mjs",
|
|
21440
|
+
"prettier.config.js",
|
|
21441
|
+
"prettier.config.cjs",
|
|
21442
|
+
"prettier.config.mjs"
|
|
21443
|
+
];
|
|
21444
|
+
for (const cfg of PRETTIER_CONFIGS) {
|
|
21445
|
+
if (await exists(cfg)) return "prettier";
|
|
21446
|
+
}
|
|
21447
|
+
try {
|
|
21448
|
+
const raw = await fs36.readFile(`${cwd}/package.json`, "utf8");
|
|
21449
|
+
const pkg = JSON.parse(raw);
|
|
21450
|
+
if (pkg["prettier"] !== void 0) return "prettier";
|
|
21451
|
+
} catch {
|
|
21124
21452
|
}
|
|
21453
|
+
return "biome";
|
|
21125
21454
|
}
|
|
21126
21455
|
|
|
21127
21456
|
// src/git.ts
|
|
21128
21457
|
init_util();
|
|
21129
21458
|
import { spawn as spawn9 } from "node:child_process";
|
|
21130
21459
|
import { statSync as statSync4 } from "node:fs";
|
|
21131
|
-
import { dirname as dirname13, resolve as
|
|
21460
|
+
import { dirname as dirname13, resolve as resolve14, sep as sep6 } from "node:path";
|
|
21132
21461
|
import { assessCommitSafety } from "@wrongstack/core/coordination";
|
|
21133
21462
|
import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
|
|
21134
21463
|
var TIMEOUT_MS2 = 3e4;
|
|
@@ -21289,8 +21618,8 @@ function validateWorktreeInput(input, projectRoot) {
|
|
|
21289
21618
|
return reject(`unsafe worktree path: ${input.worktreePath}`);
|
|
21290
21619
|
}
|
|
21291
21620
|
if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
|
|
21292
|
-
const root =
|
|
21293
|
-
const abs =
|
|
21621
|
+
const root = resolve14(projectRoot);
|
|
21622
|
+
const abs = resolve14(root, input.worktreePath);
|
|
21294
21623
|
if (abs !== root && !abs.startsWith(root + sep6)) {
|
|
21295
21624
|
return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
|
|
21296
21625
|
}
|
|
@@ -21302,8 +21631,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
21302
21631
|
let dir = cwd;
|
|
21303
21632
|
for (let i = 0; i < 20; i++) {
|
|
21304
21633
|
try {
|
|
21305
|
-
const
|
|
21306
|
-
if (
|
|
21634
|
+
const stat20 = statSync4(`${dir}/.git`);
|
|
21635
|
+
if (stat20.isDirectory() || stat20.isFile()) return dir;
|
|
21307
21636
|
} catch {
|
|
21308
21637
|
}
|
|
21309
21638
|
if (dir === root) break;
|
|
@@ -21384,7 +21713,7 @@ function buildArgs(input) {
|
|
|
21384
21713
|
}
|
|
21385
21714
|
}
|
|
21386
21715
|
function runGit2(args, cwd, signal) {
|
|
21387
|
-
return new Promise((
|
|
21716
|
+
return new Promise((resolve18) => {
|
|
21388
21717
|
let stdout = "";
|
|
21389
21718
|
let stderr = "";
|
|
21390
21719
|
const child = spawn9("git", args, {
|
|
@@ -21405,7 +21734,7 @@ function runGit2(args, cwd, signal) {
|
|
|
21405
21734
|
}
|
|
21406
21735
|
});
|
|
21407
21736
|
child.on("error", (err) => {
|
|
21408
|
-
|
|
21737
|
+
resolve18({
|
|
21409
21738
|
command: args[0],
|
|
21410
21739
|
stdout: normalizeCommandOutput(stdout),
|
|
21411
21740
|
stderr: err.message,
|
|
@@ -21414,7 +21743,7 @@ function runGit2(args, cwd, signal) {
|
|
|
21414
21743
|
});
|
|
21415
21744
|
});
|
|
21416
21745
|
child.on("close", (code) => {
|
|
21417
|
-
|
|
21746
|
+
resolve18({
|
|
21418
21747
|
command: args[0],
|
|
21419
21748
|
stdout: normalizeCommandOutput(stdout),
|
|
21420
21749
|
stderr: normalizeCommandOutput(stderr),
|
|
@@ -21427,8 +21756,9 @@ function runGit2(args, cwd, signal) {
|
|
|
21427
21756
|
|
|
21428
21757
|
// src/glob.ts
|
|
21429
21758
|
import * as fs25 from "node:fs/promises";
|
|
21430
|
-
import * as
|
|
21759
|
+
import * as path32 from "node:path";
|
|
21431
21760
|
import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
|
|
21761
|
+
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
21432
21762
|
|
|
21433
21763
|
// src/_concurrency.ts
|
|
21434
21764
|
async function mapWithConcurrency2(items, limit, fn) {
|
|
@@ -21454,7 +21784,7 @@ var WALK_CONCURRENCY = 16;
|
|
|
21454
21784
|
var globTool = {
|
|
21455
21785
|
name: "glob",
|
|
21456
21786
|
category: "Filesystem",
|
|
21457
|
-
description: "Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
|
|
21787
|
+
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.",
|
|
21458
21788
|
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.",
|
|
21459
21789
|
selection: {
|
|
21460
21790
|
doNotUseWhen: "you need to search inside file contents.",
|
|
@@ -21465,7 +21795,7 @@ var globTool = {
|
|
|
21465
21795
|
capabilities: ["fs.read"],
|
|
21466
21796
|
icon: "folder",
|
|
21467
21797
|
maxOutputBytes: 65536,
|
|
21468
|
-
timeoutMs:
|
|
21798
|
+
timeoutMs: 15e3,
|
|
21469
21799
|
inputSchema: {
|
|
21470
21800
|
type: "object",
|
|
21471
21801
|
properties: {
|
|
@@ -21479,13 +21809,20 @@ var globTool = {
|
|
|
21479
21809
|
},
|
|
21480
21810
|
limit: {
|
|
21481
21811
|
type: "integer",
|
|
21482
|
-
|
|
21812
|
+
minimum: 1,
|
|
21813
|
+
maximum: 5e3,
|
|
21814
|
+
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."
|
|
21483
21815
|
}
|
|
21484
21816
|
},
|
|
21485
21817
|
required: ["pattern"]
|
|
21486
21818
|
},
|
|
21487
21819
|
async execute(input, ctx, opts) {
|
|
21488
|
-
if (!input?.pattern)
|
|
21820
|
+
if (!input?.pattern) {
|
|
21821
|
+
throw new ToolValidationError6({
|
|
21822
|
+
message: "glob: pattern is required",
|
|
21823
|
+
field: "pattern"
|
|
21824
|
+
});
|
|
21825
|
+
}
|
|
21489
21826
|
const signal = opts?.signal;
|
|
21490
21827
|
const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
|
|
21491
21828
|
const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
|
|
@@ -21530,7 +21867,7 @@ var globTool = {
|
|
|
21530
21867
|
const name = e.name;
|
|
21531
21868
|
if (DEFAULT_IGNORE2.has(name)) continue;
|
|
21532
21869
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
21533
|
-
const full =
|
|
21870
|
+
const full = path32.join(dir, name);
|
|
21534
21871
|
if (e.isDirectory()) {
|
|
21535
21872
|
if (isGitIgnored(rel, true)) continue;
|
|
21536
21873
|
subdirs.push({ full, rel });
|
|
@@ -21580,8 +21917,8 @@ var globTool = {
|
|
|
21580
21917
|
// src/grep.ts
|
|
21581
21918
|
import { spawn as spawn10 } from "node:child_process";
|
|
21582
21919
|
import * as fs26 from "node:fs/promises";
|
|
21583
|
-
import * as
|
|
21584
|
-
import { ToolValidationError as
|
|
21920
|
+
import * as path33 from "node:path";
|
|
21921
|
+
import { ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
|
|
21585
21922
|
import {
|
|
21586
21923
|
buildChildEnv as buildChildEnv5,
|
|
21587
21924
|
compileGlob as compileGlob3,
|
|
@@ -21785,7 +22122,7 @@ var grepTool = {
|
|
|
21785
22122
|
},
|
|
21786
22123
|
async *executeStream(input, ctx, opts) {
|
|
21787
22124
|
if (!input?.pattern) {
|
|
21788
|
-
throw new
|
|
22125
|
+
throw new ToolValidationError7({
|
|
21789
22126
|
message: "grep: pattern is required",
|
|
21790
22127
|
field: "pattern"
|
|
21791
22128
|
});
|
|
@@ -21795,12 +22132,12 @@ var grepTool = {
|
|
|
21795
22132
|
const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
|
|
21796
22133
|
const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
|
|
21797
22134
|
if (!validation.ok) {
|
|
21798
|
-
throw new
|
|
22135
|
+
throw new ToolValidationError7({
|
|
21799
22136
|
message: `grep: ${validation.reason}`,
|
|
21800
22137
|
field: "pattern"
|
|
21801
22138
|
});
|
|
21802
22139
|
}
|
|
21803
|
-
const rgAvailable = await detectRg(
|
|
22140
|
+
const rgAvailable = await detectRg();
|
|
21804
22141
|
if (rgAvailable) {
|
|
21805
22142
|
try {
|
|
21806
22143
|
yield* runRgStream(input, base, mode, limit, opts.signal);
|
|
@@ -21813,16 +22150,23 @@ var grepTool = {
|
|
|
21813
22150
|
yield { type: "final", output: out };
|
|
21814
22151
|
}
|
|
21815
22152
|
};
|
|
21816
|
-
|
|
21817
|
-
|
|
22153
|
+
var rgAvailabilityCache;
|
|
22154
|
+
function detectRg() {
|
|
22155
|
+
rgAvailabilityCache ??= new Promise((resolve18) => {
|
|
21818
22156
|
try {
|
|
21819
|
-
const p = spawn10("rg", ["--version"], {
|
|
21820
|
-
|
|
21821
|
-
|
|
22157
|
+
const p = spawn10("rg", ["--version"], {
|
|
22158
|
+
env: buildChildEnv5(),
|
|
22159
|
+
stdio: "ignore",
|
|
22160
|
+
signal: AbortSignal.timeout(1e4),
|
|
22161
|
+
windowsHide: true
|
|
22162
|
+
});
|
|
22163
|
+
p.on("error", () => resolve18(false));
|
|
22164
|
+
p.on("close", (code) => resolve18(code === 0));
|
|
21822
22165
|
} catch {
|
|
21823
|
-
|
|
22166
|
+
resolve18(false);
|
|
21824
22167
|
}
|
|
21825
22168
|
});
|
|
22169
|
+
return rgAvailabilityCache;
|
|
21826
22170
|
}
|
|
21827
22171
|
async function* runRgStream(input, base, mode, limit, signal) {
|
|
21828
22172
|
const args = ["--no-heading"];
|
|
@@ -21836,7 +22180,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
21836
22180
|
for (const ignored of DEFAULT_IGNORE3) {
|
|
21837
22181
|
args.push("--glob", `!${ignored}/**`, "--glob", `!**/${ignored}/**`);
|
|
21838
22182
|
}
|
|
21839
|
-
const gitignorePath =
|
|
22183
|
+
const gitignorePath = path33.join(base, ".gitignore");
|
|
21840
22184
|
if (await fs26.access(gitignorePath).then(() => true, () => false)) {
|
|
21841
22185
|
args.push("--ignore-file", gitignorePath);
|
|
21842
22186
|
}
|
|
@@ -22008,7 +22352,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
22008
22352
|
const flags = input.case_insensitive ? "i" : "";
|
|
22009
22353
|
const compiled = compileUserRegex(input.pattern, flags);
|
|
22010
22354
|
if (!compiled.ok) {
|
|
22011
|
-
throw new
|
|
22355
|
+
throw new ToolValidationError7({
|
|
22012
22356
|
message: `grep: ${compiled.reason}`,
|
|
22013
22357
|
field: "pattern"
|
|
22014
22358
|
});
|
|
@@ -22026,8 +22370,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
22026
22370
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
22027
22371
|
if (globRe) globRe.lastIndex = 0;
|
|
22028
22372
|
try {
|
|
22029
|
-
const
|
|
22030
|
-
if (!
|
|
22373
|
+
const stat20 = await fs26.stat(full);
|
|
22374
|
+
if (!stat20.isFile() || stat20.size > maxBytes || stopped || signal.aborted) return;
|
|
22031
22375
|
const file = await fs26.open(full, "r");
|
|
22032
22376
|
try {
|
|
22033
22377
|
let bytesReadTotal = 0;
|
|
@@ -22118,7 +22462,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
22118
22462
|
if (DEFAULT_IGNORE3.has(e.name)) continue;
|
|
22119
22463
|
if (e.isSymbolicLink()) continue;
|
|
22120
22464
|
const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
|
|
22121
|
-
const full =
|
|
22465
|
+
const full = path33.join(dir, e.name);
|
|
22122
22466
|
if (e.isDirectory()) {
|
|
22123
22467
|
if (isGitIgnored(rel, true)) continue;
|
|
22124
22468
|
subdirs.push({ full, rel });
|
|
@@ -22238,26 +22582,14 @@ var installTool = {
|
|
|
22238
22582
|
return;
|
|
22239
22583
|
}
|
|
22240
22584
|
}
|
|
22241
|
-
const pkgManager = await detectPackageManager(cwd);
|
|
22585
|
+
const pkgManager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
22242
22586
|
yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
|
|
22243
|
-
const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
|
|
22244
22587
|
const globalFlag = input.global ? ["-g"] : [];
|
|
22245
22588
|
const ignoreScripts = input.lifecycleScripts !== true;
|
|
22246
|
-
const args = [];
|
|
22247
|
-
if (input.dry_run) args.push("--dry-run");
|
|
22248
|
-
if (ignoreScripts) args.push("--ignore-scripts");
|
|
22249
|
-
if (pkgManager === "pnpm") {
|
|
22250
|
-
if (save) args.push(save);
|
|
22251
|
-
args.push("add", ...globalFlag);
|
|
22252
|
-
} else if (pkgManager === "yarn") {
|
|
22253
|
-
args.push("add", ...globalFlag);
|
|
22254
|
-
} else {
|
|
22255
|
-
args.push("install", ...globalFlag);
|
|
22256
|
-
}
|
|
22257
22589
|
const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
|
|
22258
22590
|
(p) => p.trim()
|
|
22259
22591
|
) : [];
|
|
22260
|
-
const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]
|
|
22592
|
+
const PKG_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9^~><=*.+-]+)?$/i;
|
|
22261
22593
|
for (const pkg of pkgList) {
|
|
22262
22594
|
if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-") || pkg.length > 200) {
|
|
22263
22595
|
yield {
|
|
@@ -22273,7 +22605,34 @@ var installTool = {
|
|
|
22273
22605
|
return;
|
|
22274
22606
|
}
|
|
22275
22607
|
}
|
|
22276
|
-
|
|
22608
|
+
const hasPkgs = pkgList.length > 0;
|
|
22609
|
+
const args = [];
|
|
22610
|
+
if (input.dry_run) args.push("--dry-run");
|
|
22611
|
+
if (ignoreScripts) args.push("--ignore-scripts");
|
|
22612
|
+
if (pkgManager === "pnpm") {
|
|
22613
|
+
if (hasPkgs) {
|
|
22614
|
+
if (input.save === "dev") args.push("-D");
|
|
22615
|
+
else if (input.save === "optional") args.push("-O");
|
|
22616
|
+
args.push("add", ...globalFlag);
|
|
22617
|
+
} else {
|
|
22618
|
+
args.push("install", ...globalFlag);
|
|
22619
|
+
}
|
|
22620
|
+
} else if (pkgManager === "yarn") {
|
|
22621
|
+
if (hasPkgs) {
|
|
22622
|
+
args.push("add", ...globalFlag);
|
|
22623
|
+
if (input.save === "dev") args.push("--dev");
|
|
22624
|
+
else if (input.save === "optional") args.push("--optional");
|
|
22625
|
+
} else {
|
|
22626
|
+
args.push("install", ...globalFlag);
|
|
22627
|
+
}
|
|
22628
|
+
} else {
|
|
22629
|
+
args.push("install", ...globalFlag);
|
|
22630
|
+
if (hasPkgs) {
|
|
22631
|
+
if (input.save === "dev") args.push("--save-dev");
|
|
22632
|
+
else if (input.save === "optional") args.push("--save-optional");
|
|
22633
|
+
}
|
|
22634
|
+
}
|
|
22635
|
+
if (hasPkgs) args.push(...pkgList);
|
|
22277
22636
|
yield {
|
|
22278
22637
|
type: "log",
|
|
22279
22638
|
text: `Fetching ${pkgList.length || "all"} packages\u2026`,
|
|
@@ -22354,9 +22713,9 @@ var JsonFileTooLargeError = class extends Error {
|
|
|
22354
22713
|
};
|
|
22355
22714
|
async function readJsonFileBounded(filePath, ctx) {
|
|
22356
22715
|
const resolved = await safeResolveReal(filePath, ctx);
|
|
22357
|
-
const
|
|
22358
|
-
if (
|
|
22359
|
-
throw new JsonFileTooLargeError(filePath,
|
|
22716
|
+
const stat20 = await fs27.stat(resolved);
|
|
22717
|
+
if (stat20.size > MAX_JSON_FILE_BYTES) {
|
|
22718
|
+
throw new JsonFileTooLargeError(filePath, stat20.size);
|
|
22360
22719
|
}
|
|
22361
22720
|
return fs27.readFile(resolved, "utf8");
|
|
22362
22721
|
}
|
|
@@ -22762,60 +23121,60 @@ function jmespathSearch(data, query) {
|
|
|
22762
23121
|
}
|
|
22763
23122
|
function validateJsonSchema(data, schema) {
|
|
22764
23123
|
const errors = [];
|
|
22765
|
-
function check(value, s,
|
|
23124
|
+
function check(value, s, path41) {
|
|
22766
23125
|
if (s["type"]) {
|
|
22767
23126
|
const expectedType = s["type"];
|
|
22768
23127
|
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
22769
23128
|
if (expectedType === "integer") {
|
|
22770
|
-
if (!Number.isInteger(value)) errors.push(`${
|
|
23129
|
+
if (!Number.isInteger(value)) errors.push(`${path41}: expected integer, got ${actualType}`);
|
|
22771
23130
|
} else if (expectedType !== actualType) {
|
|
22772
|
-
errors.push(`${
|
|
23131
|
+
errors.push(`${path41}: expected ${expectedType}, got ${actualType}`);
|
|
22773
23132
|
}
|
|
22774
23133
|
}
|
|
22775
23134
|
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
22776
23135
|
try {
|
|
22777
23136
|
new URL(value);
|
|
22778
23137
|
} catch {
|
|
22779
|
-
errors.push(`${
|
|
23138
|
+
errors.push(`${path41}: not a valid URI`);
|
|
22780
23139
|
}
|
|
22781
23140
|
}
|
|
22782
23141
|
if (typeof value === "string" && s["pattern"]) {
|
|
22783
23142
|
const compiled = compileUserRegex(s["pattern"], "");
|
|
22784
23143
|
if (!compiled.ok) {
|
|
22785
|
-
errors.push(`${
|
|
23144
|
+
errors.push(`${path41}: invalid schema pattern \u2014 ${compiled.reason}`);
|
|
22786
23145
|
} else if (!compiled.regex.test(capSubject(value))) {
|
|
22787
|
-
errors.push(`${
|
|
23146
|
+
errors.push(`${path41}: does not match pattern ${s["pattern"]}`);
|
|
22788
23147
|
}
|
|
22789
23148
|
}
|
|
22790
23149
|
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
22791
|
-
errors.push(`${
|
|
23150
|
+
errors.push(`${path41}: string too short (min ${s["minLength"]})`);
|
|
22792
23151
|
}
|
|
22793
23152
|
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
22794
|
-
errors.push(`${
|
|
23153
|
+
errors.push(`${path41}: string too long (max ${s["maxLength"]})`);
|
|
22795
23154
|
}
|
|
22796
23155
|
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
22797
|
-
errors.push(`${
|
|
23156
|
+
errors.push(`${path41}: below minimum ${s["minimum"]}`);
|
|
22798
23157
|
}
|
|
22799
23158
|
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
22800
|
-
errors.push(`${
|
|
23159
|
+
errors.push(`${path41}: above maximum ${s["maximum"]}`);
|
|
22801
23160
|
}
|
|
22802
23161
|
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
22803
23162
|
for (let i = 0; i < value.length; i++) {
|
|
22804
|
-
check(value[i], s["items"], `${
|
|
23163
|
+
check(value[i], s["items"], `${path41}[${i}]`);
|
|
22805
23164
|
}
|
|
22806
23165
|
}
|
|
22807
23166
|
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
22808
23167
|
const props = s["properties"];
|
|
22809
23168
|
for (const [k, propSchema] of Object.entries(props)) {
|
|
22810
|
-
check(value[k], propSchema, `${
|
|
23169
|
+
check(value[k], propSchema, `${path41}.${k}`);
|
|
22811
23170
|
}
|
|
22812
23171
|
}
|
|
22813
23172
|
}
|
|
22814
23173
|
check(data, schema, "$");
|
|
22815
23174
|
return { valid: errors.length === 0, errors };
|
|
22816
23175
|
}
|
|
22817
|
-
function simpleQuery(data,
|
|
22818
|
-
const parts =
|
|
23176
|
+
function simpleQuery(data, path41) {
|
|
23177
|
+
const parts = path41.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
22819
23178
|
let current = data;
|
|
22820
23179
|
for (const part of parts) {
|
|
22821
23180
|
if (current === null || current === void 0) return void 0;
|
|
@@ -23726,6 +24085,18 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
23726
24085
|
},
|
|
23727
24086
|
transitionAction: { type: "string" },
|
|
23728
24087
|
transitionComment: { type: "string" },
|
|
24088
|
+
tickChecks: {
|
|
24089
|
+
type: "array",
|
|
24090
|
+
items: {
|
|
24091
|
+
type: "object",
|
|
24092
|
+
properties: {
|
|
24093
|
+
checkId: { type: "string" },
|
|
24094
|
+
checkStatus: { type: "string", enum: ["passed", "failed", "skipped"] }
|
|
24095
|
+
},
|
|
24096
|
+
required: ["checkId", "checkStatus"]
|
|
24097
|
+
},
|
|
24098
|
+
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."
|
|
24099
|
+
},
|
|
23729
24100
|
attachmentUrl: { type: "string" },
|
|
23730
24101
|
attachmentTitle: { type: "string" },
|
|
23731
24102
|
attachmentType: {
|
|
@@ -24854,6 +25225,9 @@ var kanbanTool = {
|
|
|
24854
25225
|
description: KANBAN_TOOL_DESCRIPTION,
|
|
24855
25226
|
usageHint: KANBAN_TOOL_USAGE_HINT,
|
|
24856
25227
|
permission: "confirm",
|
|
25228
|
+
// WS-046: gives permission decisions something to key on.
|
|
25229
|
+
// The action performed; kanban has no single file or path subject.
|
|
25230
|
+
subjectKey: "action",
|
|
24857
25231
|
mutating: true,
|
|
24858
25232
|
capabilities: ["fs.write"],
|
|
24859
25233
|
icon: "task",
|
|
@@ -25295,6 +25669,7 @@ var kanbanTool = {
|
|
|
25295
25669
|
actor: input.author,
|
|
25296
25670
|
comment: input.transitionComment,
|
|
25297
25671
|
...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
|
|
25672
|
+
...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
|
|
25298
25673
|
...input.attachmentUrl !== void 0 ? {
|
|
25299
25674
|
attachment: {
|
|
25300
25675
|
url: input.attachmentUrl,
|
|
@@ -25687,8 +26062,52 @@ var kanbanTool = {
|
|
|
25687
26062
|
} catch (err) {
|
|
25688
26063
|
return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
|
|
25689
26064
|
}
|
|
26065
|
+
},
|
|
26066
|
+
serialize(output, input) {
|
|
26067
|
+
return serializeKanbanOutput(output, input);
|
|
25690
26068
|
}
|
|
25691
26069
|
};
|
|
26070
|
+
var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
|
|
26071
|
+
var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
|
|
26072
|
+
"get_board",
|
|
26073
|
+
"export_markdown",
|
|
26074
|
+
"export_task_graph"
|
|
26075
|
+
]);
|
|
26076
|
+
function serializeKanbanOutput(output, input) {
|
|
26077
|
+
const action = input && typeof input === "object" ? input.action : void 0;
|
|
26078
|
+
const board = output.board;
|
|
26079
|
+
if (board) {
|
|
26080
|
+
const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
|
|
26081
|
+
let boardBytes = 0;
|
|
26082
|
+
if (!keepFull) {
|
|
26083
|
+
try {
|
|
26084
|
+
boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
|
|
26085
|
+
} catch {
|
|
26086
|
+
boardBytes = 0;
|
|
26087
|
+
}
|
|
26088
|
+
}
|
|
26089
|
+
if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
|
|
26090
|
+
const columns = {};
|
|
26091
|
+
for (const column of board.columns) {
|
|
26092
|
+
columns[column.title || column.id] = board.tasks.filter(
|
|
26093
|
+
(task) => task.columnId === column.id
|
|
26094
|
+
).length;
|
|
26095
|
+
}
|
|
26096
|
+
const compact = {
|
|
26097
|
+
...output,
|
|
26098
|
+
board: {
|
|
26099
|
+
id: board.id,
|
|
26100
|
+
title: board.title,
|
|
26101
|
+
columns,
|
|
26102
|
+
totalTasks: board.tasks.length,
|
|
26103
|
+
note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
|
|
26104
|
+
}
|
|
26105
|
+
};
|
|
26106
|
+
return JSON.stringify(compact, null, 2);
|
|
26107
|
+
}
|
|
26108
|
+
}
|
|
26109
|
+
return JSON.stringify(output, null, 2);
|
|
26110
|
+
}
|
|
25692
26111
|
|
|
25693
26112
|
// src/builtin.ts
|
|
25694
26113
|
init_execute_tool();
|
|
@@ -25803,11 +26222,11 @@ var lintTool = {
|
|
|
25803
26222
|
}
|
|
25804
26223
|
};
|
|
25805
26224
|
async function detectLinter(cwd) {
|
|
25806
|
-
const { stat:
|
|
26225
|
+
const { stat: stat20 } = await import("node:fs/promises");
|
|
25807
26226
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
25808
26227
|
for (const f of checks) {
|
|
25809
26228
|
try {
|
|
25810
|
-
await
|
|
26229
|
+
await stat20(`${cwd}/${f}`);
|
|
25811
26230
|
if (f.includes("biome")) return "biome";
|
|
25812
26231
|
if (f.includes("eslint")) return "eslint";
|
|
25813
26232
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -25824,11 +26243,12 @@ init_util();
|
|
|
25824
26243
|
var logsTool = {
|
|
25825
26244
|
name: "logs",
|
|
25826
26245
|
category: "Logs",
|
|
25827
|
-
description: "Read
|
|
25828
|
-
usageHint: "DEBUGGING TOOL \u2014 USE CAREFULLY IN AUTONOMOUS MODE:\n\n- Prefer `path` for local files or `service` for containers
|
|
26246
|
+
description: "Read logs from files or Docker containers. Useful for debugging running applications.",
|
|
26247
|
+
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.",
|
|
25829
26248
|
permission: "confirm",
|
|
25830
26249
|
mutating: false,
|
|
25831
26250
|
timeoutMs: 3e4,
|
|
26251
|
+
maxOutputBytes: 262144,
|
|
25832
26252
|
capabilities: ["shell.restricted"],
|
|
25833
26253
|
icon: "logs",
|
|
25834
26254
|
inputSchema: {
|
|
@@ -25836,7 +26256,7 @@ var logsTool = {
|
|
|
25836
26256
|
properties: {
|
|
25837
26257
|
service: {
|
|
25838
26258
|
type: "string",
|
|
25839
|
-
description: "
|
|
26259
|
+
description: "Docker container name (passed to `docker logs`)"
|
|
25840
26260
|
},
|
|
25841
26261
|
path: {
|
|
25842
26262
|
type: "string",
|
|
@@ -25848,10 +26268,6 @@ var logsTool = {
|
|
|
25848
26268
|
minimum: 0,
|
|
25849
26269
|
maximum: 1e4
|
|
25850
26270
|
},
|
|
25851
|
-
stream: {
|
|
25852
|
-
type: "boolean",
|
|
25853
|
-
description: "Stream logs continuously (like tail -f) (default: false)"
|
|
25854
|
-
},
|
|
25855
26271
|
filter: {
|
|
25856
26272
|
type: "string",
|
|
25857
26273
|
description: "Regex pattern to filter log lines"
|
|
@@ -25859,7 +26275,7 @@ var logsTool = {
|
|
|
25859
26275
|
since: {
|
|
25860
26276
|
type: "string",
|
|
25861
26277
|
enum: ["1h", "6h", "24h", "all"],
|
|
25862
|
-
description:
|
|
26278
|
+
description: 'Only show Docker logs since duration (ignored for files; "all" = no limit)'
|
|
25863
26279
|
},
|
|
25864
26280
|
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
25865
26281
|
}
|
|
@@ -25876,10 +26292,10 @@ var logsTool = {
|
|
|
25876
26292
|
filterRe = compiled.regex;
|
|
25877
26293
|
}
|
|
25878
26294
|
if (input.service) {
|
|
25879
|
-
return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);
|
|
26295
|
+
return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal, input.since);
|
|
25880
26296
|
}
|
|
25881
26297
|
if (input.path) {
|
|
25882
|
-
return await fileLogs(
|
|
26298
|
+
return await fileLogs(await safeResolveReal(input.path, ctx), lines, filterRe);
|
|
25883
26299
|
}
|
|
25884
26300
|
return {
|
|
25885
26301
|
source: "none",
|
|
@@ -25893,7 +26309,7 @@ var logsTool = {
|
|
|
25893
26309
|
async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
25894
26310
|
const args = ["logs"];
|
|
25895
26311
|
if (lines > 0) args.push("--tail", String(lines));
|
|
25896
|
-
if (since) {
|
|
26312
|
+
if (since && since !== "all") {
|
|
25897
26313
|
const sinceMap = { "1h": "1h", "6h": "6h", "24h": "24h" };
|
|
25898
26314
|
args.push("--since", sinceMap[since] ?? "1h");
|
|
25899
26315
|
}
|
|
@@ -25907,7 +26323,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25907
26323
|
};
|
|
25908
26324
|
}
|
|
25909
26325
|
args.push("--timestamps", service);
|
|
25910
|
-
return new Promise((
|
|
26326
|
+
return new Promise((resolve18) => {
|
|
25911
26327
|
let stdout = "";
|
|
25912
26328
|
let stderr = "";
|
|
25913
26329
|
const MAX = 2e5;
|
|
@@ -25923,7 +26339,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25923
26339
|
if (settled) return;
|
|
25924
26340
|
settled = true;
|
|
25925
26341
|
clearTimeout(timer);
|
|
25926
|
-
|
|
26342
|
+
resolve18(result);
|
|
25927
26343
|
};
|
|
25928
26344
|
const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
25929
26345
|
const timer = setTimeout(() => {
|
|
@@ -25958,7 +26374,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25958
26374
|
}
|
|
25959
26375
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
25960
26376
|
var MAX_TAIL_LINES = 1e5;
|
|
25961
|
-
async function fileLogs(
|
|
26377
|
+
async function fileLogs(path41, lines, filterRe) {
|
|
25962
26378
|
const { createInterface } = await import("node:readline");
|
|
25963
26379
|
const { createReadStream: createReadStream2 } = await import("node:fs");
|
|
25964
26380
|
const entries = [];
|
|
@@ -25967,7 +26383,7 @@ async function fileLogs(path40, lines, filterRe, stream) {
|
|
|
25967
26383
|
let writeIdx = 0;
|
|
25968
26384
|
let totalLines = 0;
|
|
25969
26385
|
const rl = createInterface({
|
|
25970
|
-
input: createReadStream2(
|
|
26386
|
+
input: createReadStream2(path41),
|
|
25971
26387
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
25972
26388
|
});
|
|
25973
26389
|
for await (const line of rl) {
|
|
@@ -25988,11 +26404,11 @@ async function fileLogs(path40, lines, filterRe, stream) {
|
|
|
25988
26404
|
if (parsed) entries.push(parsed);
|
|
25989
26405
|
}
|
|
25990
26406
|
return {
|
|
25991
|
-
source:
|
|
26407
|
+
source: path41,
|
|
25992
26408
|
entries,
|
|
25993
26409
|
total: entries.length,
|
|
25994
26410
|
truncated: totalLines > effLines,
|
|
25995
|
-
stream_mode:
|
|
26411
|
+
stream_mode: false
|
|
25996
26412
|
};
|
|
25997
26413
|
}
|
|
25998
26414
|
function parseLogLines(output, filterRe) {
|
|
@@ -26068,25 +26484,12 @@ var outdatedTool = {
|
|
|
26068
26484
|
inputSchema: {
|
|
26069
26485
|
type: "object",
|
|
26070
26486
|
properties: {
|
|
26071
|
-
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
26072
|
-
format: {
|
|
26073
|
-
type: "string",
|
|
26074
|
-
enum: ["list", "table"],
|
|
26075
|
-
description: "Output format (default: list)"
|
|
26076
|
-
},
|
|
26077
|
-
include_deprecated: {
|
|
26078
|
-
type: "boolean",
|
|
26079
|
-
description: "Include deprecated packages (default: false)"
|
|
26080
|
-
},
|
|
26081
|
-
check: {
|
|
26082
|
-
type: "string",
|
|
26083
|
-
description: "Specific package(s) to check (comma-separated)"
|
|
26084
|
-
}
|
|
26487
|
+
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
26085
26488
|
}
|
|
26086
26489
|
},
|
|
26087
26490
|
async execute(input, ctx, opts) {
|
|
26088
26491
|
const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
|
|
26089
|
-
const manager = await detectPackageManager(cwd);
|
|
26492
|
+
const manager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
26090
26493
|
if (manager === "npm") {
|
|
26091
26494
|
try {
|
|
26092
26495
|
const { detectNonJsEcosystem: detectNonJsEcosystem2 } = await Promise.resolve().then(() => (init_legacy_bridge(), legacy_bridge_exports));
|
|
@@ -26139,13 +26542,11 @@ var outdatedTool = {
|
|
|
26139
26542
|
}
|
|
26140
26543
|
}
|
|
26141
26544
|
const args = ["outdated", "--json"];
|
|
26142
|
-
if (input.format === "table") args.push("--table");
|
|
26143
|
-
if (input.include_deprecated) args.push("--include", "deprecated");
|
|
26144
26545
|
return runOutdated(manager, args, cwd, opts.signal);
|
|
26145
26546
|
}
|
|
26146
26547
|
};
|
|
26147
26548
|
function runOutdated(manager, args, cwd, signal) {
|
|
26148
|
-
return new Promise((
|
|
26549
|
+
return new Promise((resolve18) => {
|
|
26149
26550
|
let stdout = "";
|
|
26150
26551
|
let stderr = "";
|
|
26151
26552
|
const MAX = 1e5;
|
|
@@ -26170,10 +26571,10 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
26170
26571
|
});
|
|
26171
26572
|
child.on("close", (code) => {
|
|
26172
26573
|
const result = parseOutdatedOutput(stdout, code ?? 0);
|
|
26173
|
-
|
|
26574
|
+
resolve18(result);
|
|
26174
26575
|
});
|
|
26175
26576
|
child.on("error", (e) => {
|
|
26176
|
-
|
|
26577
|
+
resolve18({
|
|
26177
26578
|
exit_code: 1,
|
|
26178
26579
|
packages: [],
|
|
26179
26580
|
total: 0,
|
|
@@ -26194,27 +26595,39 @@ function parseOutdatedOutput(json2, exitCode) {
|
|
|
26194
26595
|
truncated: false
|
|
26195
26596
|
};
|
|
26196
26597
|
}
|
|
26598
|
+
const truncated = json2.length >= 1e5 || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
|
|
26599
|
+
let parsedOk = false;
|
|
26197
26600
|
try {
|
|
26198
26601
|
const data = JSON.parse(json2);
|
|
26602
|
+
parsedOk = true;
|
|
26199
26603
|
for (const name of Object.keys(data)) {
|
|
26200
|
-
const info = data[name];
|
|
26604
|
+
const info = data[name] ?? {};
|
|
26605
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
26201
26606
|
packages.push({
|
|
26202
26607
|
name,
|
|
26203
|
-
current: info
|
|
26204
|
-
latest: info
|
|
26205
|
-
wanted: info
|
|
26206
|
-
|
|
26207
|
-
|
|
26608
|
+
current: str(info["current"]) ?? "unknown",
|
|
26609
|
+
latest: str(info["latest"]) ?? "unknown",
|
|
26610
|
+
wanted: str(info["wanted"]) ?? "unknown",
|
|
26611
|
+
// npm calls it `type`; pnpm calls it `dependencyType`.
|
|
26612
|
+
type: str(info["type"]) ?? str(info["dependencyType"]) ?? "unknown",
|
|
26613
|
+
location: str(info["location"]) ?? name
|
|
26208
26614
|
});
|
|
26209
26615
|
}
|
|
26210
26616
|
} catch {
|
|
26617
|
+
}
|
|
26618
|
+
const outdatedFound = parsedOk && exitCode === 1;
|
|
26619
|
+
let output = normalizeCommandOutput(json2);
|
|
26620
|
+
if (outdatedFound) {
|
|
26621
|
+
output = `${output}
|
|
26622
|
+
|
|
26623
|
+
Note: exit code 1 from \`outdated\` means outdated packages were found (expected); treated as success.`;
|
|
26211
26624
|
}
|
|
26212
26625
|
return {
|
|
26213
|
-
exit_code: exitCode,
|
|
26626
|
+
exit_code: outdatedFound ? 0 : exitCode,
|
|
26214
26627
|
packages,
|
|
26215
26628
|
total: packages.length,
|
|
26216
|
-
output
|
|
26217
|
-
truncated
|
|
26629
|
+
output,
|
|
26630
|
+
truncated
|
|
26218
26631
|
};
|
|
26219
26632
|
}
|
|
26220
26633
|
|
|
@@ -26223,8 +26636,8 @@ init_util();
|
|
|
26223
26636
|
import { spawn as spawn13 } from "node:child_process";
|
|
26224
26637
|
import * as fs28 from "node:fs/promises";
|
|
26225
26638
|
import * as os9 from "node:os";
|
|
26226
|
-
import * as
|
|
26227
|
-
import { buildChildEnv as buildChildEnv8, toErrorMessage as
|
|
26639
|
+
import * as path34 from "node:path";
|
|
26640
|
+
import { buildChildEnv as buildChildEnv8, toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
|
|
26228
26641
|
var patchTool = {
|
|
26229
26642
|
name: "patch",
|
|
26230
26643
|
category: "Filesystem",
|
|
@@ -26268,26 +26681,26 @@ var patchTool = {
|
|
|
26268
26681
|
try {
|
|
26269
26682
|
dir = input.directory ? await safeResolveReal(input.directory, ctx) : ctx.cwd;
|
|
26270
26683
|
} catch (err) {
|
|
26271
|
-
return refuse(`patch refused: ${
|
|
26684
|
+
return refuse(`patch refused: ${toErrorMessage7(err)}`);
|
|
26272
26685
|
}
|
|
26273
|
-
const realRoot = await fs28.realpath(ctx.projectRoot).catch(() =>
|
|
26686
|
+
const realRoot = await fs28.realpath(ctx.projectRoot).catch(() => path34.resolve(ctx.projectRoot));
|
|
26274
26687
|
const targets = extractDiffTargets(input.patch);
|
|
26275
26688
|
const resolvedTargets = [];
|
|
26276
26689
|
for (const t of targets) {
|
|
26277
26690
|
const stripped = stripPathComponents(t.raw, strip);
|
|
26278
26691
|
if (!stripped) continue;
|
|
26279
|
-
if (
|
|
26692
|
+
if (path34.isAbsolute(stripped)) {
|
|
26280
26693
|
return refuse(`patch refused: target "${t.raw}" strips to absolute path`);
|
|
26281
26694
|
}
|
|
26282
|
-
const candidate =
|
|
26695
|
+
const candidate = path34.resolve(dir, stripped);
|
|
26283
26696
|
let real;
|
|
26284
26697
|
try {
|
|
26285
26698
|
real = await resolveRealInsideRoot(candidate, ctx);
|
|
26286
26699
|
} catch (err) {
|
|
26287
|
-
return refuse(`patch refused: target "${t.raw}" ${
|
|
26700
|
+
return refuse(`patch refused: target "${t.raw}" ${toErrorMessage7(err)}`);
|
|
26288
26701
|
}
|
|
26289
|
-
const rel =
|
|
26290
|
-
if (rel.startsWith("..") ||
|
|
26702
|
+
const rel = path34.relative(realRoot, real);
|
|
26703
|
+
if (rel.startsWith("..") || path34.isAbsolute(rel)) {
|
|
26291
26704
|
return refuse(`patch refused: target "${t.raw}" resolves outside project root`);
|
|
26292
26705
|
}
|
|
26293
26706
|
resolvedTargets.push({ raw: t.raw, deleted: t.deleted, abs: real });
|
|
@@ -26301,11 +26714,11 @@ var patchTool = {
|
|
|
26301
26714
|
beforeContents.set(target.abs, await readTextForTracking(target.abs));
|
|
26302
26715
|
}
|
|
26303
26716
|
}
|
|
26304
|
-
const tmpDir = await fs28.mkdtemp(
|
|
26717
|
+
const tmpDir = await fs28.mkdtemp(path34.join(os9.tmpdir(), ".wstack_patch_"));
|
|
26305
26718
|
try {
|
|
26306
26719
|
await fs28.chmod(tmpDir, 448).catch(() => {
|
|
26307
26720
|
});
|
|
26308
|
-
const patchFile =
|
|
26721
|
+
const patchFile = path34.join(tmpDir, "in.diff");
|
|
26309
26722
|
await fs28.writeFile(patchFile, input.patch, { mode: 384 });
|
|
26310
26723
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
26311
26724
|
const result = await runPatch(args, dir, opts.signal, {
|
|
@@ -26318,8 +26731,8 @@ var patchTool = {
|
|
|
26318
26731
|
for (const target of resolvedTargets) {
|
|
26319
26732
|
const abs = target.abs;
|
|
26320
26733
|
const before = beforeContents.get(abs) ?? null;
|
|
26321
|
-
const
|
|
26322
|
-
if (!
|
|
26734
|
+
const stat20 = await fs28.stat(abs).catch(() => null);
|
|
26735
|
+
if (!stat20?.isFile()) {
|
|
26323
26736
|
if (beforeExisted.has(abs)) {
|
|
26324
26737
|
touched.push(abs);
|
|
26325
26738
|
ctx.session?.recordFileChange?.({
|
|
@@ -26334,7 +26747,7 @@ var patchTool = {
|
|
|
26334
26747
|
const after = await readTextForTracking(abs);
|
|
26335
26748
|
if (after === null || after === before) continue;
|
|
26336
26749
|
touched.push(abs);
|
|
26337
|
-
ctx.recordRead?.(abs,
|
|
26750
|
+
ctx.recordRead?.(abs, stat20.mtimeMs, "write", sha256hex(after));
|
|
26338
26751
|
ctx.session?.recordFileChange?.({
|
|
26339
26752
|
path: abs,
|
|
26340
26753
|
action: before === null ? "created" : "modified",
|
|
@@ -26345,7 +26758,7 @@ var patchTool = {
|
|
|
26345
26758
|
}
|
|
26346
26759
|
if (result.exitCode !== 0) {
|
|
26347
26760
|
if (!dryRun) {
|
|
26348
|
-
const partial = touched.length > 0 ? ` ${touched.length} file(s) were still modified on disk and have been recorded for rewind: ${touched.map((p) =>
|
|
26761
|
+
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(", ")}.` : "";
|
|
26349
26762
|
return {
|
|
26350
26763
|
applied: touched.length,
|
|
26351
26764
|
rejected: 1,
|
|
@@ -26353,7 +26766,7 @@ var patchTool = {
|
|
|
26353
26766
|
// success path (which returns GNU patch's dir-relative names).
|
|
26354
26767
|
// `touched` entries are realpaths from resolveRealInsideRoot, and
|
|
26355
26768
|
// realRoot is also a realpath, so path.relative is like-for-like.
|
|
26356
|
-
files: touched.map((p) =>
|
|
26769
|
+
files: touched.map((p) => path34.relative(realRoot, p) || p),
|
|
26357
26770
|
dry_run: dryRun,
|
|
26358
26771
|
message: `patch failed: ${result.stderr || result.stdout}${partial}`
|
|
26359
26772
|
};
|
|
@@ -26369,7 +26782,7 @@ var patchTool = {
|
|
|
26369
26782
|
}
|
|
26370
26783
|
const patched = result.engine === "git" ? [
|
|
26371
26784
|
...new Set(
|
|
26372
|
-
resolvedTargets.map((target) =>
|
|
26785
|
+
resolvedTargets.map((target) => path34.relative(dir, target.abs) || target.abs)
|
|
26373
26786
|
)
|
|
26374
26787
|
] : extractPatchedFiles(result.stdout);
|
|
26375
26788
|
return {
|
|
@@ -26388,8 +26801,8 @@ var patchTool = {
|
|
|
26388
26801
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
26389
26802
|
async function readTextForTracking(absPath) {
|
|
26390
26803
|
try {
|
|
26391
|
-
const
|
|
26392
|
-
if (!
|
|
26804
|
+
const stat20 = await fs28.stat(absPath);
|
|
26805
|
+
if (!stat20.isFile() || stat20.size > MAX_TRACKING_BYTES) return null;
|
|
26393
26806
|
const buf = await fs28.readFile(absPath);
|
|
26394
26807
|
if (buf.includes(0)) return null;
|
|
26395
26808
|
return buf.toString("utf8");
|
|
@@ -26474,7 +26887,7 @@ function runPatch(args, cwd, signal, fallback) {
|
|
|
26474
26887
|
});
|
|
26475
26888
|
}
|
|
26476
26889
|
function runPatchProcess(command, args, cwd, signal) {
|
|
26477
|
-
return new Promise((
|
|
26890
|
+
return new Promise((resolve18) => {
|
|
26478
26891
|
let stdout = "";
|
|
26479
26892
|
let stderr = "";
|
|
26480
26893
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
@@ -26493,11 +26906,11 @@ function runPatchProcess(command, args, cwd, signal) {
|
|
|
26493
26906
|
});
|
|
26494
26907
|
child.on(
|
|
26495
26908
|
"close",
|
|
26496
|
-
(code) =>
|
|
26909
|
+
(code) => resolve18({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
|
|
26497
26910
|
);
|
|
26498
26911
|
child.on(
|
|
26499
26912
|
"error",
|
|
26500
|
-
(e) =>
|
|
26913
|
+
(e) => resolve18({
|
|
26501
26914
|
exitCode: 1,
|
|
26502
26915
|
stdout: "",
|
|
26503
26916
|
stderr: e.message,
|
|
@@ -26875,7 +27288,8 @@ var todoTool = {
|
|
|
26875
27288
|
}
|
|
26876
27289
|
for (const planId of completedPlanIds) {
|
|
26877
27290
|
if (pendingPlanIds.has(planId)) continue;
|
|
26878
|
-
const
|
|
27291
|
+
const meta = ctx.meta;
|
|
27292
|
+
const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
|
|
26879
27293
|
if (typeof planPath !== "string" || !planPath) continue;
|
|
26880
27294
|
try {
|
|
26881
27295
|
const plan = await loadPlan2(planPath);
|
|
@@ -26888,7 +27302,8 @@ var todoTool = {
|
|
|
26888
27302
|
}
|
|
26889
27303
|
for (const taskId of completedTaskIds) {
|
|
26890
27304
|
if (pendingTaskIds.has(taskId)) continue;
|
|
26891
|
-
const
|
|
27305
|
+
const meta = ctx.meta;
|
|
27306
|
+
const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
|
|
26892
27307
|
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
26893
27308
|
try {
|
|
26894
27309
|
const file = await loadTasks3(taskPath);
|
|
@@ -27004,7 +27419,16 @@ var planTool = {
|
|
|
27004
27419
|
sessionPlanPath.lastIndexOf("/"),
|
|
27005
27420
|
sessionPlanPath.lastIndexOf("\\")
|
|
27006
27421
|
);
|
|
27007
|
-
|
|
27422
|
+
if (lastSep < 0) {
|
|
27423
|
+
return {
|
|
27424
|
+
ok: false,
|
|
27425
|
+
message: `Cannot derive the project-scoped plan path: session plan path "${sessionPlanPath}" has no directory component.`,
|
|
27426
|
+
plan: "",
|
|
27427
|
+
count: 0,
|
|
27428
|
+
open: 0
|
|
27429
|
+
};
|
|
27430
|
+
}
|
|
27431
|
+
planPath = sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json";
|
|
27008
27432
|
}
|
|
27009
27433
|
} else {
|
|
27010
27434
|
planPath = sessionPlanPath;
|
|
@@ -27197,6 +27621,7 @@ var planTool = {
|
|
|
27197
27621
|
open: 0
|
|
27198
27622
|
};
|
|
27199
27623
|
}
|
|
27624
|
+
ctx.meta["plan.path.resolved"] = planPath;
|
|
27200
27625
|
await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
|
|
27201
27626
|
if (todosToReplace) {
|
|
27202
27627
|
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
@@ -27229,6 +27654,7 @@ var planTool = {
|
|
|
27229
27654
|
});
|
|
27230
27655
|
return f;
|
|
27231
27656
|
});
|
|
27657
|
+
ctx.meta["task.path.resolved"] = taskPath;
|
|
27232
27658
|
return mkResult(
|
|
27233
27659
|
plan,
|
|
27234
27660
|
true,
|
|
@@ -27262,14 +27688,14 @@ function mkResult(plan, ok, message, todos) {
|
|
|
27262
27688
|
// src/read.ts
|
|
27263
27689
|
init_util();
|
|
27264
27690
|
import * as fs29 from "node:fs/promises";
|
|
27265
|
-
import { FsError, ToolValidationError as
|
|
27266
|
-
import { toErrorMessage as
|
|
27691
|
+
import { FsError, ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
|
|
27692
|
+
import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
|
|
27267
27693
|
var ADVANCED_MODE_META_KEY = "tools.read.advancedMode";
|
|
27268
27694
|
var MAX_BYTES2 = 5 * 1024 * 1024;
|
|
27269
27695
|
var readTool = {
|
|
27270
27696
|
name: "read",
|
|
27271
27697
|
category: "Filesystem",
|
|
27272
|
-
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
|
|
27698
|
+
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).",
|
|
27273
27699
|
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.",
|
|
27274
27700
|
selection: {
|
|
27275
27701
|
doNotUseWhen: "you need to search many files for matching content.",
|
|
@@ -27290,11 +27716,13 @@ var readTool = {
|
|
|
27290
27716
|
},
|
|
27291
27717
|
offset: {
|
|
27292
27718
|
type: "integer",
|
|
27719
|
+
minimum: 1,
|
|
27293
27720
|
description: "1-based starting line number. Use together with `limit` for large files."
|
|
27294
27721
|
},
|
|
27295
27722
|
limit: {
|
|
27296
27723
|
type: "integer",
|
|
27297
|
-
|
|
27724
|
+
minimum: 0,
|
|
27725
|
+
description: "Maximum number of lines to return (default 2000). Values above 5000 are clamped to 5000 \u2014 page with `offset` for more."
|
|
27298
27726
|
},
|
|
27299
27727
|
mode: {
|
|
27300
27728
|
type: "string",
|
|
@@ -27310,16 +27738,16 @@ var readTool = {
|
|
|
27310
27738
|
},
|
|
27311
27739
|
async execute(input, ctx, execOpts) {
|
|
27312
27740
|
if (!input?.path) {
|
|
27313
|
-
throw new
|
|
27741
|
+
throw new ToolValidationError8({
|
|
27314
27742
|
message: "read: path is required",
|
|
27315
27743
|
field: "path"
|
|
27316
27744
|
});
|
|
27317
27745
|
}
|
|
27318
27746
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
27319
27747
|
const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
|
|
27320
|
-
let
|
|
27748
|
+
let stat20;
|
|
27321
27749
|
try {
|
|
27322
|
-
|
|
27750
|
+
stat20 = await fs29.stat(absPath);
|
|
27323
27751
|
} catch (err) {
|
|
27324
27752
|
const code = err.code;
|
|
27325
27753
|
if (code === "ENOENT") {
|
|
@@ -27331,14 +27759,14 @@ var readTool = {
|
|
|
27331
27759
|
});
|
|
27332
27760
|
}
|
|
27333
27761
|
throw new FsError({
|
|
27334
|
-
message: `read: failed to stat "${input.path}": ${
|
|
27762
|
+
message: `read: failed to stat "${input.path}": ${toErrorMessage8(err)}`,
|
|
27335
27763
|
code: "FS_READ_FAILED",
|
|
27336
27764
|
path: absPath,
|
|
27337
27765
|
context: { errno: code },
|
|
27338
27766
|
cause: err
|
|
27339
27767
|
});
|
|
27340
27768
|
}
|
|
27341
|
-
if (!
|
|
27769
|
+
if (!stat20.isFile()) {
|
|
27342
27770
|
throw new FsError({
|
|
27343
27771
|
message: `read: "${input.path}" is not a regular file`,
|
|
27344
27772
|
code: "FS_READ_FAILED",
|
|
@@ -27346,23 +27774,23 @@ var readTool = {
|
|
|
27346
27774
|
context: { reason: "not-a-regular-file" }
|
|
27347
27775
|
});
|
|
27348
27776
|
}
|
|
27349
|
-
if (
|
|
27777
|
+
if (stat20.size > MAX_BYTES2) {
|
|
27350
27778
|
throw new FsError({
|
|
27351
|
-
message: `read: file too large (${
|
|
27779
|
+
message: `read: file too large (${stat20.size} bytes, limit ${MAX_BYTES2})`,
|
|
27352
27780
|
code: "FS_READ_FAILED",
|
|
27353
27781
|
path: absPath,
|
|
27354
|
-
context: { size:
|
|
27782
|
+
context: { size: stat20.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
27355
27783
|
});
|
|
27356
27784
|
}
|
|
27357
27785
|
const offset = Math.max(1, input.offset ?? 1);
|
|
27358
27786
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
27359
27787
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
27360
27788
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
27361
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
27362
|
-
ctx.recordRead(absPath,
|
|
27789
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat20.mtimeMs, offset, requestedEnd)) {
|
|
27790
|
+
ctx.recordRead(absPath, stat20.mtimeMs);
|
|
27363
27791
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27364
27792
|
return {
|
|
27365
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
27793
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat20.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
27366
27794
|
total_lines: prior.totalLines,
|
|
27367
27795
|
encoding: "utf8",
|
|
27368
27796
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -27373,18 +27801,23 @@ var readTool = {
|
|
|
27373
27801
|
}
|
|
27374
27802
|
const buf = await fs29.readFile(absPath);
|
|
27375
27803
|
if (isBinaryBuffer(buf)) {
|
|
27376
|
-
throw new
|
|
27804
|
+
throw new FsError({
|
|
27805
|
+
message: `read: "${input.path}" appears to be binary`,
|
|
27806
|
+
code: "FS_READ_FAILED",
|
|
27807
|
+
path: absPath,
|
|
27808
|
+
context: { reason: "binary" }
|
|
27809
|
+
});
|
|
27377
27810
|
}
|
|
27378
27811
|
const text = buf.toString("utf8");
|
|
27379
27812
|
const contentHash = sha256hex(text);
|
|
27380
27813
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
27381
27814
|
const total = allLines.length;
|
|
27382
27815
|
if (input.mode === "summary") {
|
|
27383
|
-
ctx.recordRead(absPath,
|
|
27384
|
-
rememberReadRange(ctx, absPath,
|
|
27816
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
|
|
27817
|
+
rememberReadRange(ctx, absPath, stat20.mtimeMs, total, 1, Math.min(total, 200));
|
|
27385
27818
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27386
27819
|
return {
|
|
27387
|
-
text: summarizeFile(input.path,
|
|
27820
|
+
text: summarizeFile(input.path, stat20.size, allLines),
|
|
27388
27821
|
total_lines: total,
|
|
27389
27822
|
encoding: "utf8",
|
|
27390
27823
|
truncated: total > 200,
|
|
@@ -27396,8 +27829,8 @@ var readTool = {
|
|
|
27396
27829
|
};
|
|
27397
27830
|
}
|
|
27398
27831
|
if (limit === 0) {
|
|
27399
|
-
ctx.recordRead(absPath,
|
|
27400
|
-
rememberReadRange(ctx, absPath,
|
|
27832
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
|
|
27833
|
+
rememberReadRange(ctx, absPath, stat20.mtimeMs, total, 1, 0);
|
|
27401
27834
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27402
27835
|
return {
|
|
27403
27836
|
text: "",
|
|
@@ -27409,8 +27842,8 @@ var readTool = {
|
|
|
27409
27842
|
};
|
|
27410
27843
|
}
|
|
27411
27844
|
if (offset > total) {
|
|
27412
|
-
ctx.recordRead(absPath,
|
|
27413
|
-
rememberReadRange(ctx, absPath,
|
|
27845
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
|
|
27846
|
+
rememberReadRange(ctx, absPath, stat20.mtimeMs, total, total + 1, total + 1);
|
|
27414
27847
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27415
27848
|
return {
|
|
27416
27849
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
@@ -27425,8 +27858,8 @@ var readTool = {
|
|
|
27425
27858
|
const truncated = offset - 1 + slice.length < total;
|
|
27426
27859
|
const width = String(offset + slice.length - 1).length;
|
|
27427
27860
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
27428
|
-
ctx.recordRead(absPath,
|
|
27429
|
-
rememberReadRange(ctx, absPath,
|
|
27861
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
|
|
27862
|
+
rememberReadRange(ctx, absPath, stat20.mtimeMs, total, offset, offset + slice.length - 1);
|
|
27430
27863
|
const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27431
27864
|
return {
|
|
27432
27865
|
text: numbered,
|
|
@@ -27534,8 +27967,8 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
27534
27967
|
// src/replace.ts
|
|
27535
27968
|
import { spawn as spawn14 } from "node:child_process";
|
|
27536
27969
|
import * as fs30 from "node:fs/promises";
|
|
27537
|
-
import * as
|
|
27538
|
-
import { ToolValidationError as
|
|
27970
|
+
import * as path35 from "node:path";
|
|
27971
|
+
import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
|
|
27539
27972
|
import {
|
|
27540
27973
|
atomicWrite as atomicWrite3,
|
|
27541
27974
|
buildChildEnv as buildChildEnv9,
|
|
@@ -27547,12 +27980,13 @@ import {
|
|
|
27547
27980
|
unifiedDiff as unifiedDiff2
|
|
27548
27981
|
} from "@wrongstack/core/utils";
|
|
27549
27982
|
init_util();
|
|
27983
|
+
var MAX_DIFF_BYTES2 = 262144;
|
|
27550
27984
|
var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
27551
27985
|
var replaceTool = {
|
|
27552
27986
|
name: "replace",
|
|
27553
27987
|
category: "Transform",
|
|
27554
27988
|
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.",
|
|
27555
|
-
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.",
|
|
27989
|
+
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.",
|
|
27556
27990
|
permission: "confirm",
|
|
27557
27991
|
// WS-046: gives permission decisions something to key on.
|
|
27558
27992
|
// The file scope being rewritten, not the pattern: a trust rule should say
|
|
@@ -27562,11 +27996,15 @@ var replaceTool = {
|
|
|
27562
27996
|
capabilities: ["fs.write"],
|
|
27563
27997
|
icon: "edit",
|
|
27564
27998
|
timeoutMs: 3e4,
|
|
27999
|
+
maxOutputBytes: 262144,
|
|
27565
28000
|
inputSchema: {
|
|
27566
28001
|
type: "object",
|
|
27567
28002
|
properties: {
|
|
27568
28003
|
pattern: { type: "string", description: "Regex pattern to match" },
|
|
27569
|
-
replacement: {
|
|
28004
|
+
replacement: {
|
|
28005
|
+
type: "string",
|
|
28006
|
+
description: "Replacement string. Supports `$1`\u2013`$9` (capture groups), `$&` (whole match), and `$$` (literal dollar sign) \u2014 same semantics as JavaScript String.replace."
|
|
28007
|
+
},
|
|
27570
28008
|
files: {
|
|
27571
28009
|
type: "string",
|
|
27572
28010
|
description: "File(s) to target: single path, comma-separated list, or glob pattern"
|
|
@@ -27582,19 +28020,19 @@ var replaceTool = {
|
|
|
27582
28020
|
},
|
|
27583
28021
|
async execute(input, ctx) {
|
|
27584
28022
|
if (!input?.pattern) {
|
|
27585
|
-
throw new
|
|
28023
|
+
throw new ToolValidationError9({
|
|
27586
28024
|
message: "replace: pattern is required",
|
|
27587
28025
|
field: "pattern"
|
|
27588
28026
|
});
|
|
27589
28027
|
}
|
|
27590
28028
|
if (input.replacement === void 0) {
|
|
27591
|
-
throw new
|
|
28029
|
+
throw new ToolValidationError9({
|
|
27592
28030
|
message: "replace: replacement is required",
|
|
27593
28031
|
field: "replacement"
|
|
27594
28032
|
});
|
|
27595
28033
|
}
|
|
27596
28034
|
if (!input?.files) {
|
|
27597
|
-
throw new
|
|
28035
|
+
throw new ToolValidationError9({
|
|
27598
28036
|
message: "replace: files is required",
|
|
27599
28037
|
field: "files"
|
|
27600
28038
|
});
|
|
@@ -27602,7 +28040,7 @@ var replaceTool = {
|
|
|
27602
28040
|
const replaceAll = input.replace_all ?? true;
|
|
27603
28041
|
const compiled = compileUserRegex(input.pattern, "g");
|
|
27604
28042
|
if (!compiled.ok) {
|
|
27605
|
-
throw new
|
|
28043
|
+
throw new ToolValidationError9({
|
|
27606
28044
|
message: `replace: ${compiled.reason}`,
|
|
27607
28045
|
field: "pattern"
|
|
27608
28046
|
});
|
|
@@ -27615,6 +28053,9 @@ var replaceTool = {
|
|
|
27615
28053
|
const realRoot = await fs30.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
27616
28054
|
const results = [];
|
|
27617
28055
|
let totalReplacements = 0;
|
|
28056
|
+
let diffBytesUsed = 0;
|
|
28057
|
+
let diffsOmitted = 0;
|
|
28058
|
+
let diffsTruncated = 0;
|
|
27618
28059
|
for (const absPath of fileList) {
|
|
27619
28060
|
const lstat2 = await fs30.lstat(absPath).catch((err) => {
|
|
27620
28061
|
if (err.code === "ENOENT") return null;
|
|
@@ -27628,10 +28069,10 @@ var replaceTool = {
|
|
|
27628
28069
|
} catch {
|
|
27629
28070
|
continue;
|
|
27630
28071
|
}
|
|
27631
|
-
const rel =
|
|
27632
|
-
if (rel.startsWith("..") ||
|
|
27633
|
-
const
|
|
27634
|
-
if (!
|
|
28072
|
+
const rel = path35.relative(realRoot, realPath);
|
|
28073
|
+
if (rel.startsWith("..") || path35.isAbsolute(rel)) continue;
|
|
28074
|
+
const stat20 = await fs30.stat(realPath).catch(() => null);
|
|
28075
|
+
if (!stat20?.isFile()) continue;
|
|
27635
28076
|
let content;
|
|
27636
28077
|
try {
|
|
27637
28078
|
const buf = await fs30.readFile(realPath);
|
|
@@ -27650,13 +28091,13 @@ var replaceTool = {
|
|
|
27650
28091
|
let newContentLf = contentLf;
|
|
27651
28092
|
for (let i = matches.length - 1; i >= 0; i--) {
|
|
27652
28093
|
const m = expectDefined8(matches[i]);
|
|
27653
|
-
newContentLf = newContentLf.slice(0, m.index) + input.replacement + newContentLf.slice(expectDefined8(m.index) + m[0].length);
|
|
28094
|
+
newContentLf = newContentLf.slice(0, m.index) + expandReplacement(input.replacement, m) + newContentLf.slice(expectDefined8(m.index) + m[0].length);
|
|
27654
28095
|
}
|
|
27655
28096
|
re.lastIndex = 0;
|
|
27656
28097
|
totalReplacements += count;
|
|
27657
28098
|
if (!dryRun) {
|
|
27658
28099
|
const newContent = toStyle2(newContentLf, style);
|
|
27659
|
-
await atomicWrite3(realPath, newContent, { mode:
|
|
28100
|
+
await atomicWrite3(realPath, newContent, { mode: stat20.mode & 511 });
|
|
27660
28101
|
const written = await fs30.stat(realPath).catch(() => null);
|
|
27661
28102
|
if (written) {
|
|
27662
28103
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
@@ -27668,24 +28109,76 @@ var replaceTool = {
|
|
|
27668
28109
|
after: newContent
|
|
27669
28110
|
});
|
|
27670
28111
|
}
|
|
27671
|
-
|
|
28112
|
+
let diff = dryRun || matches.length > 0 ? unifiedDiff2(content, toStyle2(newContentLf, style), {
|
|
27672
28113
|
fromFile: absPath,
|
|
27673
28114
|
toFile: absPath
|
|
27674
28115
|
}) : void 0;
|
|
28116
|
+
if (diff !== void 0) {
|
|
28117
|
+
const remaining = MAX_DIFF_BYTES2 - diffBytesUsed;
|
|
28118
|
+
if (remaining <= 0) {
|
|
28119
|
+
diff = void 0;
|
|
28120
|
+
diffsOmitted++;
|
|
28121
|
+
} else {
|
|
28122
|
+
const capped = truncateDiffPayload(diff, remaining);
|
|
28123
|
+
if (capped.truncated) diffsTruncated++;
|
|
28124
|
+
diff = capped.text;
|
|
28125
|
+
diffBytesUsed += Buffer.byteLength(diff, "utf8");
|
|
28126
|
+
}
|
|
28127
|
+
}
|
|
27675
28128
|
results.push({
|
|
27676
28129
|
path: absPath,
|
|
27677
28130
|
replacements: matches.length,
|
|
27678
28131
|
diff
|
|
27679
28132
|
});
|
|
27680
28133
|
}
|
|
28134
|
+
const overBudget = diffsOmitted > 0 || diffsTruncated > 0;
|
|
27681
28135
|
return {
|
|
27682
28136
|
files_modified: results.length,
|
|
27683
28137
|
total_replacements: totalReplacements,
|
|
27684
28138
|
results,
|
|
27685
|
-
dry_run: dryRun
|
|
28139
|
+
dry_run: dryRun,
|
|
28140
|
+
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
|
|
27686
28141
|
};
|
|
27687
28142
|
}
|
|
27688
28143
|
};
|
|
28144
|
+
function expandReplacement(template, match) {
|
|
28145
|
+
if (!template.includes("$")) return template;
|
|
28146
|
+
let out = "";
|
|
28147
|
+
for (let i = 0; i < template.length; i++) {
|
|
28148
|
+
const ch = template[i];
|
|
28149
|
+
if (ch !== "$") {
|
|
28150
|
+
out += ch;
|
|
28151
|
+
continue;
|
|
28152
|
+
}
|
|
28153
|
+
const next = template[i + 1];
|
|
28154
|
+
if (next === "$") {
|
|
28155
|
+
out += "$";
|
|
28156
|
+
i++;
|
|
28157
|
+
} else if (next === "&") {
|
|
28158
|
+
out += match[0];
|
|
28159
|
+
i++;
|
|
28160
|
+
} else if (next !== void 0 && next >= "1" && next <= "9") {
|
|
28161
|
+
const idx = next.charCodeAt(0) - 48;
|
|
28162
|
+
if (idx < match.length) {
|
|
28163
|
+
out += match[idx] ?? "";
|
|
28164
|
+
i++;
|
|
28165
|
+
} else {
|
|
28166
|
+
out += "$";
|
|
28167
|
+
}
|
|
28168
|
+
} else {
|
|
28169
|
+
out += "$";
|
|
28170
|
+
}
|
|
28171
|
+
}
|
|
28172
|
+
return out;
|
|
28173
|
+
}
|
|
28174
|
+
function passesExtraGlob(extraGlob, name, full) {
|
|
28175
|
+
extraGlob.lastIndex = 0;
|
|
28176
|
+
const nameMatch = extraGlob.test(name);
|
|
28177
|
+
extraGlob.lastIndex = 0;
|
|
28178
|
+
const fullMatch = extraGlob.test(full);
|
|
28179
|
+
extraGlob.lastIndex = 0;
|
|
28180
|
+
return nameMatch || fullMatch;
|
|
28181
|
+
}
|
|
27689
28182
|
async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
27690
28183
|
const base = ctx.cwd;
|
|
27691
28184
|
const normalized = filesInput.trim();
|
|
@@ -27696,8 +28189,9 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
27696
28189
|
const resolved = [];
|
|
27697
28190
|
for (const p of parts) {
|
|
27698
28191
|
const absPath = safeResolve(p, ctx);
|
|
27699
|
-
|
|
27700
|
-
|
|
28192
|
+
if (extraGlob && !passesExtraGlob(extraGlob, path35.basename(absPath), absPath)) continue;
|
|
28193
|
+
const stat20 = await fs30.stat(absPath).catch(() => null);
|
|
28194
|
+
if (stat20?.isFile()) {
|
|
27701
28195
|
resolved.push(absPath);
|
|
27702
28196
|
}
|
|
27703
28197
|
}
|
|
@@ -27708,26 +28202,32 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
27708
28202
|
if (rgAvailable) {
|
|
27709
28203
|
try {
|
|
27710
28204
|
const { promise } = spawnRgFind(pattern, base);
|
|
27711
|
-
|
|
28205
|
+
const files = await promise;
|
|
28206
|
+
if (extraGlob) {
|
|
28207
|
+
return files.filter((f) => passesExtraGlob(extraGlob, path35.basename(f), f));
|
|
28208
|
+
}
|
|
28209
|
+
return files;
|
|
27712
28210
|
} catch {
|
|
27713
28211
|
}
|
|
27714
28212
|
}
|
|
27715
28213
|
return await globNative(pattern, base, extraGlob);
|
|
27716
28214
|
}
|
|
28215
|
+
var rgAvailabilityCache2;
|
|
27717
28216
|
function checkRg() {
|
|
27718
|
-
|
|
28217
|
+
rgAvailabilityCache2 ??= new Promise((resolve18) => {
|
|
27719
28218
|
try {
|
|
27720
28219
|
const p = spawn14("rg", ["--version"], {
|
|
27721
28220
|
env: buildChildEnv9(),
|
|
27722
28221
|
stdio: "ignore",
|
|
27723
28222
|
windowsHide: true
|
|
27724
28223
|
});
|
|
27725
|
-
p.on("error", () =>
|
|
27726
|
-
p.on("close", (code) =>
|
|
28224
|
+
p.on("error", () => resolve18(false));
|
|
28225
|
+
p.on("close", (code) => resolve18(code === 0));
|
|
27727
28226
|
} catch {
|
|
27728
|
-
|
|
28227
|
+
resolve18(false);
|
|
27729
28228
|
}
|
|
27730
28229
|
});
|
|
28230
|
+
return rgAvailabilityCache2;
|
|
27731
28231
|
}
|
|
27732
28232
|
function spawnRgFind(pattern, base) {
|
|
27733
28233
|
const args = ["--files", "--glob", pattern, base];
|
|
@@ -27750,10 +28250,10 @@ function spawnRgFind(pattern, base) {
|
|
|
27750
28250
|
}
|
|
27751
28251
|
});
|
|
27752
28252
|
return {
|
|
27753
|
-
promise: new Promise((
|
|
28253
|
+
promise: new Promise((resolve18, reject) => {
|
|
27754
28254
|
child.on("error", reject);
|
|
27755
28255
|
child.on("close", () => {
|
|
27756
|
-
|
|
28256
|
+
resolve18(buf.split("\n").filter(Boolean));
|
|
27757
28257
|
});
|
|
27758
28258
|
})
|
|
27759
28259
|
};
|
|
@@ -27770,10 +28270,10 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
27770
28270
|
}
|
|
27771
28271
|
for (const e of entries) {
|
|
27772
28272
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
27773
|
-
const full =
|
|
28273
|
+
const full = path35.join(dir, e.name);
|
|
27774
28274
|
try {
|
|
27775
|
-
const
|
|
27776
|
-
if (
|
|
28275
|
+
const stat20 = await fs30.lstat(full);
|
|
28276
|
+
if (stat20.isSymbolicLink()) continue;
|
|
27777
28277
|
} catch {
|
|
27778
28278
|
continue;
|
|
27779
28279
|
}
|
|
@@ -27797,7 +28297,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
27797
28297
|
// src/scaffold.ts
|
|
27798
28298
|
init_util();
|
|
27799
28299
|
import * as fs31 from "node:fs/promises";
|
|
27800
|
-
import * as
|
|
28300
|
+
import * as path36 from "node:path";
|
|
27801
28301
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
|
|
27802
28302
|
var BUILT_IN_TEMPLATES = {
|
|
27803
28303
|
"npm-package": {
|
|
@@ -27948,16 +28448,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
27948
28448
|
let filesCreated = 0;
|
|
27949
28449
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
27950
28450
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
27951
|
-
const joinedPath =
|
|
27952
|
-
const root =
|
|
27953
|
-
const target =
|
|
27954
|
-
const rel =
|
|
27955
|
-
if (rel.startsWith("..") ||
|
|
28451
|
+
const joinedPath = path36.join(cwd, resolvedPath);
|
|
28452
|
+
const root = path36.resolve(ctx.projectRoot);
|
|
28453
|
+
const target = path36.resolve(joinedPath);
|
|
28454
|
+
const rel = path36.relative(root, target);
|
|
28455
|
+
if (rel.startsWith("..") || path36.isAbsolute(rel)) {
|
|
27956
28456
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
27957
28457
|
}
|
|
27958
28458
|
const fullPath = target;
|
|
27959
28459
|
if (!dryRun) {
|
|
27960
|
-
await fs31.mkdir(
|
|
28460
|
+
await fs31.mkdir(path36.dirname(fullPath), { recursive: true });
|
|
27961
28461
|
await atomicWrite4(fullPath, substituteVars(content, name, vars));
|
|
27962
28462
|
}
|
|
27963
28463
|
files.push(resolvedPath);
|
|
@@ -27988,11 +28488,12 @@ function substituteVars(content, name, vars) {
|
|
|
27988
28488
|
}
|
|
27989
28489
|
|
|
27990
28490
|
// src/search.ts
|
|
27991
|
-
import { FetchError as FetchError3, ToolValidationError as
|
|
28491
|
+
import { FetchError as FetchError3, ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
|
|
27992
28492
|
import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
|
|
27993
|
-
import { toErrorMessage as
|
|
28493
|
+
import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
|
|
27994
28494
|
var DEFAULT_NUM = 10;
|
|
27995
28495
|
var MAX_RESULTS = 50;
|
|
28496
|
+
var MAX_SNIPPET_CHARS = 300;
|
|
27996
28497
|
var TIMEOUT_MS3 = 15e3;
|
|
27997
28498
|
var CACHE_TTL_MS = 3e5;
|
|
27998
28499
|
var CACHE_MAX_ENTRIES = 200;
|
|
@@ -28000,7 +28501,7 @@ var cache = /* @__PURE__ */ new Map();
|
|
|
28000
28501
|
var searchTool = {
|
|
28001
28502
|
name: "search",
|
|
28002
28503
|
category: "Search",
|
|
28003
|
-
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.",
|
|
28504
|
+
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.",
|
|
28004
28505
|
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.",
|
|
28005
28506
|
permission: "auto",
|
|
28006
28507
|
mutating: false,
|
|
@@ -28041,7 +28542,7 @@ var searchTool = {
|
|
|
28041
28542
|
},
|
|
28042
28543
|
async *executeStream(input, _ctx, opts) {
|
|
28043
28544
|
if (!input?.query || input.query.trim() === "") {
|
|
28044
|
-
throw new
|
|
28545
|
+
throw new ToolValidationError10({
|
|
28045
28546
|
message: "search: query is required and must be a non-empty string",
|
|
28046
28547
|
field: "query"
|
|
28047
28548
|
});
|
|
@@ -28074,7 +28575,7 @@ var searchTool = {
|
|
|
28074
28575
|
query: input.query,
|
|
28075
28576
|
results: results.slice(0, num),
|
|
28076
28577
|
source: entry.source,
|
|
28077
|
-
truncated: results.length
|
|
28578
|
+
truncated: results.length > num,
|
|
28078
28579
|
cached: true
|
|
28079
28580
|
}
|
|
28080
28581
|
};
|
|
@@ -28086,41 +28587,45 @@ var searchTool = {
|
|
|
28086
28587
|
text: `Querying ${source} for "${input.query}"\u2026`,
|
|
28087
28588
|
data: { source, query: input.query, cached: false }
|
|
28088
28589
|
};
|
|
28089
|
-
let
|
|
28590
|
+
let engine;
|
|
28090
28591
|
let effectiveSource = source;
|
|
28091
28592
|
switch (source) {
|
|
28092
28593
|
case "duckduckgo":
|
|
28093
|
-
|
|
28594
|
+
engine = await duckduckgoSearch(input.query, opts.signal);
|
|
28094
28595
|
break;
|
|
28095
28596
|
case "google":
|
|
28096
|
-
|
|
28597
|
+
engine = await googleSearch(input.query, opts.signal);
|
|
28097
28598
|
break;
|
|
28098
28599
|
case "bing":
|
|
28099
|
-
|
|
28600
|
+
engine = await bingSearch(input.query, opts.signal);
|
|
28100
28601
|
break;
|
|
28101
28602
|
default:
|
|
28102
|
-
throw new
|
|
28603
|
+
throw new ToolValidationError10({
|
|
28103
28604
|
message: `search: unknown source "${source}"`,
|
|
28104
28605
|
field: "source"
|
|
28105
28606
|
});
|
|
28106
28607
|
}
|
|
28107
|
-
let ranked = rankSearchResults(
|
|
28608
|
+
let ranked = rankSearchResults(engine.results, input.query);
|
|
28609
|
+
let engineError = engine.error;
|
|
28108
28610
|
if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
|
|
28109
28611
|
yield {
|
|
28110
28612
|
type: "log",
|
|
28111
28613
|
text: `${source} returned no relevant static results; falling back to duckduckgo`,
|
|
28112
28614
|
data: { source, fallback: "duckduckgo", query: input.query }
|
|
28113
28615
|
};
|
|
28114
|
-
|
|
28115
|
-
ranked = rankSearchResults(
|
|
28616
|
+
const fallback = await duckduckgoSearch(input.query, opts.signal);
|
|
28617
|
+
ranked = rankSearchResults(fallback.results, input.query);
|
|
28618
|
+
engineError = fallback.error;
|
|
28116
28619
|
effectiveSource = "duckduckgo";
|
|
28117
28620
|
}
|
|
28118
28621
|
const finalResults = ranked.slice(0, num);
|
|
28119
|
-
|
|
28120
|
-
|
|
28622
|
+
if (!engineError) {
|
|
28623
|
+
cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
|
|
28624
|
+
pruneCacheEntries();
|
|
28625
|
+
}
|
|
28121
28626
|
yield {
|
|
28122
28627
|
type: "partial_output",
|
|
28123
|
-
text: `${finalResults.length} results from ${effectiveSource}`,
|
|
28628
|
+
text: engineError ? `search failed: ${engineError}` : `${finalResults.length} results from ${effectiveSource}`,
|
|
28124
28629
|
data: { count: finalResults.length, cached: false, source: effectiveSource }
|
|
28125
28630
|
};
|
|
28126
28631
|
yield {
|
|
@@ -28133,8 +28638,9 @@ var searchTool = {
|
|
|
28133
28638
|
snippet: r.snippet
|
|
28134
28639
|
})),
|
|
28135
28640
|
source: effectiveSource,
|
|
28136
|
-
truncated:
|
|
28137
|
-
cached: false
|
|
28641
|
+
truncated: ranked.length > num,
|
|
28642
|
+
cached: false,
|
|
28643
|
+
...engineError ? { error: engineError } : {}
|
|
28138
28644
|
}
|
|
28139
28645
|
};
|
|
28140
28646
|
}
|
|
@@ -28185,18 +28691,18 @@ function shouldFallbackToDuckDuckGo(results, query) {
|
|
|
28185
28691
|
return terms.some((term) => haystack.includes(term));
|
|
28186
28692
|
});
|
|
28187
28693
|
}
|
|
28188
|
-
async function duckduckgoSearch(query,
|
|
28694
|
+
async function duckduckgoSearch(query, signal) {
|
|
28189
28695
|
const encoded = encodeURIComponent(query);
|
|
28190
28696
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
28191
28697
|
try {
|
|
28192
28698
|
const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
|
|
28193
28699
|
const html = await response.text();
|
|
28194
|
-
return parseDuckDuckGo(html,
|
|
28700
|
+
return { results: parseDuckDuckGo(html, MAX_RESULTS) };
|
|
28195
28701
|
} catch (err) {
|
|
28196
28702
|
console.log(
|
|
28197
|
-
JSON.stringify({ level: "debug", event: "search_failed", query, error:
|
|
28703
|
+
JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage9(err) })
|
|
28198
28704
|
);
|
|
28199
|
-
return
|
|
28705
|
+
return { results: [], error: `duckduckgo unreachable: ${toErrorMessage9(err)}` };
|
|
28200
28706
|
}
|
|
28201
28707
|
}
|
|
28202
28708
|
function takeFrom(iter, max) {
|
|
@@ -28230,7 +28736,7 @@ function parseDuckDuckGo(html, num) {
|
|
|
28230
28736
|
results.push({
|
|
28231
28737
|
title: entry.title ?? "",
|
|
28232
28738
|
url: entry.url ?? "",
|
|
28233
|
-
snippet: snippetMatches[i] ?? "",
|
|
28739
|
+
snippet: capSnippet(snippetMatches[i] ?? ""),
|
|
28234
28740
|
score: 1
|
|
28235
28741
|
});
|
|
28236
28742
|
}
|
|
@@ -28255,11 +28761,15 @@ function normalizeDuckDuckGoUrl(raw) {
|
|
|
28255
28761
|
return raw;
|
|
28256
28762
|
}
|
|
28257
28763
|
}
|
|
28258
|
-
async function googleSearch(query,
|
|
28764
|
+
async function googleSearch(query, signal) {
|
|
28259
28765
|
const encoded = encodeURIComponent(query);
|
|
28260
28766
|
const url = `https://www.google.com/search?q=${encoded}&hl=en`;
|
|
28261
|
-
|
|
28262
|
-
|
|
28767
|
+
try {
|
|
28768
|
+
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
|
|
28769
|
+
return { results: parseGoogleResults(html, MAX_RESULTS) };
|
|
28770
|
+
} catch (err) {
|
|
28771
|
+
return { results: [], error: `google unreachable: ${toErrorMessage9(err)}` };
|
|
28772
|
+
}
|
|
28263
28773
|
}
|
|
28264
28774
|
function parseGoogleResults(html, num) {
|
|
28265
28775
|
const results = [];
|
|
@@ -28282,17 +28792,21 @@ function parseGoogleResults(html, num) {
|
|
|
28282
28792
|
results.push({
|
|
28283
28793
|
title: titles[i] ?? "",
|
|
28284
28794
|
url: urls[i] ?? "",
|
|
28285
|
-
snippet: snippets[i] ?? "",
|
|
28795
|
+
snippet: capSnippet(snippets[i] ?? ""),
|
|
28286
28796
|
score: 1
|
|
28287
28797
|
});
|
|
28288
28798
|
}
|
|
28289
28799
|
return results;
|
|
28290
28800
|
}
|
|
28291
|
-
async function bingSearch(query,
|
|
28801
|
+
async function bingSearch(query, signal) {
|
|
28292
28802
|
const encoded = encodeURIComponent(query);
|
|
28293
28803
|
const url = `https://www.bing.com/search?q=${encoded}`;
|
|
28294
|
-
|
|
28295
|
-
|
|
28804
|
+
try {
|
|
28805
|
+
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
|
|
28806
|
+
return { results: parseBingResults(html, MAX_RESULTS) };
|
|
28807
|
+
} catch (err) {
|
|
28808
|
+
return { results: [], error: `bing unreachable: ${toErrorMessage9(err)}` };
|
|
28809
|
+
}
|
|
28296
28810
|
}
|
|
28297
28811
|
function parseBingResults(html, num) {
|
|
28298
28812
|
const results = [];
|
|
@@ -28305,7 +28819,7 @@ function parseBingResults(html, num) {
|
|
|
28305
28819
|
const title = stripTags(expectDefined9(titleMatch[2]));
|
|
28306
28820
|
if (!href || !title) return [];
|
|
28307
28821
|
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);
|
|
28308
|
-
const snippet = snippetMatch ? stripTags(expectDefined9(snippetMatch.at(-1))) : "";
|
|
28822
|
+
const snippet = snippetMatch ? capSnippet(stripTags(expectDefined9(snippetMatch.at(-1)))) : "";
|
|
28309
28823
|
return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
|
|
28310
28824
|
}), num);
|
|
28311
28825
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -28372,17 +28886,20 @@ function anySignal(...signals) {
|
|
|
28372
28886
|
function stripTags(html) {
|
|
28373
28887
|
return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
|
|
28374
28888
|
}
|
|
28889
|
+
function capSnippet(snippet) {
|
|
28890
|
+
return snippet.length > MAX_SNIPPET_CHARS ? `${snippet.slice(0, MAX_SNIPPET_CHARS - 1)}\u2026` : snippet;
|
|
28891
|
+
}
|
|
28375
28892
|
function decodeHtmlEntities(text) {
|
|
28376
28893
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
28377
28894
|
}
|
|
28378
28895
|
|
|
28379
28896
|
// src/set-working-dir.ts
|
|
28380
28897
|
import * as fs32 from "node:fs/promises";
|
|
28381
|
-
import { toErrorMessage as
|
|
28898
|
+
import { toErrorMessage as toErrorMessage10 } from "@wrongstack/core/utils";
|
|
28382
28899
|
var setWorkingDirTool = {
|
|
28383
28900
|
name: "set_working_dir",
|
|
28384
28901
|
category: "Context",
|
|
28385
|
-
description: "Change the current working directory for
|
|
28902
|
+
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.",
|
|
28386
28903
|
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.",
|
|
28387
28904
|
permission: "confirm",
|
|
28388
28905
|
mutating: true,
|
|
@@ -28412,19 +28929,23 @@ var setWorkingDirTool = {
|
|
|
28412
28929
|
} catch (err) {
|
|
28413
28930
|
return {
|
|
28414
28931
|
current: ctx.workingDir,
|
|
28415
|
-
error:
|
|
28932
|
+
error: toErrorMessage10(err)
|
|
28416
28933
|
};
|
|
28417
28934
|
}
|
|
28935
|
+
let isDirectory = false;
|
|
28418
28936
|
try {
|
|
28419
|
-
await fs32.
|
|
28937
|
+
isDirectory = (await fs32.stat(resolved)).isDirectory();
|
|
28420
28938
|
} catch {
|
|
28939
|
+
isDirectory = false;
|
|
28940
|
+
}
|
|
28941
|
+
if (!isDirectory) {
|
|
28421
28942
|
try {
|
|
28422
28943
|
ctx.setWorkingDir(previous);
|
|
28423
28944
|
} catch {
|
|
28424
28945
|
}
|
|
28425
28946
|
return {
|
|
28426
28947
|
current: ctx.workingDir,
|
|
28427
|
-
error: `Directory does not exist: ${resolved}`
|
|
28948
|
+
error: `Directory does not exist (or is not a directory): ${resolved}`
|
|
28428
28949
|
};
|
|
28429
28950
|
}
|
|
28430
28951
|
return {
|
|
@@ -28879,6 +29400,7 @@ var taskTool = {
|
|
|
28879
29400
|
inProgress: 0
|
|
28880
29401
|
};
|
|
28881
29402
|
}
|
|
29403
|
+
ctx.meta["task.path.resolved"] = taskPath;
|
|
28882
29404
|
if (todosToReplace) {
|
|
28883
29405
|
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
28884
29406
|
signal: AbortSignal.timeout(3e4)
|
|
@@ -28903,6 +29425,7 @@ var taskTool = {
|
|
|
28903
29425
|
formatted = formatPlan2(updated);
|
|
28904
29426
|
return updated;
|
|
28905
29427
|
});
|
|
29428
|
+
ctx.meta["plan.path.resolved"] = planPath;
|
|
28906
29429
|
} catch (err) {
|
|
28907
29430
|
return {
|
|
28908
29431
|
ok: false,
|
|
@@ -28946,7 +29469,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
|
|
|
28946
29469
|
init_spawn_stream();
|
|
28947
29470
|
init_util();
|
|
28948
29471
|
init_legacy_bridge();
|
|
28949
|
-
import * as
|
|
29472
|
+
import * as path37 from "node:path";
|
|
28950
29473
|
var testTool = {
|
|
28951
29474
|
name: "test",
|
|
28952
29475
|
category: "Code Quality",
|
|
@@ -29049,11 +29572,11 @@ var testTool = {
|
|
|
29049
29572
|
}
|
|
29050
29573
|
};
|
|
29051
29574
|
async function detectRunner(cwd) {
|
|
29052
|
-
const { stat:
|
|
29575
|
+
const { stat: stat20 } = await import("node:fs/promises");
|
|
29053
29576
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
29054
29577
|
for (const f of candidates) {
|
|
29055
29578
|
try {
|
|
29056
|
-
await
|
|
29579
|
+
await stat20(path37.join(cwd, f));
|
|
29057
29580
|
if (f.includes("vitest")) return "vitest";
|
|
29058
29581
|
if (f.includes("jest")) return "jest";
|
|
29059
29582
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -29431,7 +29954,7 @@ var toolUseTool = {
|
|
|
29431
29954
|
// src/tree.ts
|
|
29432
29955
|
init_util();
|
|
29433
29956
|
import * as fs33 from "node:fs/promises";
|
|
29434
|
-
import * as
|
|
29957
|
+
import * as path38 from "node:path";
|
|
29435
29958
|
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
29436
29959
|
var DEFAULT_IGNORE5 = /* @__PURE__ */ new Set([
|
|
29437
29960
|
...DEFAULT_WALK_IGNORE_DIRS4,
|
|
@@ -29451,6 +29974,7 @@ var treeTool = {
|
|
|
29451
29974
|
mutating: false,
|
|
29452
29975
|
capabilities: ["fs.read"],
|
|
29453
29976
|
icon: "tree",
|
|
29977
|
+
maxOutputBytes: 262144,
|
|
29454
29978
|
timeoutMs: 15e3,
|
|
29455
29979
|
inputSchema: {
|
|
29456
29980
|
type: "object",
|
|
@@ -29599,17 +30123,15 @@ async function walkDir(dir, depth, opts) {
|
|
|
29599
30123
|
if (opts.exclude.has(e.name)) return false;
|
|
29600
30124
|
return true;
|
|
29601
30125
|
});
|
|
29602
|
-
|
|
29603
|
-
|
|
29604
|
-
|
|
29605
|
-
|
|
29606
|
-
|
|
29607
|
-
|
|
29608
|
-
|
|
29609
|
-
|
|
29610
|
-
|
|
29611
|
-
opts.onProgress?.();
|
|
29612
|
-
}
|
|
30126
|
+
let dirCount = 0;
|
|
30127
|
+
let fileCount = 0;
|
|
30128
|
+
for (const e of filtered) {
|
|
30129
|
+
if (e.isDirectory()) dirCount++;
|
|
30130
|
+
else if (e.isFile()) fileCount++;
|
|
30131
|
+
}
|
|
30132
|
+
opts.totalDirs.value += dirCount;
|
|
30133
|
+
opts.totalFiles.value += fileCount;
|
|
30134
|
+
opts.onProgress?.();
|
|
29613
30135
|
const items = filtered.sort((a, b) => {
|
|
29614
30136
|
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
29615
30137
|
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
@@ -29637,7 +30159,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
29637
30159
|
opts.retention.outputBytes += lineBytes;
|
|
29638
30160
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
29639
30161
|
const childPrefix = opts.prefix + connector;
|
|
29640
|
-
await walkDir(
|
|
30162
|
+
await walkDir(path38.join(dir, entry.name), depth + 1, {
|
|
29641
30163
|
...opts,
|
|
29642
30164
|
prefix: childPrefix,
|
|
29643
30165
|
isLast
|
|
@@ -29650,7 +30172,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
29650
30172
|
init_spawn_stream();
|
|
29651
30173
|
init_util();
|
|
29652
30174
|
init_legacy_bridge();
|
|
29653
|
-
import * as
|
|
30175
|
+
import * as path39 from "node:path";
|
|
29654
30176
|
var typecheckTool = {
|
|
29655
30177
|
name: "typecheck",
|
|
29656
30178
|
category: "Code Quality",
|
|
@@ -29672,11 +30194,7 @@ var typecheckTool = {
|
|
|
29672
30194
|
},
|
|
29673
30195
|
all: {
|
|
29674
30196
|
type: "boolean",
|
|
29675
|
-
description: "Type-check all
|
|
29676
|
-
},
|
|
29677
|
-
json: {
|
|
29678
|
-
type: "boolean",
|
|
29679
|
-
description: "Emit JSON output from tsc (default: false)"
|
|
30197
|
+
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)"
|
|
29680
30198
|
}
|
|
29681
30199
|
}
|
|
29682
30200
|
},
|
|
@@ -29712,29 +30230,42 @@ var typecheckTool = {
|
|
|
29712
30230
|
};
|
|
29713
30231
|
return;
|
|
29714
30232
|
}
|
|
29715
|
-
let
|
|
30233
|
+
let cmd;
|
|
30234
|
+
let cmdArgs;
|
|
29716
30235
|
let project;
|
|
29717
30236
|
if (input.all) {
|
|
29718
|
-
args = ["--noEmit"];
|
|
29719
30237
|
project = "workspace";
|
|
30238
|
+
const tscArgs = ["--noEmit"];
|
|
30239
|
+
if (input.strict) tscArgs.push("--strict");
|
|
30240
|
+
const manager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
30241
|
+
if (manager === "pnpm") {
|
|
30242
|
+
cmd = "pnpm";
|
|
30243
|
+
cmdArgs = ["-r", "--no-bail", "exec", "tsc", ...tscArgs];
|
|
30244
|
+
} else {
|
|
30245
|
+
cmd = "npx";
|
|
30246
|
+
cmdArgs = ["tsc", ...tscArgs];
|
|
30247
|
+
}
|
|
29720
30248
|
} else {
|
|
29721
30249
|
const tsconfig = input.project ? safeResolve(input.project, ctx) : await findTsConfig(cwd);
|
|
29722
|
-
|
|
29723
|
-
if (input.strict)
|
|
29724
|
-
if (tsconfig)
|
|
30250
|
+
const tscArgs = ["--noEmit"];
|
|
30251
|
+
if (input.strict) tscArgs.push("--strict");
|
|
30252
|
+
if (tsconfig) tscArgs.push("--project", tsconfig);
|
|
29725
30253
|
project = tsconfig ?? "default";
|
|
30254
|
+
cmd = "npx";
|
|
30255
|
+
cmdArgs = ["tsc", ...tscArgs];
|
|
29726
30256
|
}
|
|
29727
|
-
|
|
29728
|
-
yield { type: "log", text: `tsc ${args.join(" ")}`, data: { project } };
|
|
30257
|
+
yield { type: "log", text: `${cmd} ${cmdArgs.join(" ")}`, data: { project } };
|
|
29729
30258
|
const result = yield* spawnStream({
|
|
29730
|
-
cmd
|
|
29731
|
-
args:
|
|
30259
|
+
cmd,
|
|
30260
|
+
args: cmdArgs,
|
|
29732
30261
|
cwd,
|
|
29733
30262
|
signal: opts.signal,
|
|
29734
30263
|
maxBytes: 2e5
|
|
29735
30264
|
});
|
|
29736
|
-
const
|
|
29737
|
-
|
|
30265
|
+
const combined = `${result.stdout}
|
|
30266
|
+
${result.stderr}`;
|
|
30267
|
+
const errors = [...combined.matchAll(/^.*\berror TS\d+:/gm)].length;
|
|
30268
|
+
const warnings = [...combined.matchAll(/^.*\bwarning TS\d+:/gm)].length;
|
|
29738
30269
|
yield {
|
|
29739
30270
|
type: "final",
|
|
29740
30271
|
output: {
|
|
@@ -29749,12 +30280,12 @@ var typecheckTool = {
|
|
|
29749
30280
|
}
|
|
29750
30281
|
};
|
|
29751
30282
|
async function findTsConfig(cwd) {
|
|
29752
|
-
const { stat:
|
|
30283
|
+
const { stat: stat20 } = await import("node:fs/promises");
|
|
29753
30284
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
29754
30285
|
for (const f of candidates) {
|
|
29755
30286
|
try {
|
|
29756
|
-
const s = await
|
|
29757
|
-
if (s.isFile()) return
|
|
30287
|
+
const s = await stat20(path39.join(cwd, f));
|
|
30288
|
+
if (s.isFile()) return path39.join(cwd, f);
|
|
29758
30289
|
} catch {
|
|
29759
30290
|
}
|
|
29760
30291
|
}
|
|
@@ -29763,21 +30294,32 @@ async function findTsConfig(cwd) {
|
|
|
29763
30294
|
|
|
29764
30295
|
// src/write.ts
|
|
29765
30296
|
import * as fs34 from "node:fs/promises";
|
|
29766
|
-
import { ToolValidationError as
|
|
29767
|
-
import {
|
|
30297
|
+
import { ToolValidationError as ToolValidationError11 } from "@wrongstack/core/types";
|
|
30298
|
+
import {
|
|
30299
|
+
atomicWrite as atomicWrite5,
|
|
30300
|
+
detectNewlineStyle as detectNewlineStyle3,
|
|
30301
|
+
normalizeToLf as normalizeToLf3,
|
|
30302
|
+
toStyle as toStyle3,
|
|
30303
|
+
unifiedDiff as unifiedDiff3
|
|
30304
|
+
} from "@wrongstack/core/utils";
|
|
29768
30305
|
init_util();
|
|
30306
|
+
var MAX_DIFF_BYTES3 = 262144;
|
|
29769
30307
|
var writeTool = {
|
|
29770
30308
|
name: "write",
|
|
29771
30309
|
category: "Filesystem",
|
|
29772
30310
|
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.",
|
|
29773
|
-
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-
|
|
30311
|
+
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.",
|
|
29774
30312
|
selection: {
|
|
29775
30313
|
doNotUseWhen: "making a precise change to part of an existing file.",
|
|
29776
30314
|
useInstead: ["edit"]
|
|
29777
30315
|
},
|
|
29778
30316
|
permission: "confirm",
|
|
30317
|
+
// WS-046: gives permission decisions something to key on — the file being
|
|
30318
|
+
// written, so trust rules can scope by path.
|
|
30319
|
+
subjectKey: "path",
|
|
29779
30320
|
mutating: true,
|
|
29780
30321
|
timeoutMs: 5e3,
|
|
30322
|
+
maxOutputBytes: 262144,
|
|
29781
30323
|
capabilities: ["fs.write"],
|
|
29782
30324
|
icon: "file",
|
|
29783
30325
|
inputSchema: {
|
|
@@ -29813,13 +30355,13 @@ async function writeFile6(input, ctx, signal) {
|
|
|
29813
30355
|
}
|
|
29814
30356
|
async function prepareWrite(input, ctx) {
|
|
29815
30357
|
if (!input?.path) {
|
|
29816
|
-
throw new
|
|
30358
|
+
throw new ToolValidationError11({
|
|
29817
30359
|
message: "write: path is required",
|
|
29818
30360
|
field: "path"
|
|
29819
30361
|
});
|
|
29820
30362
|
}
|
|
29821
30363
|
if (input.content === void 0) {
|
|
29822
|
-
throw new
|
|
30364
|
+
throw new ToolValidationError11({
|
|
29823
30365
|
message: "write: content is required",
|
|
29824
30366
|
field: "content"
|
|
29825
30367
|
});
|
|
@@ -29828,12 +30370,12 @@ async function prepareWrite(input, ctx) {
|
|
|
29828
30370
|
let existed = false;
|
|
29829
30371
|
let prev = "";
|
|
29830
30372
|
try {
|
|
29831
|
-
const
|
|
29832
|
-
existed =
|
|
30373
|
+
const stat20 = await fs34.stat(absPath);
|
|
30374
|
+
existed = stat20.isFile();
|
|
29833
30375
|
if (existed) {
|
|
29834
30376
|
if (!ctx.hasRead(absPath)) {
|
|
29835
30377
|
prev = await fs34.readFile(absPath, "utf8");
|
|
29836
|
-
ctx.recordRead(absPath,
|
|
30378
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "write", sha256hex(prev));
|
|
29837
30379
|
} else {
|
|
29838
30380
|
prev = await fs34.readFile(absPath, "utf8");
|
|
29839
30381
|
}
|
|
@@ -29846,31 +30388,42 @@ async function prepareWrite(input, ctx) {
|
|
|
29846
30388
|
return { absPath, existed, prev };
|
|
29847
30389
|
}
|
|
29848
30390
|
async function finishWrite(input, ctx, prepared, signal) {
|
|
30391
|
+
const content = prepared.existed ? toStyle3(normalizeToLf3(input.content), detectNewlineStyle3(prepared.prev)) : input.content;
|
|
29849
30392
|
signal?.throwIfAborted();
|
|
29850
|
-
await atomicWrite5(prepared.absPath,
|
|
29851
|
-
const
|
|
29852
|
-
+ (new file, ${
|
|
29853
|
-
const
|
|
29854
|
-
|
|
30393
|
+
await atomicWrite5(prepared.absPath, content);
|
|
30394
|
+
const rawDiff = prepared.existed ? unifiedDiff3(prepared.prev, content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
30395
|
+
+ (new file, ${content.split("\n").length} lines)`;
|
|
30396
|
+
const { text: diff, truncated: diffTruncated } = truncateDiffPayload(rawDiff, MAX_DIFF_BYTES3);
|
|
30397
|
+
const stat20 = await fs34.stat(prepared.absPath);
|
|
30398
|
+
ctx.recordRead(prepared.absPath, stat20.mtimeMs, "write", sha256hex(content));
|
|
29855
30399
|
ctx.session.recordFileChange({
|
|
29856
30400
|
path: prepared.absPath,
|
|
29857
30401
|
action: prepared.existed ? "modified" : "created",
|
|
29858
30402
|
before: prepared.existed ? prepared.prev : null,
|
|
29859
|
-
after:
|
|
30403
|
+
after: content
|
|
29860
30404
|
});
|
|
29861
30405
|
const syntax = await checkSyntax(
|
|
29862
30406
|
prepared.absPath,
|
|
29863
|
-
|
|
30407
|
+
content,
|
|
29864
30408
|
prepared.existed ? prepared.prev : void 0
|
|
29865
30409
|
).catch(() => void 0);
|
|
29866
30410
|
const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
|
|
30411
|
+
const notes = [];
|
|
30412
|
+
if (diffTruncated) {
|
|
30413
|
+
notes.push("Diff truncated to the 256 KiB output budget \u2014 the full write is on disk.");
|
|
30414
|
+
}
|
|
30415
|
+
if (hasSyntaxErrors) {
|
|
30416
|
+
notes.push(
|
|
30417
|
+
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.`
|
|
30418
|
+
);
|
|
30419
|
+
}
|
|
29867
30420
|
return {
|
|
29868
30421
|
path: prepared.absPath,
|
|
29869
|
-
bytes_written: Buffer.byteLength(
|
|
30422
|
+
bytes_written: Buffer.byteLength(content, "utf8"),
|
|
29870
30423
|
created: !prepared.existed,
|
|
29871
30424
|
diff,
|
|
29872
30425
|
syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
|
|
29873
|
-
note:
|
|
30426
|
+
note: notes.length > 0 ? notes.join("\n") : void 0
|
|
29874
30427
|
};
|
|
29875
30428
|
}
|
|
29876
30429
|
|
|
@@ -29995,7 +30548,7 @@ init_circuit_breaker();
|
|
|
29995
30548
|
init_languages();
|
|
29996
30549
|
|
|
29997
30550
|
// src/memory.ts
|
|
29998
|
-
import { ToolValidationError as
|
|
30551
|
+
import { ToolValidationError as ToolValidationError12 } from "@wrongstack/core/types";
|
|
29999
30552
|
function rememberTool(memory) {
|
|
30000
30553
|
return {
|
|
30001
30554
|
name: "remember",
|
|
@@ -30039,7 +30592,7 @@ function rememberTool(memory) {
|
|
|
30039
30592
|
},
|
|
30040
30593
|
async execute(input) {
|
|
30041
30594
|
if (!input?.text) {
|
|
30042
|
-
throw new
|
|
30595
|
+
throw new ToolValidationError12({
|
|
30043
30596
|
message: "remember: text is required",
|
|
30044
30597
|
field: "text"
|
|
30045
30598
|
});
|
|
@@ -30058,28 +30611,48 @@ function forgetTool(memory) {
|
|
|
30058
30611
|
return {
|
|
30059
30612
|
name: "forget",
|
|
30060
30613
|
category: "Session",
|
|
30061
|
-
description: "Remove memory entries that contain the given substring (case-insensitive). Use with caution.",
|
|
30062
|
-
usageHint: "This permanently deletes matching memories in the chosen scope.\n- Provide a reasonably specific `query` to avoid deleting unrelated memories.\n- Always double-check before calling with broad queries.\n- Use `remember` + `forget` together to maintain clean long-term memory.",
|
|
30614
|
+
description: "Remove memory entries that contain the given substring (case-insensitive). Use with caution. Pass `dry_run: true` to preview the matching entries (capped at 20) without deleting anything.",
|
|
30615
|
+
usageHint: "This permanently deletes matching memories in the chosen scope.\n- Provide a reasonably specific `query` to avoid deleting unrelated memories.\n- Always double-check before calling with broad queries \u2014 `dry_run: true` previews the matches without deleting.\n- Use `remember` + `forget` together to maintain clean long-term memory.",
|
|
30063
30616
|
permission: "confirm",
|
|
30617
|
+
// WS-046: gives permission decisions something to key on — the substring
|
|
30618
|
+
// being forgotten.
|
|
30619
|
+
subjectKey: "query",
|
|
30064
30620
|
mutating: true,
|
|
30065
30621
|
timeoutMs: 2e3,
|
|
30066
30622
|
capabilities: ["memory.delete"],
|
|
30623
|
+
icon: "settings",
|
|
30067
30624
|
inputSchema: {
|
|
30068
30625
|
type: "object",
|
|
30069
30626
|
properties: {
|
|
30070
30627
|
query: { type: "string" },
|
|
30071
|
-
scope: { type: "string", enum: ["project-agents", "project-memory", "user-memory"] }
|
|
30628
|
+
scope: { type: "string", enum: ["project-agents", "project-memory", "user-memory"] },
|
|
30629
|
+
dry_run: {
|
|
30630
|
+
type: "boolean",
|
|
30631
|
+
description: "When true, return the matched entries (capped at 20) WITHOUT deleting them. Default false."
|
|
30632
|
+
}
|
|
30072
30633
|
},
|
|
30073
30634
|
required: ["query"]
|
|
30074
30635
|
},
|
|
30075
30636
|
async execute(input) {
|
|
30076
30637
|
if (!input?.query) {
|
|
30077
|
-
throw new
|
|
30638
|
+
throw new ToolValidationError12({
|
|
30078
30639
|
message: "forget: query is required",
|
|
30079
30640
|
field: "query"
|
|
30080
30641
|
});
|
|
30081
30642
|
}
|
|
30082
30643
|
const scope = input.scope ?? "project-memory";
|
|
30644
|
+
if (input.dry_run) {
|
|
30645
|
+
const entries = await memory.list(scope);
|
|
30646
|
+
const needle = input.query.toLowerCase();
|
|
30647
|
+
const matching = entries.filter((entry) => entry.text.toLowerCase().includes(needle));
|
|
30648
|
+
return {
|
|
30649
|
+
removed: 0,
|
|
30650
|
+
scope,
|
|
30651
|
+
dryRun: true,
|
|
30652
|
+
matched: matching.length,
|
|
30653
|
+
matches: matching.slice(0, 20).map((entry) => entry.text)
|
|
30654
|
+
};
|
|
30655
|
+
}
|
|
30083
30656
|
const removed = await memory.forget(input.query, scope);
|
|
30084
30657
|
return { removed, scope };
|
|
30085
30658
|
}
|
|
@@ -30116,7 +30689,7 @@ function searchMemoryTool(memory) {
|
|
|
30116
30689
|
},
|
|
30117
30690
|
async execute(input) {
|
|
30118
30691
|
if (!input?.query) {
|
|
30119
|
-
throw new
|
|
30692
|
+
throw new ToolValidationError12({
|
|
30120
30693
|
message: "search_memory: query is required",
|
|
30121
30694
|
field: "query"
|
|
30122
30695
|
});
|
|
@@ -30168,7 +30741,7 @@ function relatedMemoryTool(memory) {
|
|
|
30168
30741
|
},
|
|
30169
30742
|
async execute(input) {
|
|
30170
30743
|
if (!input?.text) {
|
|
30171
|
-
throw new
|
|
30744
|
+
throw new ToolValidationError12({
|
|
30172
30745
|
message: "find_related_memories: text is required",
|
|
30173
30746
|
field: "text"
|
|
30174
30747
|
});
|
|
@@ -30204,6 +30777,9 @@ function createModeTool(modeStore) {
|
|
|
30204
30777
|
description: "Manage agent operating modes. Modes change the agent's behavior, personality, and system prompt for different workflows (e.g. coding, security review, planning).",
|
|
30205
30778
|
usageHint: "POWERFUL BEHAVIOR CONTROL TOOL:\n\n- Use `list` to see available modes.\n- Use `set <modeId>` to switch the agent into a specific role/mode.\n- Use `get` to check current mode.\n- Use `clear` to return to default behavior.\nSwitching modes is very effective for specialized tasks. The mode change affects how the agent reasons and which guidelines it follows.",
|
|
30206
30779
|
permission: "confirm",
|
|
30780
|
+
// WS-046: gives permission decisions something to key on — the mode being
|
|
30781
|
+
// activated. Permission semantics are unchanged.
|
|
30782
|
+
subjectKey: "mode",
|
|
30207
30783
|
mutating: true,
|
|
30208
30784
|
timeoutMs: 5e3,
|
|
30209
30785
|
capabilities: ["session.mode"],
|
|
@@ -30942,14 +31518,14 @@ function createGlobalPsSlashCommand() {
|
|
|
30942
31518
|
|
|
30943
31519
|
// src/skill.ts
|
|
30944
31520
|
import * as fs35 from "node:fs/promises";
|
|
30945
|
-
import * as
|
|
31521
|
+
import * as path40 from "node:path";
|
|
30946
31522
|
import {
|
|
30947
31523
|
missingRequiredRuntimeTools,
|
|
30948
31524
|
missingRuntimeCapabilities,
|
|
30949
31525
|
runtimeToolReferencesFromText
|
|
30950
31526
|
} from "@wrongstack/core/agent-catalog";
|
|
30951
31527
|
import { SKILL_LIMITS, stripFrontmatter } from "@wrongstack/core/skills";
|
|
30952
|
-
import { ToolValidationError as
|
|
31528
|
+
import { ToolValidationError as ToolValidationError13 } from "@wrongstack/core/types";
|
|
30953
31529
|
var MAX_BODY_CHARS = SKILL_LIMITS.MAX_SKILL_BODY_CHARS;
|
|
30954
31530
|
var MAX_RESOURCE_CHARS = SKILL_LIMITS.MAX_RESOURCE_CHARS;
|
|
30955
31531
|
var MAX_LISTED_RESOURCES = SKILL_LIMITS.MAX_LISTED_RESOURCES;
|
|
@@ -30982,11 +31558,11 @@ function makeSkillTool(skillLoader) {
|
|
|
30982
31558
|
async execute(input, ctx) {
|
|
30983
31559
|
const name = input?.name?.trim();
|
|
30984
31560
|
if (!name) {
|
|
30985
|
-
throw new
|
|
31561
|
+
throw new ToolValidationError13({ message: "skill: name is required", field: "name" });
|
|
30986
31562
|
}
|
|
30987
31563
|
const manifest = await skillLoader.find(name);
|
|
30988
31564
|
if (!manifest) {
|
|
30989
|
-
throw new
|
|
31565
|
+
throw new ToolValidationError13({
|
|
30990
31566
|
message: `skill "${name}" not found \u2014 use /skill to list available skills`,
|
|
30991
31567
|
field: "name"
|
|
30992
31568
|
});
|
|
@@ -30998,7 +31574,7 @@ function makeSkillTool(skillLoader) {
|
|
|
30998
31574
|
);
|
|
30999
31575
|
const missingTools = missingRequiredRuntimeTools(manifest.requiredTools, availableToolNames);
|
|
31000
31576
|
if (missingCapabilities.length > 0 || missingTools.length > 0) {
|
|
31001
|
-
throw new
|
|
31577
|
+
throw new ToolValidationError13({
|
|
31002
31578
|
message: `skill "${name}" is unavailable in this runtime; ` + [
|
|
31003
31579
|
missingCapabilities.length > 0 ? `missing capabilities: ${missingCapabilities.join(", ")}` : "",
|
|
31004
31580
|
missingTools.length > 0 ? `missing tools: ${missingTools.join(", ")}` : ""
|
|
@@ -31006,7 +31582,7 @@ function makeSkillTool(skillLoader) {
|
|
|
31006
31582
|
field: "name"
|
|
31007
31583
|
});
|
|
31008
31584
|
}
|
|
31009
|
-
const dir =
|
|
31585
|
+
const dir = path40.dirname(manifest.path);
|
|
31010
31586
|
let loadedResource;
|
|
31011
31587
|
if (input.resource?.trim()) {
|
|
31012
31588
|
loadedResource = await loadResource(dir, input.resource.trim());
|
|
@@ -31016,12 +31592,7 @@ function makeSkillTool(skillLoader) {
|
|
|
31016
31592
|
runtimeToolReferencesFromText(raw),
|
|
31017
31593
|
availableToolNames
|
|
31018
31594
|
);
|
|
31019
|
-
|
|
31020
|
-
throw new ToolValidationError10({
|
|
31021
|
-
message: `skill "${name}" references unregistered tools: ${missingBodyTools.join(", ")}`,
|
|
31022
|
-
field: "name"
|
|
31023
|
-
});
|
|
31024
|
-
}
|
|
31595
|
+
const warning = missingBodyTools.length > 0 ? `Warning: skill "${name}" references tools not registered in this runtime: ${missingBodyTools.join(", ")}. Steps that call them may be unavailable.` : void 0;
|
|
31025
31596
|
const body = stripFrontmatter(raw).trim().slice(0, MAX_BODY_CHARS);
|
|
31026
31597
|
const resources = loadedResource ? [] : await listResources(dir);
|
|
31027
31598
|
try {
|
|
@@ -31038,44 +31609,48 @@ function makeSkillTool(skillLoader) {
|
|
|
31038
31609
|
body,
|
|
31039
31610
|
resources,
|
|
31040
31611
|
dir,
|
|
31041
|
-
loadedResource
|
|
31612
|
+
loadedResource,
|
|
31613
|
+
warning
|
|
31042
31614
|
};
|
|
31043
31615
|
},
|
|
31044
31616
|
serialize(output) {
|
|
31617
|
+
const warningLine = output.warning ? `
|
|
31618
|
+
|
|
31619
|
+
${output.warning}` : "";
|
|
31045
31620
|
if (output.loadedResource) {
|
|
31046
31621
|
const lr = output.loadedResource;
|
|
31047
31622
|
const note = lr.truncated ? ` (truncated to ${lr.content.length} chars of ${lr.bytes} B)` : "";
|
|
31048
31623
|
return `# Resource: ${output.name}/${lr.rel}
|
|
31049
31624
|
(abs path: ${lr.absPath})${note}
|
|
31050
31625
|
|
|
31051
|
-
${lr.content}`;
|
|
31626
|
+
${lr.content}${warningLine}`;
|
|
31052
31627
|
}
|
|
31053
31628
|
const head = `# Skill: ${output.name}
|
|
31054
31629
|
${output.description}
|
|
31055
31630
|
|
|
31056
31631
|
${output.body}`;
|
|
31057
|
-
if (output.resources.length === 0) return head
|
|
31632
|
+
if (output.resources.length === 0) return `${head}${warningLine}`;
|
|
31058
31633
|
const listing = output.resources.map((r) => `- ${r.path} (${r.bytes} B)`).join("\n");
|
|
31059
31634
|
return `${head}
|
|
31060
31635
|
|
|
31061
31636
|
## Bundled resources (load on demand)
|
|
31062
31637
|
Load any with: \`skill({ name: "${output.name}", resource: "<path>" })\`. Run scripts via bash using their abs path under ${output.dir}.
|
|
31063
|
-
${listing}`;
|
|
31638
|
+
${listing}${warningLine}`;
|
|
31064
31639
|
}
|
|
31065
31640
|
};
|
|
31066
31641
|
}
|
|
31067
31642
|
async function loadResource(skillDir, rel) {
|
|
31068
31643
|
const norm = rel.replace(/\\/g, "/");
|
|
31069
|
-
if (
|
|
31070
|
-
throw new
|
|
31644
|
+
if (path40.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
|
|
31645
|
+
throw new ToolValidationError13({
|
|
31071
31646
|
message: `skill: invalid resource path "${rel}"`,
|
|
31072
31647
|
field: "resource"
|
|
31073
31648
|
});
|
|
31074
31649
|
}
|
|
31075
|
-
const absPath =
|
|
31076
|
-
const root =
|
|
31077
|
-
if (absPath !== root && !absPath.startsWith(root +
|
|
31078
|
-
throw new
|
|
31650
|
+
const absPath = path40.resolve(skillDir, rel);
|
|
31651
|
+
const root = path40.resolve(skillDir);
|
|
31652
|
+
if (absPath !== root && !absPath.startsWith(root + path40.sep)) {
|
|
31653
|
+
throw new ToolValidationError13({
|
|
31079
31654
|
message: `skill: resource "${rel}" escapes the skill directory`,
|
|
31080
31655
|
field: "resource"
|
|
31081
31656
|
});
|
|
@@ -31086,13 +31661,13 @@ async function loadResource(skillDir, rel) {
|
|
|
31086
31661
|
realRoot = await fs35.realpath(root);
|
|
31087
31662
|
realPath = await fs35.realpath(absPath);
|
|
31088
31663
|
} catch {
|
|
31089
|
-
throw new
|
|
31664
|
+
throw new ToolValidationError13({
|
|
31090
31665
|
message: `skill: resource "${rel}" not readable`,
|
|
31091
31666
|
field: "resource"
|
|
31092
31667
|
});
|
|
31093
31668
|
}
|
|
31094
|
-
if (realPath !== realRoot && !realPath.startsWith(realRoot +
|
|
31095
|
-
throw new
|
|
31669
|
+
if (realPath !== realRoot && !realPath.startsWith(realRoot + path40.sep)) {
|
|
31670
|
+
throw new ToolValidationError13({
|
|
31096
31671
|
message: `skill: resource "${rel}" resolves outside the skill directory`,
|
|
31097
31672
|
field: "resource"
|
|
31098
31673
|
});
|
|
@@ -31101,7 +31676,7 @@ async function loadResource(skillDir, rel) {
|
|
|
31101
31676
|
try {
|
|
31102
31677
|
buf = await fs35.readFile(realPath);
|
|
31103
31678
|
} catch {
|
|
31104
|
-
throw new
|
|
31679
|
+
throw new ToolValidationError13({
|
|
31105
31680
|
message: `skill: resource "${rel}" not readable`,
|
|
31106
31681
|
field: "resource"
|
|
31107
31682
|
});
|
|
@@ -31134,7 +31709,7 @@ async function walk(root, dir, out) {
|
|
|
31134
31709
|
}
|
|
31135
31710
|
for (const e of entries) {
|
|
31136
31711
|
if (out.length >= MAX_LISTED_RESOURCES) return;
|
|
31137
|
-
const fullPath =
|
|
31712
|
+
const fullPath = path40.join(dir, e.name);
|
|
31138
31713
|
let isDir = e.isDirectory();
|
|
31139
31714
|
if (e.isSymbolicLink()) {
|
|
31140
31715
|
try {
|
|
@@ -31149,9 +31724,9 @@ async function walk(root, dir, out) {
|
|
|
31149
31724
|
} else if (e.isFile()) {
|
|
31150
31725
|
if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
|
|
31151
31726
|
try {
|
|
31152
|
-
const
|
|
31153
|
-
const rel =
|
|
31154
|
-
out.push({ path: rel, bytes:
|
|
31727
|
+
const stat20 = await fs35.stat(fullPath);
|
|
31728
|
+
const rel = path40.relative(root, fullPath).split(path40.sep).join("/");
|
|
31729
|
+
out.push({ path: rel, bytes: stat20.size });
|
|
31155
31730
|
} catch {
|
|
31156
31731
|
}
|
|
31157
31732
|
}
|
|
@@ -31244,9 +31819,15 @@ var TOOL_ICON_MAP = {
|
|
|
31244
31819
|
"codebase-index": "index",
|
|
31245
31820
|
"codebase-search": "index",
|
|
31246
31821
|
"codebase-stats": "index",
|
|
31822
|
+
"codebase-incoming-calls": "index",
|
|
31823
|
+
"codebase-outgoing-calls": "index",
|
|
31824
|
+
"dead-code-scan": "index",
|
|
31247
31825
|
codebase_index: "index",
|
|
31248
31826
|
codebase_search: "index",
|
|
31249
31827
|
codebase_stats: "index",
|
|
31828
|
+
codebase_incoming_calls: "index",
|
|
31829
|
+
codebase_outgoing_calls: "index",
|
|
31830
|
+
dead_code_scan: "index",
|
|
31250
31831
|
// Data
|
|
31251
31832
|
json: "json",
|
|
31252
31833
|
parse: "json",
|