@wrongstack/tools 0.305.1 → 0.306.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_shell-pick.d.ts +4 -5
- package/dist/_util.d.ts +22 -5
- package/dist/audit.d.ts +0 -1
- package/dist/audit.js +135 -46
- package/dist/bash.js +61 -37
- package/dist/browser/index.js +29 -9
- package/dist/browser/types.d.ts +7 -1
- package/dist/builtin.js +1294 -726
- package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
- package/dist/codebase-index/index.js +223 -152
- package/dist/codebase-index/project-server.js +10 -11
- package/dist/diff.d.ts +5 -0
- package/dist/diff.js +78 -12
- package/dist/document.js +18 -6
- package/dist/edit.js +69 -16
- package/dist/exec.js +44 -22
- package/dist/fetch.js +13 -1
- package/dist/format.d.ts +4 -2
- package/dist/format.js +81 -31
- package/dist/glob.js +12 -4
- package/dist/grep.d.ts +2 -0
- package/dist/grep.js +15 -4
- package/dist/index.js +1359 -762
- package/dist/install.js +96 -37
- package/dist/kanban-tool-types.d.ts +6 -1
- package/dist/kanban.js +60 -0
- package/dist/languages/index.js +28 -13
- package/dist/lint.js +28 -13
- package/dist/logs.d.ts +0 -1
- package/dist/logs.js +44 -13
- package/dist/memory.d.ts +8 -0
- package/dist/memory.js +23 -3
- package/dist/mode.d.ts +1 -1
- package/dist/mode.js +3 -0
- package/dist/next-steps.d.ts +2 -3
- package/dist/next-steps.js +3 -3
- package/dist/outdated.d.ts +0 -3
- package/dist/outdated.js +89 -48
- package/dist/pack.js +1294 -726
- package/dist/plan.js +91 -3
- package/dist/process-registry.d.ts +8 -2
- package/dist/process-registry.js +28 -13
- package/dist/ps-slash.js +22 -12
- package/dist/read.js +10 -3
- package/dist/replace.d.ts +4 -0
- package/dist/replace.js +104 -7
- package/dist/search.d.ts +6 -0
- package/dist/search.js +47 -26
- package/dist/session-kanban.js +3 -1
- package/dist/skill.d.ts +6 -0
- package/dist/skill.js +9 -10
- package/dist/task.js +81 -2
- package/dist/test.js +28 -13
- package/dist/todo.js +79 -2
- package/dist/tool-icons.js +4 -2
- package/dist/tool-summary.d.ts +1 -1
- package/dist/tool-summary.js +76 -1
- package/dist/tool-tier.js +1294 -726
- package/dist/tree.js +9 -10
- package/dist/typecheck.d.ts +0 -2
- package/dist/typecheck.js +98 -31
- package/dist/write.js +58 -10
- package/package.json +4 -4
package/dist/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";
|
|
21124
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 {
|
|
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: {
|
|
@@ -24664,6 +25035,7 @@ function sourceStatus(task) {
|
|
|
24664
25035
|
function todoStatus(task) {
|
|
24665
25036
|
const status = sourceStatus(task);
|
|
24666
25037
|
if (status === "completed") return "completed";
|
|
25038
|
+
if (status === "review" && task.assignment?.status === "completed") return "completed";
|
|
24667
25039
|
if (status === "in_progress" || status === "review") return "in_progress";
|
|
24668
25040
|
return "pending";
|
|
24669
25041
|
}
|
|
@@ -24785,11 +25157,12 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
|
|
|
24785
25157
|
const graphId = task.origin?.graphId ?? "";
|
|
24786
25158
|
if (!originId) return { source: null };
|
|
24787
25159
|
if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
|
|
25160
|
+
const mappedStatus = todoStatus(task);
|
|
24788
25161
|
const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
|
|
24789
25162
|
(todo) => todo.id === originId ? {
|
|
24790
25163
|
...todo,
|
|
24791
25164
|
content: task.title,
|
|
24792
|
-
status:
|
|
25165
|
+
status: mappedStatus
|
|
24793
25166
|
} : todo
|
|
24794
25167
|
);
|
|
24795
25168
|
suppressedTodoMirrors.add(context);
|
|
@@ -24854,6 +25227,9 @@ var kanbanTool = {
|
|
|
24854
25227
|
description: KANBAN_TOOL_DESCRIPTION,
|
|
24855
25228
|
usageHint: KANBAN_TOOL_USAGE_HINT,
|
|
24856
25229
|
permission: "confirm",
|
|
25230
|
+
// WS-046: gives permission decisions something to key on.
|
|
25231
|
+
// The action performed; kanban has no single file or path subject.
|
|
25232
|
+
subjectKey: "action",
|
|
24857
25233
|
mutating: true,
|
|
24858
25234
|
capabilities: ["fs.write"],
|
|
24859
25235
|
icon: "task",
|
|
@@ -25295,6 +25671,7 @@ var kanbanTool = {
|
|
|
25295
25671
|
actor: input.author,
|
|
25296
25672
|
comment: input.transitionComment,
|
|
25297
25673
|
...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
|
|
25674
|
+
...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
|
|
25298
25675
|
...input.attachmentUrl !== void 0 ? {
|
|
25299
25676
|
attachment: {
|
|
25300
25677
|
url: input.attachmentUrl,
|
|
@@ -25687,8 +26064,52 @@ var kanbanTool = {
|
|
|
25687
26064
|
} catch (err) {
|
|
25688
26065
|
return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
|
|
25689
26066
|
}
|
|
26067
|
+
},
|
|
26068
|
+
serialize(output, input) {
|
|
26069
|
+
return serializeKanbanOutput(output, input);
|
|
25690
26070
|
}
|
|
25691
26071
|
};
|
|
26072
|
+
var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
|
|
26073
|
+
var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
|
|
26074
|
+
"get_board",
|
|
26075
|
+
"export_markdown",
|
|
26076
|
+
"export_task_graph"
|
|
26077
|
+
]);
|
|
26078
|
+
function serializeKanbanOutput(output, input) {
|
|
26079
|
+
const action = input && typeof input === "object" ? input.action : void 0;
|
|
26080
|
+
const board = output.board;
|
|
26081
|
+
if (board) {
|
|
26082
|
+
const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
|
|
26083
|
+
let boardBytes = 0;
|
|
26084
|
+
if (!keepFull) {
|
|
26085
|
+
try {
|
|
26086
|
+
boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
|
|
26087
|
+
} catch {
|
|
26088
|
+
boardBytes = 0;
|
|
26089
|
+
}
|
|
26090
|
+
}
|
|
26091
|
+
if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
|
|
26092
|
+
const columns = {};
|
|
26093
|
+
for (const column of board.columns) {
|
|
26094
|
+
columns[column.title || column.id] = board.tasks.filter(
|
|
26095
|
+
(task) => task.columnId === column.id
|
|
26096
|
+
).length;
|
|
26097
|
+
}
|
|
26098
|
+
const compact = {
|
|
26099
|
+
...output,
|
|
26100
|
+
board: {
|
|
26101
|
+
id: board.id,
|
|
26102
|
+
title: board.title,
|
|
26103
|
+
columns,
|
|
26104
|
+
totalTasks: board.tasks.length,
|
|
26105
|
+
note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
|
|
26106
|
+
}
|
|
26107
|
+
};
|
|
26108
|
+
return JSON.stringify(compact, null, 2);
|
|
26109
|
+
}
|
|
26110
|
+
}
|
|
26111
|
+
return JSON.stringify(output, null, 2);
|
|
26112
|
+
}
|
|
25692
26113
|
|
|
25693
26114
|
// src/builtin.ts
|
|
25694
26115
|
init_execute_tool();
|
|
@@ -25803,11 +26224,11 @@ var lintTool = {
|
|
|
25803
26224
|
}
|
|
25804
26225
|
};
|
|
25805
26226
|
async function detectLinter(cwd) {
|
|
25806
|
-
const { stat:
|
|
26227
|
+
const { stat: stat20 } = await import("node:fs/promises");
|
|
25807
26228
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
25808
26229
|
for (const f of checks) {
|
|
25809
26230
|
try {
|
|
25810
|
-
await
|
|
26231
|
+
await stat20(`${cwd}/${f}`);
|
|
25811
26232
|
if (f.includes("biome")) return "biome";
|
|
25812
26233
|
if (f.includes("eslint")) return "eslint";
|
|
25813
26234
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -25824,11 +26245,12 @@ init_util();
|
|
|
25824
26245
|
var logsTool = {
|
|
25825
26246
|
name: "logs",
|
|
25826
26247
|
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
|
|
26248
|
+
description: "Read logs from files or Docker containers. Useful for debugging running applications.",
|
|
26249
|
+
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
26250
|
permission: "confirm",
|
|
25830
26251
|
mutating: false,
|
|
25831
26252
|
timeoutMs: 3e4,
|
|
26253
|
+
maxOutputBytes: 262144,
|
|
25832
26254
|
capabilities: ["shell.restricted"],
|
|
25833
26255
|
icon: "logs",
|
|
25834
26256
|
inputSchema: {
|
|
@@ -25836,7 +26258,7 @@ var logsTool = {
|
|
|
25836
26258
|
properties: {
|
|
25837
26259
|
service: {
|
|
25838
26260
|
type: "string",
|
|
25839
|
-
description: "
|
|
26261
|
+
description: "Docker container name (passed to `docker logs`)"
|
|
25840
26262
|
},
|
|
25841
26263
|
path: {
|
|
25842
26264
|
type: "string",
|
|
@@ -25848,10 +26270,6 @@ var logsTool = {
|
|
|
25848
26270
|
minimum: 0,
|
|
25849
26271
|
maximum: 1e4
|
|
25850
26272
|
},
|
|
25851
|
-
stream: {
|
|
25852
|
-
type: "boolean",
|
|
25853
|
-
description: "Stream logs continuously (like tail -f) (default: false)"
|
|
25854
|
-
},
|
|
25855
26273
|
filter: {
|
|
25856
26274
|
type: "string",
|
|
25857
26275
|
description: "Regex pattern to filter log lines"
|
|
@@ -25859,7 +26277,7 @@ var logsTool = {
|
|
|
25859
26277
|
since: {
|
|
25860
26278
|
type: "string",
|
|
25861
26279
|
enum: ["1h", "6h", "24h", "all"],
|
|
25862
|
-
description:
|
|
26280
|
+
description: 'Only show Docker logs since duration (ignored for files; "all" = no limit)'
|
|
25863
26281
|
},
|
|
25864
26282
|
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
25865
26283
|
}
|
|
@@ -25876,10 +26294,10 @@ var logsTool = {
|
|
|
25876
26294
|
filterRe = compiled.regex;
|
|
25877
26295
|
}
|
|
25878
26296
|
if (input.service) {
|
|
25879
|
-
return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);
|
|
26297
|
+
return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal, input.since);
|
|
25880
26298
|
}
|
|
25881
26299
|
if (input.path) {
|
|
25882
|
-
return await fileLogs(
|
|
26300
|
+
return await fileLogs(await safeResolveReal(input.path, ctx), lines, filterRe);
|
|
25883
26301
|
}
|
|
25884
26302
|
return {
|
|
25885
26303
|
source: "none",
|
|
@@ -25893,7 +26311,7 @@ var logsTool = {
|
|
|
25893
26311
|
async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
25894
26312
|
const args = ["logs"];
|
|
25895
26313
|
if (lines > 0) args.push("--tail", String(lines));
|
|
25896
|
-
if (since) {
|
|
26314
|
+
if (since && since !== "all") {
|
|
25897
26315
|
const sinceMap = { "1h": "1h", "6h": "6h", "24h": "24h" };
|
|
25898
26316
|
args.push("--since", sinceMap[since] ?? "1h");
|
|
25899
26317
|
}
|
|
@@ -25907,7 +26325,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25907
26325
|
};
|
|
25908
26326
|
}
|
|
25909
26327
|
args.push("--timestamps", service);
|
|
25910
|
-
return new Promise((
|
|
26328
|
+
return new Promise((resolve18) => {
|
|
25911
26329
|
let stdout = "";
|
|
25912
26330
|
let stderr = "";
|
|
25913
26331
|
const MAX = 2e5;
|
|
@@ -25923,7 +26341,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25923
26341
|
if (settled) return;
|
|
25924
26342
|
settled = true;
|
|
25925
26343
|
clearTimeout(timer);
|
|
25926
|
-
|
|
26344
|
+
resolve18(result);
|
|
25927
26345
|
};
|
|
25928
26346
|
const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
25929
26347
|
const timer = setTimeout(() => {
|
|
@@ -25958,7 +26376,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25958
26376
|
}
|
|
25959
26377
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
25960
26378
|
var MAX_TAIL_LINES = 1e5;
|
|
25961
|
-
async function fileLogs(
|
|
26379
|
+
async function fileLogs(path41, lines, filterRe) {
|
|
25962
26380
|
const { createInterface } = await import("node:readline");
|
|
25963
26381
|
const { createReadStream: createReadStream2 } = await import("node:fs");
|
|
25964
26382
|
const entries = [];
|
|
@@ -25967,7 +26385,7 @@ async function fileLogs(path40, lines, filterRe, stream) {
|
|
|
25967
26385
|
let writeIdx = 0;
|
|
25968
26386
|
let totalLines = 0;
|
|
25969
26387
|
const rl = createInterface({
|
|
25970
|
-
input: createReadStream2(
|
|
26388
|
+
input: createReadStream2(path41),
|
|
25971
26389
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
25972
26390
|
});
|
|
25973
26391
|
for await (const line of rl) {
|
|
@@ -25988,11 +26406,11 @@ async function fileLogs(path40, lines, filterRe, stream) {
|
|
|
25988
26406
|
if (parsed) entries.push(parsed);
|
|
25989
26407
|
}
|
|
25990
26408
|
return {
|
|
25991
|
-
source:
|
|
26409
|
+
source: path41,
|
|
25992
26410
|
entries,
|
|
25993
26411
|
total: entries.length,
|
|
25994
26412
|
truncated: totalLines > effLines,
|
|
25995
|
-
stream_mode:
|
|
26413
|
+
stream_mode: false
|
|
25996
26414
|
};
|
|
25997
26415
|
}
|
|
25998
26416
|
function parseLogLines(output, filterRe) {
|
|
@@ -26068,25 +26486,12 @@ var outdatedTool = {
|
|
|
26068
26486
|
inputSchema: {
|
|
26069
26487
|
type: "object",
|
|
26070
26488
|
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
|
-
}
|
|
26489
|
+
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
26085
26490
|
}
|
|
26086
26491
|
},
|
|
26087
26492
|
async execute(input, ctx, opts) {
|
|
26088
26493
|
const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
|
|
26089
|
-
const manager = await detectPackageManager(cwd);
|
|
26494
|
+
const manager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
26090
26495
|
if (manager === "npm") {
|
|
26091
26496
|
try {
|
|
26092
26497
|
const { detectNonJsEcosystem: detectNonJsEcosystem2 } = await Promise.resolve().then(() => (init_legacy_bridge(), legacy_bridge_exports));
|
|
@@ -26139,13 +26544,11 @@ var outdatedTool = {
|
|
|
26139
26544
|
}
|
|
26140
26545
|
}
|
|
26141
26546
|
const args = ["outdated", "--json"];
|
|
26142
|
-
if (input.format === "table") args.push("--table");
|
|
26143
|
-
if (input.include_deprecated) args.push("--include", "deprecated");
|
|
26144
26547
|
return runOutdated(manager, args, cwd, opts.signal);
|
|
26145
26548
|
}
|
|
26146
26549
|
};
|
|
26147
26550
|
function runOutdated(manager, args, cwd, signal) {
|
|
26148
|
-
return new Promise((
|
|
26551
|
+
return new Promise((resolve18) => {
|
|
26149
26552
|
let stdout = "";
|
|
26150
26553
|
let stderr = "";
|
|
26151
26554
|
const MAX = 1e5;
|
|
@@ -26170,10 +26573,10 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
26170
26573
|
});
|
|
26171
26574
|
child.on("close", (code) => {
|
|
26172
26575
|
const result = parseOutdatedOutput(stdout, code ?? 0);
|
|
26173
|
-
|
|
26576
|
+
resolve18(result);
|
|
26174
26577
|
});
|
|
26175
26578
|
child.on("error", (e) => {
|
|
26176
|
-
|
|
26579
|
+
resolve18({
|
|
26177
26580
|
exit_code: 1,
|
|
26178
26581
|
packages: [],
|
|
26179
26582
|
total: 0,
|
|
@@ -26194,27 +26597,39 @@ function parseOutdatedOutput(json2, exitCode) {
|
|
|
26194
26597
|
truncated: false
|
|
26195
26598
|
};
|
|
26196
26599
|
}
|
|
26600
|
+
const truncated = json2.length >= 1e5 || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
|
|
26601
|
+
let parsedOk = false;
|
|
26197
26602
|
try {
|
|
26198
26603
|
const data = JSON.parse(json2);
|
|
26604
|
+
parsedOk = true;
|
|
26199
26605
|
for (const name of Object.keys(data)) {
|
|
26200
|
-
const info = data[name];
|
|
26606
|
+
const info = data[name] ?? {};
|
|
26607
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
26201
26608
|
packages.push({
|
|
26202
26609
|
name,
|
|
26203
|
-
current: info
|
|
26204
|
-
latest: info
|
|
26205
|
-
wanted: info
|
|
26206
|
-
|
|
26207
|
-
|
|
26610
|
+
current: str(info["current"]) ?? "unknown",
|
|
26611
|
+
latest: str(info["latest"]) ?? "unknown",
|
|
26612
|
+
wanted: str(info["wanted"]) ?? "unknown",
|
|
26613
|
+
// npm calls it `type`; pnpm calls it `dependencyType`.
|
|
26614
|
+
type: str(info["type"]) ?? str(info["dependencyType"]) ?? "unknown",
|
|
26615
|
+
location: str(info["location"]) ?? name
|
|
26208
26616
|
});
|
|
26209
26617
|
}
|
|
26210
26618
|
} catch {
|
|
26619
|
+
}
|
|
26620
|
+
const outdatedFound = parsedOk && exitCode === 1;
|
|
26621
|
+
let output = normalizeCommandOutput(json2);
|
|
26622
|
+
if (outdatedFound) {
|
|
26623
|
+
output = `${output}
|
|
26624
|
+
|
|
26625
|
+
Note: exit code 1 from \`outdated\` means outdated packages were found (expected); treated as success.`;
|
|
26211
26626
|
}
|
|
26212
26627
|
return {
|
|
26213
|
-
exit_code: exitCode,
|
|
26628
|
+
exit_code: outdatedFound ? 0 : exitCode,
|
|
26214
26629
|
packages,
|
|
26215
26630
|
total: packages.length,
|
|
26216
|
-
output
|
|
26217
|
-
truncated
|
|
26631
|
+
output,
|
|
26632
|
+
truncated
|
|
26218
26633
|
};
|
|
26219
26634
|
}
|
|
26220
26635
|
|
|
@@ -26223,8 +26638,8 @@ init_util();
|
|
|
26223
26638
|
import { spawn as spawn13 } from "node:child_process";
|
|
26224
26639
|
import * as fs28 from "node:fs/promises";
|
|
26225
26640
|
import * as os9 from "node:os";
|
|
26226
|
-
import * as
|
|
26227
|
-
import { buildChildEnv as buildChildEnv8, toErrorMessage as
|
|
26641
|
+
import * as path34 from "node:path";
|
|
26642
|
+
import { buildChildEnv as buildChildEnv8, toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
|
|
26228
26643
|
var patchTool = {
|
|
26229
26644
|
name: "patch",
|
|
26230
26645
|
category: "Filesystem",
|
|
@@ -26268,26 +26683,26 @@ var patchTool = {
|
|
|
26268
26683
|
try {
|
|
26269
26684
|
dir = input.directory ? await safeResolveReal(input.directory, ctx) : ctx.cwd;
|
|
26270
26685
|
} catch (err) {
|
|
26271
|
-
return refuse(`patch refused: ${
|
|
26686
|
+
return refuse(`patch refused: ${toErrorMessage7(err)}`);
|
|
26272
26687
|
}
|
|
26273
|
-
const realRoot = await fs28.realpath(ctx.projectRoot).catch(() =>
|
|
26688
|
+
const realRoot = await fs28.realpath(ctx.projectRoot).catch(() => path34.resolve(ctx.projectRoot));
|
|
26274
26689
|
const targets = extractDiffTargets(input.patch);
|
|
26275
26690
|
const resolvedTargets = [];
|
|
26276
26691
|
for (const t of targets) {
|
|
26277
26692
|
const stripped = stripPathComponents(t.raw, strip);
|
|
26278
26693
|
if (!stripped) continue;
|
|
26279
|
-
if (
|
|
26694
|
+
if (path34.isAbsolute(stripped)) {
|
|
26280
26695
|
return refuse(`patch refused: target "${t.raw}" strips to absolute path`);
|
|
26281
26696
|
}
|
|
26282
|
-
const candidate =
|
|
26697
|
+
const candidate = path34.resolve(dir, stripped);
|
|
26283
26698
|
let real;
|
|
26284
26699
|
try {
|
|
26285
26700
|
real = await resolveRealInsideRoot(candidate, ctx);
|
|
26286
26701
|
} catch (err) {
|
|
26287
|
-
return refuse(`patch refused: target "${t.raw}" ${
|
|
26702
|
+
return refuse(`patch refused: target "${t.raw}" ${toErrorMessage7(err)}`);
|
|
26288
26703
|
}
|
|
26289
|
-
const rel =
|
|
26290
|
-
if (rel.startsWith("..") ||
|
|
26704
|
+
const rel = path34.relative(realRoot, real);
|
|
26705
|
+
if (rel.startsWith("..") || path34.isAbsolute(rel)) {
|
|
26291
26706
|
return refuse(`patch refused: target "${t.raw}" resolves outside project root`);
|
|
26292
26707
|
}
|
|
26293
26708
|
resolvedTargets.push({ raw: t.raw, deleted: t.deleted, abs: real });
|
|
@@ -26301,11 +26716,11 @@ var patchTool = {
|
|
|
26301
26716
|
beforeContents.set(target.abs, await readTextForTracking(target.abs));
|
|
26302
26717
|
}
|
|
26303
26718
|
}
|
|
26304
|
-
const tmpDir = await fs28.mkdtemp(
|
|
26719
|
+
const tmpDir = await fs28.mkdtemp(path34.join(os9.tmpdir(), ".wstack_patch_"));
|
|
26305
26720
|
try {
|
|
26306
26721
|
await fs28.chmod(tmpDir, 448).catch(() => {
|
|
26307
26722
|
});
|
|
26308
|
-
const patchFile =
|
|
26723
|
+
const patchFile = path34.join(tmpDir, "in.diff");
|
|
26309
26724
|
await fs28.writeFile(patchFile, input.patch, { mode: 384 });
|
|
26310
26725
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
26311
26726
|
const result = await runPatch(args, dir, opts.signal, {
|
|
@@ -26318,8 +26733,8 @@ var patchTool = {
|
|
|
26318
26733
|
for (const target of resolvedTargets) {
|
|
26319
26734
|
const abs = target.abs;
|
|
26320
26735
|
const before = beforeContents.get(abs) ?? null;
|
|
26321
|
-
const
|
|
26322
|
-
if (!
|
|
26736
|
+
const stat20 = await fs28.stat(abs).catch(() => null);
|
|
26737
|
+
if (!stat20?.isFile()) {
|
|
26323
26738
|
if (beforeExisted.has(abs)) {
|
|
26324
26739
|
touched.push(abs);
|
|
26325
26740
|
ctx.session?.recordFileChange?.({
|
|
@@ -26334,7 +26749,7 @@ var patchTool = {
|
|
|
26334
26749
|
const after = await readTextForTracking(abs);
|
|
26335
26750
|
if (after === null || after === before) continue;
|
|
26336
26751
|
touched.push(abs);
|
|
26337
|
-
ctx.recordRead?.(abs,
|
|
26752
|
+
ctx.recordRead?.(abs, stat20.mtimeMs, "write", sha256hex(after));
|
|
26338
26753
|
ctx.session?.recordFileChange?.({
|
|
26339
26754
|
path: abs,
|
|
26340
26755
|
action: before === null ? "created" : "modified",
|
|
@@ -26345,7 +26760,7 @@ var patchTool = {
|
|
|
26345
26760
|
}
|
|
26346
26761
|
if (result.exitCode !== 0) {
|
|
26347
26762
|
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) =>
|
|
26763
|
+
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
26764
|
return {
|
|
26350
26765
|
applied: touched.length,
|
|
26351
26766
|
rejected: 1,
|
|
@@ -26353,7 +26768,7 @@ var patchTool = {
|
|
|
26353
26768
|
// success path (which returns GNU patch's dir-relative names).
|
|
26354
26769
|
// `touched` entries are realpaths from resolveRealInsideRoot, and
|
|
26355
26770
|
// realRoot is also a realpath, so path.relative is like-for-like.
|
|
26356
|
-
files: touched.map((p) =>
|
|
26771
|
+
files: touched.map((p) => path34.relative(realRoot, p) || p),
|
|
26357
26772
|
dry_run: dryRun,
|
|
26358
26773
|
message: `patch failed: ${result.stderr || result.stdout}${partial}`
|
|
26359
26774
|
};
|
|
@@ -26369,7 +26784,7 @@ var patchTool = {
|
|
|
26369
26784
|
}
|
|
26370
26785
|
const patched = result.engine === "git" ? [
|
|
26371
26786
|
...new Set(
|
|
26372
|
-
resolvedTargets.map((target) =>
|
|
26787
|
+
resolvedTargets.map((target) => path34.relative(dir, target.abs) || target.abs)
|
|
26373
26788
|
)
|
|
26374
26789
|
] : extractPatchedFiles(result.stdout);
|
|
26375
26790
|
return {
|
|
@@ -26388,8 +26803,8 @@ var patchTool = {
|
|
|
26388
26803
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
26389
26804
|
async function readTextForTracking(absPath) {
|
|
26390
26805
|
try {
|
|
26391
|
-
const
|
|
26392
|
-
if (!
|
|
26806
|
+
const stat20 = await fs28.stat(absPath);
|
|
26807
|
+
if (!stat20.isFile() || stat20.size > MAX_TRACKING_BYTES) return null;
|
|
26393
26808
|
const buf = await fs28.readFile(absPath);
|
|
26394
26809
|
if (buf.includes(0)) return null;
|
|
26395
26810
|
return buf.toString("utf8");
|
|
@@ -26474,7 +26889,7 @@ function runPatch(args, cwd, signal, fallback) {
|
|
|
26474
26889
|
});
|
|
26475
26890
|
}
|
|
26476
26891
|
function runPatchProcess(command, args, cwd, signal) {
|
|
26477
|
-
return new Promise((
|
|
26892
|
+
return new Promise((resolve18) => {
|
|
26478
26893
|
let stdout = "";
|
|
26479
26894
|
let stderr = "";
|
|
26480
26895
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
@@ -26493,11 +26908,11 @@ function runPatchProcess(command, args, cwd, signal) {
|
|
|
26493
26908
|
});
|
|
26494
26909
|
child.on(
|
|
26495
26910
|
"close",
|
|
26496
|
-
(code) =>
|
|
26911
|
+
(code) => resolve18({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
|
|
26497
26912
|
);
|
|
26498
26913
|
child.on(
|
|
26499
26914
|
"error",
|
|
26500
|
-
(e) =>
|
|
26915
|
+
(e) => resolve18({
|
|
26501
26916
|
exitCode: 1,
|
|
26502
26917
|
stdout: "",
|
|
26503
26918
|
stderr: e.message,
|
|
@@ -26664,6 +27079,20 @@ async function synchronizeManagedKanban(items, board, ctx, signal) {
|
|
|
26664
27079
|
}
|
|
26665
27080
|
const task = board.tasks.find((candidate) => candidate.id === item.kanbanTaskId);
|
|
26666
27081
|
if (!task || task.status === "completed") continue;
|
|
27082
|
+
const stage = task.lifecycle?.currentStage;
|
|
27083
|
+
if (stage === "backlog" || stage === "todo") {
|
|
27084
|
+
const started = await execute({
|
|
27085
|
+
action: "start_task",
|
|
27086
|
+
boardId: board.id,
|
|
27087
|
+
taskId: task.id,
|
|
27088
|
+
author: actor,
|
|
27089
|
+
agentId: actor,
|
|
27090
|
+
transitionComment: `Auto-started for completion: ${item.content}`
|
|
27091
|
+
});
|
|
27092
|
+
if (!started.ok) {
|
|
27093
|
+
continue;
|
|
27094
|
+
}
|
|
27095
|
+
}
|
|
26667
27096
|
await execute({
|
|
26668
27097
|
action: "mark_assignment",
|
|
26669
27098
|
boardId: board.id,
|
|
@@ -26875,7 +27304,8 @@ var todoTool = {
|
|
|
26875
27304
|
}
|
|
26876
27305
|
for (const planId of completedPlanIds) {
|
|
26877
27306
|
if (pendingPlanIds.has(planId)) continue;
|
|
26878
|
-
const
|
|
27307
|
+
const meta = ctx.meta;
|
|
27308
|
+
const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
|
|
26879
27309
|
if (typeof planPath !== "string" || !planPath) continue;
|
|
26880
27310
|
try {
|
|
26881
27311
|
const plan = await loadPlan2(planPath);
|
|
@@ -26888,7 +27318,8 @@ var todoTool = {
|
|
|
26888
27318
|
}
|
|
26889
27319
|
for (const taskId of completedTaskIds) {
|
|
26890
27320
|
if (pendingTaskIds.has(taskId)) continue;
|
|
26891
|
-
const
|
|
27321
|
+
const meta = ctx.meta;
|
|
27322
|
+
const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
|
|
26892
27323
|
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
26893
27324
|
try {
|
|
26894
27325
|
const file = await loadTasks3(taskPath);
|
|
@@ -27004,7 +27435,16 @@ var planTool = {
|
|
|
27004
27435
|
sessionPlanPath.lastIndexOf("/"),
|
|
27005
27436
|
sessionPlanPath.lastIndexOf("\\")
|
|
27006
27437
|
);
|
|
27007
|
-
|
|
27438
|
+
if (lastSep < 0) {
|
|
27439
|
+
return {
|
|
27440
|
+
ok: false,
|
|
27441
|
+
message: `Cannot derive the project-scoped plan path: session plan path "${sessionPlanPath}" has no directory component.`,
|
|
27442
|
+
plan: "",
|
|
27443
|
+
count: 0,
|
|
27444
|
+
open: 0
|
|
27445
|
+
};
|
|
27446
|
+
}
|
|
27447
|
+
planPath = sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json";
|
|
27008
27448
|
}
|
|
27009
27449
|
} else {
|
|
27010
27450
|
planPath = sessionPlanPath;
|
|
@@ -27197,6 +27637,7 @@ var planTool = {
|
|
|
27197
27637
|
open: 0
|
|
27198
27638
|
};
|
|
27199
27639
|
}
|
|
27640
|
+
ctx.meta["plan.path.resolved"] = planPath;
|
|
27200
27641
|
await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
|
|
27201
27642
|
if (todosToReplace) {
|
|
27202
27643
|
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
@@ -27229,6 +27670,7 @@ var planTool = {
|
|
|
27229
27670
|
});
|
|
27230
27671
|
return f;
|
|
27231
27672
|
});
|
|
27673
|
+
ctx.meta["task.path.resolved"] = taskPath;
|
|
27232
27674
|
return mkResult(
|
|
27233
27675
|
plan,
|
|
27234
27676
|
true,
|
|
@@ -27262,14 +27704,14 @@ function mkResult(plan, ok, message, todos) {
|
|
|
27262
27704
|
// src/read.ts
|
|
27263
27705
|
init_util();
|
|
27264
27706
|
import * as fs29 from "node:fs/promises";
|
|
27265
|
-
import { FsError, ToolValidationError as
|
|
27266
|
-
import { toErrorMessage as
|
|
27707
|
+
import { FsError, ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
|
|
27708
|
+
import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
|
|
27267
27709
|
var ADVANCED_MODE_META_KEY = "tools.read.advancedMode";
|
|
27268
27710
|
var MAX_BYTES2 = 5 * 1024 * 1024;
|
|
27269
27711
|
var readTool = {
|
|
27270
27712
|
name: "read",
|
|
27271
27713
|
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
|
|
27714
|
+
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
27715
|
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
27716
|
selection: {
|
|
27275
27717
|
doNotUseWhen: "you need to search many files for matching content.",
|
|
@@ -27290,11 +27732,13 @@ var readTool = {
|
|
|
27290
27732
|
},
|
|
27291
27733
|
offset: {
|
|
27292
27734
|
type: "integer",
|
|
27735
|
+
minimum: 1,
|
|
27293
27736
|
description: "1-based starting line number. Use together with `limit` for large files."
|
|
27294
27737
|
},
|
|
27295
27738
|
limit: {
|
|
27296
27739
|
type: "integer",
|
|
27297
|
-
|
|
27740
|
+
minimum: 0,
|
|
27741
|
+
description: "Maximum number of lines to return (default 2000). Values above 5000 are clamped to 5000 \u2014 page with `offset` for more."
|
|
27298
27742
|
},
|
|
27299
27743
|
mode: {
|
|
27300
27744
|
type: "string",
|
|
@@ -27310,16 +27754,16 @@ var readTool = {
|
|
|
27310
27754
|
},
|
|
27311
27755
|
async execute(input, ctx, execOpts) {
|
|
27312
27756
|
if (!input?.path) {
|
|
27313
|
-
throw new
|
|
27757
|
+
throw new ToolValidationError8({
|
|
27314
27758
|
message: "read: path is required",
|
|
27315
27759
|
field: "path"
|
|
27316
27760
|
});
|
|
27317
27761
|
}
|
|
27318
27762
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
27319
27763
|
const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
|
|
27320
|
-
let
|
|
27764
|
+
let stat20;
|
|
27321
27765
|
try {
|
|
27322
|
-
|
|
27766
|
+
stat20 = await fs29.stat(absPath);
|
|
27323
27767
|
} catch (err) {
|
|
27324
27768
|
const code = err.code;
|
|
27325
27769
|
if (code === "ENOENT") {
|
|
@@ -27331,14 +27775,14 @@ var readTool = {
|
|
|
27331
27775
|
});
|
|
27332
27776
|
}
|
|
27333
27777
|
throw new FsError({
|
|
27334
|
-
message: `read: failed to stat "${input.path}": ${
|
|
27778
|
+
message: `read: failed to stat "${input.path}": ${toErrorMessage8(err)}`,
|
|
27335
27779
|
code: "FS_READ_FAILED",
|
|
27336
27780
|
path: absPath,
|
|
27337
27781
|
context: { errno: code },
|
|
27338
27782
|
cause: err
|
|
27339
27783
|
});
|
|
27340
27784
|
}
|
|
27341
|
-
if (!
|
|
27785
|
+
if (!stat20.isFile()) {
|
|
27342
27786
|
throw new FsError({
|
|
27343
27787
|
message: `read: "${input.path}" is not a regular file`,
|
|
27344
27788
|
code: "FS_READ_FAILED",
|
|
@@ -27346,23 +27790,23 @@ var readTool = {
|
|
|
27346
27790
|
context: { reason: "not-a-regular-file" }
|
|
27347
27791
|
});
|
|
27348
27792
|
}
|
|
27349
|
-
if (
|
|
27793
|
+
if (stat20.size > MAX_BYTES2) {
|
|
27350
27794
|
throw new FsError({
|
|
27351
|
-
message: `read: file too large (${
|
|
27795
|
+
message: `read: file too large (${stat20.size} bytes, limit ${MAX_BYTES2})`,
|
|
27352
27796
|
code: "FS_READ_FAILED",
|
|
27353
27797
|
path: absPath,
|
|
27354
|
-
context: { size:
|
|
27798
|
+
context: { size: stat20.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
27355
27799
|
});
|
|
27356
27800
|
}
|
|
27357
27801
|
const offset = Math.max(1, input.offset ?? 1);
|
|
27358
27802
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
27359
27803
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
27360
27804
|
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,
|
|
27805
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat20.mtimeMs, offset, requestedEnd)) {
|
|
27806
|
+
ctx.recordRead(absPath, stat20.mtimeMs);
|
|
27363
27807
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27364
27808
|
return {
|
|
27365
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
27809
|
+
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
27810
|
total_lines: prior.totalLines,
|
|
27367
27811
|
encoding: "utf8",
|
|
27368
27812
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -27373,18 +27817,23 @@ var readTool = {
|
|
|
27373
27817
|
}
|
|
27374
27818
|
const buf = await fs29.readFile(absPath);
|
|
27375
27819
|
if (isBinaryBuffer(buf)) {
|
|
27376
|
-
throw new
|
|
27820
|
+
throw new FsError({
|
|
27821
|
+
message: `read: "${input.path}" appears to be binary`,
|
|
27822
|
+
code: "FS_READ_FAILED",
|
|
27823
|
+
path: absPath,
|
|
27824
|
+
context: { reason: "binary" }
|
|
27825
|
+
});
|
|
27377
27826
|
}
|
|
27378
27827
|
const text = buf.toString("utf8");
|
|
27379
27828
|
const contentHash = sha256hex(text);
|
|
27380
27829
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
27381
27830
|
const total = allLines.length;
|
|
27382
27831
|
if (input.mode === "summary") {
|
|
27383
|
-
ctx.recordRead(absPath,
|
|
27384
|
-
rememberReadRange(ctx, absPath,
|
|
27832
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
|
|
27833
|
+
rememberReadRange(ctx, absPath, stat20.mtimeMs, total, 1, Math.min(total, 200));
|
|
27385
27834
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27386
27835
|
return {
|
|
27387
|
-
text: summarizeFile(input.path,
|
|
27836
|
+
text: summarizeFile(input.path, stat20.size, allLines),
|
|
27388
27837
|
total_lines: total,
|
|
27389
27838
|
encoding: "utf8",
|
|
27390
27839
|
truncated: total > 200,
|
|
@@ -27396,8 +27845,8 @@ var readTool = {
|
|
|
27396
27845
|
};
|
|
27397
27846
|
}
|
|
27398
27847
|
if (limit === 0) {
|
|
27399
|
-
ctx.recordRead(absPath,
|
|
27400
|
-
rememberReadRange(ctx, absPath,
|
|
27848
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
|
|
27849
|
+
rememberReadRange(ctx, absPath, stat20.mtimeMs, total, 1, 0);
|
|
27401
27850
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27402
27851
|
return {
|
|
27403
27852
|
text: "",
|
|
@@ -27409,8 +27858,8 @@ var readTool = {
|
|
|
27409
27858
|
};
|
|
27410
27859
|
}
|
|
27411
27860
|
if (offset > total) {
|
|
27412
|
-
ctx.recordRead(absPath,
|
|
27413
|
-
rememberReadRange(ctx, absPath,
|
|
27861
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
|
|
27862
|
+
rememberReadRange(ctx, absPath, stat20.mtimeMs, total, total + 1, total + 1);
|
|
27414
27863
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27415
27864
|
return {
|
|
27416
27865
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
@@ -27425,8 +27874,8 @@ var readTool = {
|
|
|
27425
27874
|
const truncated = offset - 1 + slice.length < total;
|
|
27426
27875
|
const width = String(offset + slice.length - 1).length;
|
|
27427
27876
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
27428
|
-
ctx.recordRead(absPath,
|
|
27429
|
-
rememberReadRange(ctx, absPath,
|
|
27877
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
|
|
27878
|
+
rememberReadRange(ctx, absPath, stat20.mtimeMs, total, offset, offset + slice.length - 1);
|
|
27430
27879
|
const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27431
27880
|
return {
|
|
27432
27881
|
text: numbered,
|
|
@@ -27534,8 +27983,8 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
27534
27983
|
// src/replace.ts
|
|
27535
27984
|
import { spawn as spawn14 } from "node:child_process";
|
|
27536
27985
|
import * as fs30 from "node:fs/promises";
|
|
27537
|
-
import * as
|
|
27538
|
-
import { ToolValidationError as
|
|
27986
|
+
import * as path35 from "node:path";
|
|
27987
|
+
import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
|
|
27539
27988
|
import {
|
|
27540
27989
|
atomicWrite as atomicWrite3,
|
|
27541
27990
|
buildChildEnv as buildChildEnv9,
|
|
@@ -27547,12 +27996,13 @@ import {
|
|
|
27547
27996
|
unifiedDiff as unifiedDiff2
|
|
27548
27997
|
} from "@wrongstack/core/utils";
|
|
27549
27998
|
init_util();
|
|
27999
|
+
var MAX_DIFF_BYTES2 = 262144;
|
|
27550
28000
|
var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
27551
28001
|
var replaceTool = {
|
|
27552
28002
|
name: "replace",
|
|
27553
28003
|
category: "Transform",
|
|
27554
28004
|
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.",
|
|
28005
|
+
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
28006
|
permission: "confirm",
|
|
27557
28007
|
// WS-046: gives permission decisions something to key on.
|
|
27558
28008
|
// The file scope being rewritten, not the pattern: a trust rule should say
|
|
@@ -27562,11 +28012,15 @@ var replaceTool = {
|
|
|
27562
28012
|
capabilities: ["fs.write"],
|
|
27563
28013
|
icon: "edit",
|
|
27564
28014
|
timeoutMs: 3e4,
|
|
28015
|
+
maxOutputBytes: 262144,
|
|
27565
28016
|
inputSchema: {
|
|
27566
28017
|
type: "object",
|
|
27567
28018
|
properties: {
|
|
27568
28019
|
pattern: { type: "string", description: "Regex pattern to match" },
|
|
27569
|
-
replacement: {
|
|
28020
|
+
replacement: {
|
|
28021
|
+
type: "string",
|
|
28022
|
+
description: "Replacement string. Supports `$1`\u2013`$9` (capture groups), `$&` (whole match), and `$$` (literal dollar sign) \u2014 same semantics as JavaScript String.replace."
|
|
28023
|
+
},
|
|
27570
28024
|
files: {
|
|
27571
28025
|
type: "string",
|
|
27572
28026
|
description: "File(s) to target: single path, comma-separated list, or glob pattern"
|
|
@@ -27582,19 +28036,19 @@ var replaceTool = {
|
|
|
27582
28036
|
},
|
|
27583
28037
|
async execute(input, ctx) {
|
|
27584
28038
|
if (!input?.pattern) {
|
|
27585
|
-
throw new
|
|
28039
|
+
throw new ToolValidationError9({
|
|
27586
28040
|
message: "replace: pattern is required",
|
|
27587
28041
|
field: "pattern"
|
|
27588
28042
|
});
|
|
27589
28043
|
}
|
|
27590
28044
|
if (input.replacement === void 0) {
|
|
27591
|
-
throw new
|
|
28045
|
+
throw new ToolValidationError9({
|
|
27592
28046
|
message: "replace: replacement is required",
|
|
27593
28047
|
field: "replacement"
|
|
27594
28048
|
});
|
|
27595
28049
|
}
|
|
27596
28050
|
if (!input?.files) {
|
|
27597
|
-
throw new
|
|
28051
|
+
throw new ToolValidationError9({
|
|
27598
28052
|
message: "replace: files is required",
|
|
27599
28053
|
field: "files"
|
|
27600
28054
|
});
|
|
@@ -27602,7 +28056,7 @@ var replaceTool = {
|
|
|
27602
28056
|
const replaceAll = input.replace_all ?? true;
|
|
27603
28057
|
const compiled = compileUserRegex(input.pattern, "g");
|
|
27604
28058
|
if (!compiled.ok) {
|
|
27605
|
-
throw new
|
|
28059
|
+
throw new ToolValidationError9({
|
|
27606
28060
|
message: `replace: ${compiled.reason}`,
|
|
27607
28061
|
field: "pattern"
|
|
27608
28062
|
});
|
|
@@ -27615,6 +28069,9 @@ var replaceTool = {
|
|
|
27615
28069
|
const realRoot = await fs30.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
27616
28070
|
const results = [];
|
|
27617
28071
|
let totalReplacements = 0;
|
|
28072
|
+
let diffBytesUsed = 0;
|
|
28073
|
+
let diffsOmitted = 0;
|
|
28074
|
+
let diffsTruncated = 0;
|
|
27618
28075
|
for (const absPath of fileList) {
|
|
27619
28076
|
const lstat2 = await fs30.lstat(absPath).catch((err) => {
|
|
27620
28077
|
if (err.code === "ENOENT") return null;
|
|
@@ -27628,10 +28085,10 @@ var replaceTool = {
|
|
|
27628
28085
|
} catch {
|
|
27629
28086
|
continue;
|
|
27630
28087
|
}
|
|
27631
|
-
const rel =
|
|
27632
|
-
if (rel.startsWith("..") ||
|
|
27633
|
-
const
|
|
27634
|
-
if (!
|
|
28088
|
+
const rel = path35.relative(realRoot, realPath);
|
|
28089
|
+
if (rel.startsWith("..") || path35.isAbsolute(rel)) continue;
|
|
28090
|
+
const stat20 = await fs30.stat(realPath).catch(() => null);
|
|
28091
|
+
if (!stat20?.isFile()) continue;
|
|
27635
28092
|
let content;
|
|
27636
28093
|
try {
|
|
27637
28094
|
const buf = await fs30.readFile(realPath);
|
|
@@ -27650,13 +28107,13 @@ var replaceTool = {
|
|
|
27650
28107
|
let newContentLf = contentLf;
|
|
27651
28108
|
for (let i = matches.length - 1; i >= 0; i--) {
|
|
27652
28109
|
const m = expectDefined8(matches[i]);
|
|
27653
|
-
newContentLf = newContentLf.slice(0, m.index) + input.replacement + newContentLf.slice(expectDefined8(m.index) + m[0].length);
|
|
28110
|
+
newContentLf = newContentLf.slice(0, m.index) + expandReplacement(input.replacement, m) + newContentLf.slice(expectDefined8(m.index) + m[0].length);
|
|
27654
28111
|
}
|
|
27655
28112
|
re.lastIndex = 0;
|
|
27656
28113
|
totalReplacements += count;
|
|
27657
28114
|
if (!dryRun) {
|
|
27658
28115
|
const newContent = toStyle2(newContentLf, style);
|
|
27659
|
-
await atomicWrite3(realPath, newContent, { mode:
|
|
28116
|
+
await atomicWrite3(realPath, newContent, { mode: stat20.mode & 511 });
|
|
27660
28117
|
const written = await fs30.stat(realPath).catch(() => null);
|
|
27661
28118
|
if (written) {
|
|
27662
28119
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
@@ -27668,24 +28125,76 @@ var replaceTool = {
|
|
|
27668
28125
|
after: newContent
|
|
27669
28126
|
});
|
|
27670
28127
|
}
|
|
27671
|
-
|
|
28128
|
+
let diff = dryRun || matches.length > 0 ? unifiedDiff2(content, toStyle2(newContentLf, style), {
|
|
27672
28129
|
fromFile: absPath,
|
|
27673
28130
|
toFile: absPath
|
|
27674
28131
|
}) : void 0;
|
|
28132
|
+
if (diff !== void 0) {
|
|
28133
|
+
const remaining = MAX_DIFF_BYTES2 - diffBytesUsed;
|
|
28134
|
+
if (remaining <= 0) {
|
|
28135
|
+
diff = void 0;
|
|
28136
|
+
diffsOmitted++;
|
|
28137
|
+
} else {
|
|
28138
|
+
const capped = truncateDiffPayload(diff, remaining);
|
|
28139
|
+
if (capped.truncated) diffsTruncated++;
|
|
28140
|
+
diff = capped.text;
|
|
28141
|
+
diffBytesUsed += Buffer.byteLength(diff, "utf8");
|
|
28142
|
+
}
|
|
28143
|
+
}
|
|
27675
28144
|
results.push({
|
|
27676
28145
|
path: absPath,
|
|
27677
28146
|
replacements: matches.length,
|
|
27678
28147
|
diff
|
|
27679
28148
|
});
|
|
27680
28149
|
}
|
|
28150
|
+
const overBudget = diffsOmitted > 0 || diffsTruncated > 0;
|
|
27681
28151
|
return {
|
|
27682
28152
|
files_modified: results.length,
|
|
27683
28153
|
total_replacements: totalReplacements,
|
|
27684
28154
|
results,
|
|
27685
|
-
dry_run: dryRun
|
|
28155
|
+
dry_run: dryRun,
|
|
28156
|
+
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
28157
|
};
|
|
27687
28158
|
}
|
|
27688
28159
|
};
|
|
28160
|
+
function expandReplacement(template, match) {
|
|
28161
|
+
if (!template.includes("$")) return template;
|
|
28162
|
+
let out = "";
|
|
28163
|
+
for (let i = 0; i < template.length; i++) {
|
|
28164
|
+
const ch = template[i];
|
|
28165
|
+
if (ch !== "$") {
|
|
28166
|
+
out += ch;
|
|
28167
|
+
continue;
|
|
28168
|
+
}
|
|
28169
|
+
const next = template[i + 1];
|
|
28170
|
+
if (next === "$") {
|
|
28171
|
+
out += "$";
|
|
28172
|
+
i++;
|
|
28173
|
+
} else if (next === "&") {
|
|
28174
|
+
out += match[0];
|
|
28175
|
+
i++;
|
|
28176
|
+
} else if (next !== void 0 && next >= "1" && next <= "9") {
|
|
28177
|
+
const idx = next.charCodeAt(0) - 48;
|
|
28178
|
+
if (idx < match.length) {
|
|
28179
|
+
out += match[idx] ?? "";
|
|
28180
|
+
i++;
|
|
28181
|
+
} else {
|
|
28182
|
+
out += "$";
|
|
28183
|
+
}
|
|
28184
|
+
} else {
|
|
28185
|
+
out += "$";
|
|
28186
|
+
}
|
|
28187
|
+
}
|
|
28188
|
+
return out;
|
|
28189
|
+
}
|
|
28190
|
+
function passesExtraGlob(extraGlob, name, full) {
|
|
28191
|
+
extraGlob.lastIndex = 0;
|
|
28192
|
+
const nameMatch = extraGlob.test(name);
|
|
28193
|
+
extraGlob.lastIndex = 0;
|
|
28194
|
+
const fullMatch = extraGlob.test(full);
|
|
28195
|
+
extraGlob.lastIndex = 0;
|
|
28196
|
+
return nameMatch || fullMatch;
|
|
28197
|
+
}
|
|
27689
28198
|
async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
27690
28199
|
const base = ctx.cwd;
|
|
27691
28200
|
const normalized = filesInput.trim();
|
|
@@ -27696,8 +28205,9 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
27696
28205
|
const resolved = [];
|
|
27697
28206
|
for (const p of parts) {
|
|
27698
28207
|
const absPath = safeResolve(p, ctx);
|
|
27699
|
-
|
|
27700
|
-
|
|
28208
|
+
if (extraGlob && !passesExtraGlob(extraGlob, path35.basename(absPath), absPath)) continue;
|
|
28209
|
+
const stat20 = await fs30.stat(absPath).catch(() => null);
|
|
28210
|
+
if (stat20?.isFile()) {
|
|
27701
28211
|
resolved.push(absPath);
|
|
27702
28212
|
}
|
|
27703
28213
|
}
|
|
@@ -27708,26 +28218,32 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
27708
28218
|
if (rgAvailable) {
|
|
27709
28219
|
try {
|
|
27710
28220
|
const { promise } = spawnRgFind(pattern, base);
|
|
27711
|
-
|
|
28221
|
+
const files = await promise;
|
|
28222
|
+
if (extraGlob) {
|
|
28223
|
+
return files.filter((f) => passesExtraGlob(extraGlob, path35.basename(f), f));
|
|
28224
|
+
}
|
|
28225
|
+
return files;
|
|
27712
28226
|
} catch {
|
|
27713
28227
|
}
|
|
27714
28228
|
}
|
|
27715
28229
|
return await globNative(pattern, base, extraGlob);
|
|
27716
28230
|
}
|
|
28231
|
+
var rgAvailabilityCache2;
|
|
27717
28232
|
function checkRg() {
|
|
27718
|
-
|
|
28233
|
+
rgAvailabilityCache2 ??= new Promise((resolve18) => {
|
|
27719
28234
|
try {
|
|
27720
28235
|
const p = spawn14("rg", ["--version"], {
|
|
27721
28236
|
env: buildChildEnv9(),
|
|
27722
28237
|
stdio: "ignore",
|
|
27723
28238
|
windowsHide: true
|
|
27724
28239
|
});
|
|
27725
|
-
p.on("error", () =>
|
|
27726
|
-
p.on("close", (code) =>
|
|
28240
|
+
p.on("error", () => resolve18(false));
|
|
28241
|
+
p.on("close", (code) => resolve18(code === 0));
|
|
27727
28242
|
} catch {
|
|
27728
|
-
|
|
28243
|
+
resolve18(false);
|
|
27729
28244
|
}
|
|
27730
28245
|
});
|
|
28246
|
+
return rgAvailabilityCache2;
|
|
27731
28247
|
}
|
|
27732
28248
|
function spawnRgFind(pattern, base) {
|
|
27733
28249
|
const args = ["--files", "--glob", pattern, base];
|
|
@@ -27750,10 +28266,10 @@ function spawnRgFind(pattern, base) {
|
|
|
27750
28266
|
}
|
|
27751
28267
|
});
|
|
27752
28268
|
return {
|
|
27753
|
-
promise: new Promise((
|
|
28269
|
+
promise: new Promise((resolve18, reject) => {
|
|
27754
28270
|
child.on("error", reject);
|
|
27755
28271
|
child.on("close", () => {
|
|
27756
|
-
|
|
28272
|
+
resolve18(buf.split("\n").filter(Boolean));
|
|
27757
28273
|
});
|
|
27758
28274
|
})
|
|
27759
28275
|
};
|
|
@@ -27770,10 +28286,10 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
27770
28286
|
}
|
|
27771
28287
|
for (const e of entries) {
|
|
27772
28288
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
27773
|
-
const full =
|
|
28289
|
+
const full = path35.join(dir, e.name);
|
|
27774
28290
|
try {
|
|
27775
|
-
const
|
|
27776
|
-
if (
|
|
28291
|
+
const stat20 = await fs30.lstat(full);
|
|
28292
|
+
if (stat20.isSymbolicLink()) continue;
|
|
27777
28293
|
} catch {
|
|
27778
28294
|
continue;
|
|
27779
28295
|
}
|
|
@@ -27797,7 +28313,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
27797
28313
|
// src/scaffold.ts
|
|
27798
28314
|
init_util();
|
|
27799
28315
|
import * as fs31 from "node:fs/promises";
|
|
27800
|
-
import * as
|
|
28316
|
+
import * as path36 from "node:path";
|
|
27801
28317
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
|
|
27802
28318
|
var BUILT_IN_TEMPLATES = {
|
|
27803
28319
|
"npm-package": {
|
|
@@ -27948,16 +28464,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
27948
28464
|
let filesCreated = 0;
|
|
27949
28465
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
27950
28466
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
27951
|
-
const joinedPath =
|
|
27952
|
-
const root =
|
|
27953
|
-
const target =
|
|
27954
|
-
const rel =
|
|
27955
|
-
if (rel.startsWith("..") ||
|
|
28467
|
+
const joinedPath = path36.join(cwd, resolvedPath);
|
|
28468
|
+
const root = path36.resolve(ctx.projectRoot);
|
|
28469
|
+
const target = path36.resolve(joinedPath);
|
|
28470
|
+
const rel = path36.relative(root, target);
|
|
28471
|
+
if (rel.startsWith("..") || path36.isAbsolute(rel)) {
|
|
27956
28472
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
27957
28473
|
}
|
|
27958
28474
|
const fullPath = target;
|
|
27959
28475
|
if (!dryRun) {
|
|
27960
|
-
await fs31.mkdir(
|
|
28476
|
+
await fs31.mkdir(path36.dirname(fullPath), { recursive: true });
|
|
27961
28477
|
await atomicWrite4(fullPath, substituteVars(content, name, vars));
|
|
27962
28478
|
}
|
|
27963
28479
|
files.push(resolvedPath);
|
|
@@ -27988,11 +28504,12 @@ function substituteVars(content, name, vars) {
|
|
|
27988
28504
|
}
|
|
27989
28505
|
|
|
27990
28506
|
// src/search.ts
|
|
27991
|
-
import { FetchError as FetchError3, ToolValidationError as
|
|
28507
|
+
import { FetchError as FetchError3, ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
|
|
27992
28508
|
import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
|
|
27993
|
-
import { toErrorMessage as
|
|
28509
|
+
import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
|
|
27994
28510
|
var DEFAULT_NUM = 10;
|
|
27995
28511
|
var MAX_RESULTS = 50;
|
|
28512
|
+
var MAX_SNIPPET_CHARS = 300;
|
|
27996
28513
|
var TIMEOUT_MS3 = 15e3;
|
|
27997
28514
|
var CACHE_TTL_MS = 3e5;
|
|
27998
28515
|
var CACHE_MAX_ENTRIES = 200;
|
|
@@ -28000,7 +28517,7 @@ var cache = /* @__PURE__ */ new Map();
|
|
|
28000
28517
|
var searchTool = {
|
|
28001
28518
|
name: "search",
|
|
28002
28519
|
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.",
|
|
28520
|
+
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
28521
|
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
28522
|
permission: "auto",
|
|
28006
28523
|
mutating: false,
|
|
@@ -28041,7 +28558,7 @@ var searchTool = {
|
|
|
28041
28558
|
},
|
|
28042
28559
|
async *executeStream(input, _ctx, opts) {
|
|
28043
28560
|
if (!input?.query || input.query.trim() === "") {
|
|
28044
|
-
throw new
|
|
28561
|
+
throw new ToolValidationError10({
|
|
28045
28562
|
message: "search: query is required and must be a non-empty string",
|
|
28046
28563
|
field: "query"
|
|
28047
28564
|
});
|
|
@@ -28074,7 +28591,7 @@ var searchTool = {
|
|
|
28074
28591
|
query: input.query,
|
|
28075
28592
|
results: results.slice(0, num),
|
|
28076
28593
|
source: entry.source,
|
|
28077
|
-
truncated: results.length
|
|
28594
|
+
truncated: results.length > num,
|
|
28078
28595
|
cached: true
|
|
28079
28596
|
}
|
|
28080
28597
|
};
|
|
@@ -28086,41 +28603,45 @@ var searchTool = {
|
|
|
28086
28603
|
text: `Querying ${source} for "${input.query}"\u2026`,
|
|
28087
28604
|
data: { source, query: input.query, cached: false }
|
|
28088
28605
|
};
|
|
28089
|
-
let
|
|
28606
|
+
let engine;
|
|
28090
28607
|
let effectiveSource = source;
|
|
28091
28608
|
switch (source) {
|
|
28092
28609
|
case "duckduckgo":
|
|
28093
|
-
|
|
28610
|
+
engine = await duckduckgoSearch(input.query, opts.signal);
|
|
28094
28611
|
break;
|
|
28095
28612
|
case "google":
|
|
28096
|
-
|
|
28613
|
+
engine = await googleSearch(input.query, opts.signal);
|
|
28097
28614
|
break;
|
|
28098
28615
|
case "bing":
|
|
28099
|
-
|
|
28616
|
+
engine = await bingSearch(input.query, opts.signal);
|
|
28100
28617
|
break;
|
|
28101
28618
|
default:
|
|
28102
|
-
throw new
|
|
28619
|
+
throw new ToolValidationError10({
|
|
28103
28620
|
message: `search: unknown source "${source}"`,
|
|
28104
28621
|
field: "source"
|
|
28105
28622
|
});
|
|
28106
28623
|
}
|
|
28107
|
-
let ranked = rankSearchResults(
|
|
28624
|
+
let ranked = rankSearchResults(engine.results, input.query);
|
|
28625
|
+
let engineError = engine.error;
|
|
28108
28626
|
if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
|
|
28109
28627
|
yield {
|
|
28110
28628
|
type: "log",
|
|
28111
28629
|
text: `${source} returned no relevant static results; falling back to duckduckgo`,
|
|
28112
28630
|
data: { source, fallback: "duckduckgo", query: input.query }
|
|
28113
28631
|
};
|
|
28114
|
-
|
|
28115
|
-
ranked = rankSearchResults(
|
|
28632
|
+
const fallback = await duckduckgoSearch(input.query, opts.signal);
|
|
28633
|
+
ranked = rankSearchResults(fallback.results, input.query);
|
|
28634
|
+
engineError = fallback.error;
|
|
28116
28635
|
effectiveSource = "duckduckgo";
|
|
28117
28636
|
}
|
|
28118
28637
|
const finalResults = ranked.slice(0, num);
|
|
28119
|
-
|
|
28120
|
-
|
|
28638
|
+
if (!engineError) {
|
|
28639
|
+
cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
|
|
28640
|
+
pruneCacheEntries();
|
|
28641
|
+
}
|
|
28121
28642
|
yield {
|
|
28122
28643
|
type: "partial_output",
|
|
28123
|
-
text: `${finalResults.length} results from ${effectiveSource}`,
|
|
28644
|
+
text: engineError ? `search failed: ${engineError}` : `${finalResults.length} results from ${effectiveSource}`,
|
|
28124
28645
|
data: { count: finalResults.length, cached: false, source: effectiveSource }
|
|
28125
28646
|
};
|
|
28126
28647
|
yield {
|
|
@@ -28133,8 +28654,9 @@ var searchTool = {
|
|
|
28133
28654
|
snippet: r.snippet
|
|
28134
28655
|
})),
|
|
28135
28656
|
source: effectiveSource,
|
|
28136
|
-
truncated:
|
|
28137
|
-
cached: false
|
|
28657
|
+
truncated: ranked.length > num,
|
|
28658
|
+
cached: false,
|
|
28659
|
+
...engineError ? { error: engineError } : {}
|
|
28138
28660
|
}
|
|
28139
28661
|
};
|
|
28140
28662
|
}
|
|
@@ -28185,18 +28707,18 @@ function shouldFallbackToDuckDuckGo(results, query) {
|
|
|
28185
28707
|
return terms.some((term) => haystack.includes(term));
|
|
28186
28708
|
});
|
|
28187
28709
|
}
|
|
28188
|
-
async function duckduckgoSearch(query,
|
|
28710
|
+
async function duckduckgoSearch(query, signal) {
|
|
28189
28711
|
const encoded = encodeURIComponent(query);
|
|
28190
28712
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
28191
28713
|
try {
|
|
28192
28714
|
const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
|
|
28193
28715
|
const html = await response.text();
|
|
28194
|
-
return parseDuckDuckGo(html,
|
|
28716
|
+
return { results: parseDuckDuckGo(html, MAX_RESULTS) };
|
|
28195
28717
|
} catch (err) {
|
|
28196
28718
|
console.log(
|
|
28197
|
-
JSON.stringify({ level: "debug", event: "search_failed", query, error:
|
|
28719
|
+
JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage9(err) })
|
|
28198
28720
|
);
|
|
28199
|
-
return
|
|
28721
|
+
return { results: [], error: `duckduckgo unreachable: ${toErrorMessage9(err)}` };
|
|
28200
28722
|
}
|
|
28201
28723
|
}
|
|
28202
28724
|
function takeFrom(iter, max) {
|
|
@@ -28230,7 +28752,7 @@ function parseDuckDuckGo(html, num) {
|
|
|
28230
28752
|
results.push({
|
|
28231
28753
|
title: entry.title ?? "",
|
|
28232
28754
|
url: entry.url ?? "",
|
|
28233
|
-
snippet: snippetMatches[i] ?? "",
|
|
28755
|
+
snippet: capSnippet(snippetMatches[i] ?? ""),
|
|
28234
28756
|
score: 1
|
|
28235
28757
|
});
|
|
28236
28758
|
}
|
|
@@ -28255,11 +28777,15 @@ function normalizeDuckDuckGoUrl(raw) {
|
|
|
28255
28777
|
return raw;
|
|
28256
28778
|
}
|
|
28257
28779
|
}
|
|
28258
|
-
async function googleSearch(query,
|
|
28780
|
+
async function googleSearch(query, signal) {
|
|
28259
28781
|
const encoded = encodeURIComponent(query);
|
|
28260
28782
|
const url = `https://www.google.com/search?q=${encoded}&hl=en`;
|
|
28261
|
-
|
|
28262
|
-
|
|
28783
|
+
try {
|
|
28784
|
+
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
|
|
28785
|
+
return { results: parseGoogleResults(html, MAX_RESULTS) };
|
|
28786
|
+
} catch (err) {
|
|
28787
|
+
return { results: [], error: `google unreachable: ${toErrorMessage9(err)}` };
|
|
28788
|
+
}
|
|
28263
28789
|
}
|
|
28264
28790
|
function parseGoogleResults(html, num) {
|
|
28265
28791
|
const results = [];
|
|
@@ -28282,17 +28808,21 @@ function parseGoogleResults(html, num) {
|
|
|
28282
28808
|
results.push({
|
|
28283
28809
|
title: titles[i] ?? "",
|
|
28284
28810
|
url: urls[i] ?? "",
|
|
28285
|
-
snippet: snippets[i] ?? "",
|
|
28811
|
+
snippet: capSnippet(snippets[i] ?? ""),
|
|
28286
28812
|
score: 1
|
|
28287
28813
|
});
|
|
28288
28814
|
}
|
|
28289
28815
|
return results;
|
|
28290
28816
|
}
|
|
28291
|
-
async function bingSearch(query,
|
|
28817
|
+
async function bingSearch(query, signal) {
|
|
28292
28818
|
const encoded = encodeURIComponent(query);
|
|
28293
28819
|
const url = `https://www.bing.com/search?q=${encoded}`;
|
|
28294
|
-
|
|
28295
|
-
|
|
28820
|
+
try {
|
|
28821
|
+
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
|
|
28822
|
+
return { results: parseBingResults(html, MAX_RESULTS) };
|
|
28823
|
+
} catch (err) {
|
|
28824
|
+
return { results: [], error: `bing unreachable: ${toErrorMessage9(err)}` };
|
|
28825
|
+
}
|
|
28296
28826
|
}
|
|
28297
28827
|
function parseBingResults(html, num) {
|
|
28298
28828
|
const results = [];
|
|
@@ -28305,7 +28835,7 @@ function parseBingResults(html, num) {
|
|
|
28305
28835
|
const title = stripTags(expectDefined9(titleMatch[2]));
|
|
28306
28836
|
if (!href || !title) return [];
|
|
28307
28837
|
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))) : "";
|
|
28838
|
+
const snippet = snippetMatch ? capSnippet(stripTags(expectDefined9(snippetMatch.at(-1)))) : "";
|
|
28309
28839
|
return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
|
|
28310
28840
|
}), num);
|
|
28311
28841
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -28372,17 +28902,20 @@ function anySignal(...signals) {
|
|
|
28372
28902
|
function stripTags(html) {
|
|
28373
28903
|
return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
|
|
28374
28904
|
}
|
|
28905
|
+
function capSnippet(snippet) {
|
|
28906
|
+
return snippet.length > MAX_SNIPPET_CHARS ? `${snippet.slice(0, MAX_SNIPPET_CHARS - 1)}\u2026` : snippet;
|
|
28907
|
+
}
|
|
28375
28908
|
function decodeHtmlEntities(text) {
|
|
28376
28909
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
28377
28910
|
}
|
|
28378
28911
|
|
|
28379
28912
|
// src/set-working-dir.ts
|
|
28380
28913
|
import * as fs32 from "node:fs/promises";
|
|
28381
|
-
import { toErrorMessage as
|
|
28914
|
+
import { toErrorMessage as toErrorMessage10 } from "@wrongstack/core/utils";
|
|
28382
28915
|
var setWorkingDirTool = {
|
|
28383
28916
|
name: "set_working_dir",
|
|
28384
28917
|
category: "Context",
|
|
28385
|
-
description: "Change the current working directory for
|
|
28918
|
+
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
28919
|
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
28920
|
permission: "confirm",
|
|
28388
28921
|
mutating: true,
|
|
@@ -28412,19 +28945,23 @@ var setWorkingDirTool = {
|
|
|
28412
28945
|
} catch (err) {
|
|
28413
28946
|
return {
|
|
28414
28947
|
current: ctx.workingDir,
|
|
28415
|
-
error:
|
|
28948
|
+
error: toErrorMessage10(err)
|
|
28416
28949
|
};
|
|
28417
28950
|
}
|
|
28951
|
+
let isDirectory = false;
|
|
28418
28952
|
try {
|
|
28419
|
-
await fs32.
|
|
28953
|
+
isDirectory = (await fs32.stat(resolved)).isDirectory();
|
|
28420
28954
|
} catch {
|
|
28955
|
+
isDirectory = false;
|
|
28956
|
+
}
|
|
28957
|
+
if (!isDirectory) {
|
|
28421
28958
|
try {
|
|
28422
28959
|
ctx.setWorkingDir(previous);
|
|
28423
28960
|
} catch {
|
|
28424
28961
|
}
|
|
28425
28962
|
return {
|
|
28426
28963
|
current: ctx.workingDir,
|
|
28427
|
-
error: `Directory does not exist: ${resolved}`
|
|
28964
|
+
error: `Directory does not exist (or is not a directory): ${resolved}`
|
|
28428
28965
|
};
|
|
28429
28966
|
}
|
|
28430
28967
|
return {
|
|
@@ -28879,6 +29416,7 @@ var taskTool = {
|
|
|
28879
29416
|
inProgress: 0
|
|
28880
29417
|
};
|
|
28881
29418
|
}
|
|
29419
|
+
ctx.meta["task.path.resolved"] = taskPath;
|
|
28882
29420
|
if (todosToReplace) {
|
|
28883
29421
|
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
28884
29422
|
signal: AbortSignal.timeout(3e4)
|
|
@@ -28903,6 +29441,7 @@ var taskTool = {
|
|
|
28903
29441
|
formatted = formatPlan2(updated);
|
|
28904
29442
|
return updated;
|
|
28905
29443
|
});
|
|
29444
|
+
ctx.meta["plan.path.resolved"] = planPath;
|
|
28906
29445
|
} catch (err) {
|
|
28907
29446
|
return {
|
|
28908
29447
|
ok: false,
|
|
@@ -28946,7 +29485,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
|
|
|
28946
29485
|
init_spawn_stream();
|
|
28947
29486
|
init_util();
|
|
28948
29487
|
init_legacy_bridge();
|
|
28949
|
-
import * as
|
|
29488
|
+
import * as path37 from "node:path";
|
|
28950
29489
|
var testTool = {
|
|
28951
29490
|
name: "test",
|
|
28952
29491
|
category: "Code Quality",
|
|
@@ -29049,11 +29588,11 @@ var testTool = {
|
|
|
29049
29588
|
}
|
|
29050
29589
|
};
|
|
29051
29590
|
async function detectRunner(cwd) {
|
|
29052
|
-
const { stat:
|
|
29591
|
+
const { stat: stat20 } = await import("node:fs/promises");
|
|
29053
29592
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
29054
29593
|
for (const f of candidates) {
|
|
29055
29594
|
try {
|
|
29056
|
-
await
|
|
29595
|
+
await stat20(path37.join(cwd, f));
|
|
29057
29596
|
if (f.includes("vitest")) return "vitest";
|
|
29058
29597
|
if (f.includes("jest")) return "jest";
|
|
29059
29598
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -29431,7 +29970,7 @@ var toolUseTool = {
|
|
|
29431
29970
|
// src/tree.ts
|
|
29432
29971
|
init_util();
|
|
29433
29972
|
import * as fs33 from "node:fs/promises";
|
|
29434
|
-
import * as
|
|
29973
|
+
import * as path38 from "node:path";
|
|
29435
29974
|
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
29436
29975
|
var DEFAULT_IGNORE5 = /* @__PURE__ */ new Set([
|
|
29437
29976
|
...DEFAULT_WALK_IGNORE_DIRS4,
|
|
@@ -29451,6 +29990,7 @@ var treeTool = {
|
|
|
29451
29990
|
mutating: false,
|
|
29452
29991
|
capabilities: ["fs.read"],
|
|
29453
29992
|
icon: "tree",
|
|
29993
|
+
maxOutputBytes: 262144,
|
|
29454
29994
|
timeoutMs: 15e3,
|
|
29455
29995
|
inputSchema: {
|
|
29456
29996
|
type: "object",
|
|
@@ -29599,17 +30139,15 @@ async function walkDir(dir, depth, opts) {
|
|
|
29599
30139
|
if (opts.exclude.has(e.name)) return false;
|
|
29600
30140
|
return true;
|
|
29601
30141
|
});
|
|
29602
|
-
|
|
29603
|
-
|
|
29604
|
-
|
|
29605
|
-
|
|
29606
|
-
|
|
29607
|
-
|
|
29608
|
-
|
|
29609
|
-
|
|
29610
|
-
|
|
29611
|
-
opts.onProgress?.();
|
|
29612
|
-
}
|
|
30142
|
+
let dirCount = 0;
|
|
30143
|
+
let fileCount = 0;
|
|
30144
|
+
for (const e of filtered) {
|
|
30145
|
+
if (e.isDirectory()) dirCount++;
|
|
30146
|
+
else if (e.isFile()) fileCount++;
|
|
30147
|
+
}
|
|
30148
|
+
opts.totalDirs.value += dirCount;
|
|
30149
|
+
opts.totalFiles.value += fileCount;
|
|
30150
|
+
opts.onProgress?.();
|
|
29613
30151
|
const items = filtered.sort((a, b) => {
|
|
29614
30152
|
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
29615
30153
|
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
@@ -29637,7 +30175,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
29637
30175
|
opts.retention.outputBytes += lineBytes;
|
|
29638
30176
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
29639
30177
|
const childPrefix = opts.prefix + connector;
|
|
29640
|
-
await walkDir(
|
|
30178
|
+
await walkDir(path38.join(dir, entry.name), depth + 1, {
|
|
29641
30179
|
...opts,
|
|
29642
30180
|
prefix: childPrefix,
|
|
29643
30181
|
isLast
|
|
@@ -29650,7 +30188,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
29650
30188
|
init_spawn_stream();
|
|
29651
30189
|
init_util();
|
|
29652
30190
|
init_legacy_bridge();
|
|
29653
|
-
import * as
|
|
30191
|
+
import * as path39 from "node:path";
|
|
29654
30192
|
var typecheckTool = {
|
|
29655
30193
|
name: "typecheck",
|
|
29656
30194
|
category: "Code Quality",
|
|
@@ -29672,11 +30210,7 @@ var typecheckTool = {
|
|
|
29672
30210
|
},
|
|
29673
30211
|
all: {
|
|
29674
30212
|
type: "boolean",
|
|
29675
|
-
description: "Type-check all
|
|
29676
|
-
},
|
|
29677
|
-
json: {
|
|
29678
|
-
type: "boolean",
|
|
29679
|
-
description: "Emit JSON output from tsc (default: false)"
|
|
30213
|
+
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
30214
|
}
|
|
29681
30215
|
}
|
|
29682
30216
|
},
|
|
@@ -29712,29 +30246,42 @@ var typecheckTool = {
|
|
|
29712
30246
|
};
|
|
29713
30247
|
return;
|
|
29714
30248
|
}
|
|
29715
|
-
let
|
|
30249
|
+
let cmd;
|
|
30250
|
+
let cmdArgs;
|
|
29716
30251
|
let project;
|
|
29717
30252
|
if (input.all) {
|
|
29718
|
-
args = ["--noEmit"];
|
|
29719
30253
|
project = "workspace";
|
|
30254
|
+
const tscArgs = ["--noEmit"];
|
|
30255
|
+
if (input.strict) tscArgs.push("--strict");
|
|
30256
|
+
const manager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
30257
|
+
if (manager === "pnpm") {
|
|
30258
|
+
cmd = "pnpm";
|
|
30259
|
+
cmdArgs = ["-r", "--no-bail", "exec", "tsc", ...tscArgs];
|
|
30260
|
+
} else {
|
|
30261
|
+
cmd = "npx";
|
|
30262
|
+
cmdArgs = ["tsc", ...tscArgs];
|
|
30263
|
+
}
|
|
29720
30264
|
} else {
|
|
29721
30265
|
const tsconfig = input.project ? safeResolve(input.project, ctx) : await findTsConfig(cwd);
|
|
29722
|
-
|
|
29723
|
-
if (input.strict)
|
|
29724
|
-
if (tsconfig)
|
|
30266
|
+
const tscArgs = ["--noEmit"];
|
|
30267
|
+
if (input.strict) tscArgs.push("--strict");
|
|
30268
|
+
if (tsconfig) tscArgs.push("--project", tsconfig);
|
|
29725
30269
|
project = tsconfig ?? "default";
|
|
30270
|
+
cmd = "npx";
|
|
30271
|
+
cmdArgs = ["tsc", ...tscArgs];
|
|
29726
30272
|
}
|
|
29727
|
-
|
|
29728
|
-
yield { type: "log", text: `tsc ${args.join(" ")}`, data: { project } };
|
|
30273
|
+
yield { type: "log", text: `${cmd} ${cmdArgs.join(" ")}`, data: { project } };
|
|
29729
30274
|
const result = yield* spawnStream({
|
|
29730
|
-
cmd
|
|
29731
|
-
args:
|
|
30275
|
+
cmd,
|
|
30276
|
+
args: cmdArgs,
|
|
29732
30277
|
cwd,
|
|
29733
30278
|
signal: opts.signal,
|
|
29734
30279
|
maxBytes: 2e5
|
|
29735
30280
|
});
|
|
29736
|
-
const
|
|
29737
|
-
|
|
30281
|
+
const combined = `${result.stdout}
|
|
30282
|
+
${result.stderr}`;
|
|
30283
|
+
const errors = [...combined.matchAll(/^.*\berror TS\d+:/gm)].length;
|
|
30284
|
+
const warnings = [...combined.matchAll(/^.*\bwarning TS\d+:/gm)].length;
|
|
29738
30285
|
yield {
|
|
29739
30286
|
type: "final",
|
|
29740
30287
|
output: {
|
|
@@ -29749,12 +30296,12 @@ var typecheckTool = {
|
|
|
29749
30296
|
}
|
|
29750
30297
|
};
|
|
29751
30298
|
async function findTsConfig(cwd) {
|
|
29752
|
-
const { stat:
|
|
30299
|
+
const { stat: stat20 } = await import("node:fs/promises");
|
|
29753
30300
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
29754
30301
|
for (const f of candidates) {
|
|
29755
30302
|
try {
|
|
29756
|
-
const s = await
|
|
29757
|
-
if (s.isFile()) return
|
|
30303
|
+
const s = await stat20(path39.join(cwd, f));
|
|
30304
|
+
if (s.isFile()) return path39.join(cwd, f);
|
|
29758
30305
|
} catch {
|
|
29759
30306
|
}
|
|
29760
30307
|
}
|
|
@@ -29763,21 +30310,32 @@ async function findTsConfig(cwd) {
|
|
|
29763
30310
|
|
|
29764
30311
|
// src/write.ts
|
|
29765
30312
|
import * as fs34 from "node:fs/promises";
|
|
29766
|
-
import { ToolValidationError as
|
|
29767
|
-
import {
|
|
30313
|
+
import { ToolValidationError as ToolValidationError11 } from "@wrongstack/core/types";
|
|
30314
|
+
import {
|
|
30315
|
+
atomicWrite as atomicWrite5,
|
|
30316
|
+
detectNewlineStyle as detectNewlineStyle3,
|
|
30317
|
+
normalizeToLf as normalizeToLf3,
|
|
30318
|
+
toStyle as toStyle3,
|
|
30319
|
+
unifiedDiff as unifiedDiff3
|
|
30320
|
+
} from "@wrongstack/core/utils";
|
|
29768
30321
|
init_util();
|
|
30322
|
+
var MAX_DIFF_BYTES3 = 262144;
|
|
29769
30323
|
var writeTool = {
|
|
29770
30324
|
name: "write",
|
|
29771
30325
|
category: "Filesystem",
|
|
29772
30326
|
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-
|
|
30327
|
+
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
30328
|
selection: {
|
|
29775
30329
|
doNotUseWhen: "making a precise change to part of an existing file.",
|
|
29776
30330
|
useInstead: ["edit"]
|
|
29777
30331
|
},
|
|
29778
30332
|
permission: "confirm",
|
|
30333
|
+
// WS-046: gives permission decisions something to key on — the file being
|
|
30334
|
+
// written, so trust rules can scope by path.
|
|
30335
|
+
subjectKey: "path",
|
|
29779
30336
|
mutating: true,
|
|
29780
30337
|
timeoutMs: 5e3,
|
|
30338
|
+
maxOutputBytes: 262144,
|
|
29781
30339
|
capabilities: ["fs.write"],
|
|
29782
30340
|
icon: "file",
|
|
29783
30341
|
inputSchema: {
|
|
@@ -29813,13 +30371,13 @@ async function writeFile6(input, ctx, signal) {
|
|
|
29813
30371
|
}
|
|
29814
30372
|
async function prepareWrite(input, ctx) {
|
|
29815
30373
|
if (!input?.path) {
|
|
29816
|
-
throw new
|
|
30374
|
+
throw new ToolValidationError11({
|
|
29817
30375
|
message: "write: path is required",
|
|
29818
30376
|
field: "path"
|
|
29819
30377
|
});
|
|
29820
30378
|
}
|
|
29821
30379
|
if (input.content === void 0) {
|
|
29822
|
-
throw new
|
|
30380
|
+
throw new ToolValidationError11({
|
|
29823
30381
|
message: "write: content is required",
|
|
29824
30382
|
field: "content"
|
|
29825
30383
|
});
|
|
@@ -29828,12 +30386,12 @@ async function prepareWrite(input, ctx) {
|
|
|
29828
30386
|
let existed = false;
|
|
29829
30387
|
let prev = "";
|
|
29830
30388
|
try {
|
|
29831
|
-
const
|
|
29832
|
-
existed =
|
|
30389
|
+
const stat20 = await fs34.stat(absPath);
|
|
30390
|
+
existed = stat20.isFile();
|
|
29833
30391
|
if (existed) {
|
|
29834
30392
|
if (!ctx.hasRead(absPath)) {
|
|
29835
30393
|
prev = await fs34.readFile(absPath, "utf8");
|
|
29836
|
-
ctx.recordRead(absPath,
|
|
30394
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "write", sha256hex(prev));
|
|
29837
30395
|
} else {
|
|
29838
30396
|
prev = await fs34.readFile(absPath, "utf8");
|
|
29839
30397
|
}
|
|
@@ -29846,31 +30404,42 @@ async function prepareWrite(input, ctx) {
|
|
|
29846
30404
|
return { absPath, existed, prev };
|
|
29847
30405
|
}
|
|
29848
30406
|
async function finishWrite(input, ctx, prepared, signal) {
|
|
30407
|
+
const content = prepared.existed ? toStyle3(normalizeToLf3(input.content), detectNewlineStyle3(prepared.prev)) : input.content;
|
|
29849
30408
|
signal?.throwIfAborted();
|
|
29850
|
-
await atomicWrite5(prepared.absPath,
|
|
29851
|
-
const
|
|
29852
|
-
+ (new file, ${
|
|
29853
|
-
const
|
|
29854
|
-
|
|
30409
|
+
await atomicWrite5(prepared.absPath, content);
|
|
30410
|
+
const rawDiff = prepared.existed ? unifiedDiff3(prepared.prev, content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
30411
|
+
+ (new file, ${content.split("\n").length} lines)`;
|
|
30412
|
+
const { text: diff, truncated: diffTruncated } = truncateDiffPayload(rawDiff, MAX_DIFF_BYTES3);
|
|
30413
|
+
const stat20 = await fs34.stat(prepared.absPath);
|
|
30414
|
+
ctx.recordRead(prepared.absPath, stat20.mtimeMs, "write", sha256hex(content));
|
|
29855
30415
|
ctx.session.recordFileChange({
|
|
29856
30416
|
path: prepared.absPath,
|
|
29857
30417
|
action: prepared.existed ? "modified" : "created",
|
|
29858
30418
|
before: prepared.existed ? prepared.prev : null,
|
|
29859
|
-
after:
|
|
30419
|
+
after: content
|
|
29860
30420
|
});
|
|
29861
30421
|
const syntax = await checkSyntax(
|
|
29862
30422
|
prepared.absPath,
|
|
29863
|
-
|
|
30423
|
+
content,
|
|
29864
30424
|
prepared.existed ? prepared.prev : void 0
|
|
29865
30425
|
).catch(() => void 0);
|
|
29866
30426
|
const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
|
|
30427
|
+
const notes = [];
|
|
30428
|
+
if (diffTruncated) {
|
|
30429
|
+
notes.push("Diff truncated to the 256 KiB output budget \u2014 the full write is on disk.");
|
|
30430
|
+
}
|
|
30431
|
+
if (hasSyntaxErrors) {
|
|
30432
|
+
notes.push(
|
|
30433
|
+
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.`
|
|
30434
|
+
);
|
|
30435
|
+
}
|
|
29867
30436
|
return {
|
|
29868
30437
|
path: prepared.absPath,
|
|
29869
|
-
bytes_written: Buffer.byteLength(
|
|
30438
|
+
bytes_written: Buffer.byteLength(content, "utf8"),
|
|
29870
30439
|
created: !prepared.existed,
|
|
29871
30440
|
diff,
|
|
29872
30441
|
syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
|
|
29873
|
-
note:
|
|
30442
|
+
note: notes.length > 0 ? notes.join("\n") : void 0
|
|
29874
30443
|
};
|
|
29875
30444
|
}
|
|
29876
30445
|
|
|
@@ -29995,7 +30564,7 @@ init_circuit_breaker();
|
|
|
29995
30564
|
init_languages();
|
|
29996
30565
|
|
|
29997
30566
|
// src/memory.ts
|
|
29998
|
-
import { ToolValidationError as
|
|
30567
|
+
import { ToolValidationError as ToolValidationError12 } from "@wrongstack/core/types";
|
|
29999
30568
|
function rememberTool(memory) {
|
|
30000
30569
|
return {
|
|
30001
30570
|
name: "remember",
|
|
@@ -30039,7 +30608,7 @@ function rememberTool(memory) {
|
|
|
30039
30608
|
},
|
|
30040
30609
|
async execute(input) {
|
|
30041
30610
|
if (!input?.text) {
|
|
30042
|
-
throw new
|
|
30611
|
+
throw new ToolValidationError12({
|
|
30043
30612
|
message: "remember: text is required",
|
|
30044
30613
|
field: "text"
|
|
30045
30614
|
});
|
|
@@ -30058,28 +30627,48 @@ function forgetTool(memory) {
|
|
|
30058
30627
|
return {
|
|
30059
30628
|
name: "forget",
|
|
30060
30629
|
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.",
|
|
30630
|
+
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.",
|
|
30631
|
+
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
30632
|
permission: "confirm",
|
|
30633
|
+
// WS-046: gives permission decisions something to key on — the substring
|
|
30634
|
+
// being forgotten.
|
|
30635
|
+
subjectKey: "query",
|
|
30064
30636
|
mutating: true,
|
|
30065
30637
|
timeoutMs: 2e3,
|
|
30066
30638
|
capabilities: ["memory.delete"],
|
|
30639
|
+
icon: "settings",
|
|
30067
30640
|
inputSchema: {
|
|
30068
30641
|
type: "object",
|
|
30069
30642
|
properties: {
|
|
30070
30643
|
query: { type: "string" },
|
|
30071
|
-
scope: { type: "string", enum: ["project-agents", "project-memory", "user-memory"] }
|
|
30644
|
+
scope: { type: "string", enum: ["project-agents", "project-memory", "user-memory"] },
|
|
30645
|
+
dry_run: {
|
|
30646
|
+
type: "boolean",
|
|
30647
|
+
description: "When true, return the matched entries (capped at 20) WITHOUT deleting them. Default false."
|
|
30648
|
+
}
|
|
30072
30649
|
},
|
|
30073
30650
|
required: ["query"]
|
|
30074
30651
|
},
|
|
30075
30652
|
async execute(input) {
|
|
30076
30653
|
if (!input?.query) {
|
|
30077
|
-
throw new
|
|
30654
|
+
throw new ToolValidationError12({
|
|
30078
30655
|
message: "forget: query is required",
|
|
30079
30656
|
field: "query"
|
|
30080
30657
|
});
|
|
30081
30658
|
}
|
|
30082
30659
|
const scope = input.scope ?? "project-memory";
|
|
30660
|
+
if (input.dry_run) {
|
|
30661
|
+
const entries = await memory.list(scope);
|
|
30662
|
+
const needle = input.query.toLowerCase();
|
|
30663
|
+
const matching = entries.filter((entry) => entry.text.toLowerCase().includes(needle));
|
|
30664
|
+
return {
|
|
30665
|
+
removed: 0,
|
|
30666
|
+
scope,
|
|
30667
|
+
dryRun: true,
|
|
30668
|
+
matched: matching.length,
|
|
30669
|
+
matches: matching.slice(0, 20).map((entry) => entry.text)
|
|
30670
|
+
};
|
|
30671
|
+
}
|
|
30083
30672
|
const removed = await memory.forget(input.query, scope);
|
|
30084
30673
|
return { removed, scope };
|
|
30085
30674
|
}
|
|
@@ -30116,7 +30705,7 @@ function searchMemoryTool(memory) {
|
|
|
30116
30705
|
},
|
|
30117
30706
|
async execute(input) {
|
|
30118
30707
|
if (!input?.query) {
|
|
30119
|
-
throw new
|
|
30708
|
+
throw new ToolValidationError12({
|
|
30120
30709
|
message: "search_memory: query is required",
|
|
30121
30710
|
field: "query"
|
|
30122
30711
|
});
|
|
@@ -30168,7 +30757,7 @@ function relatedMemoryTool(memory) {
|
|
|
30168
30757
|
},
|
|
30169
30758
|
async execute(input) {
|
|
30170
30759
|
if (!input?.text) {
|
|
30171
|
-
throw new
|
|
30760
|
+
throw new ToolValidationError12({
|
|
30172
30761
|
message: "find_related_memories: text is required",
|
|
30173
30762
|
field: "text"
|
|
30174
30763
|
});
|
|
@@ -30204,6 +30793,9 @@ function createModeTool(modeStore) {
|
|
|
30204
30793
|
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
30794
|
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
30795
|
permission: "confirm",
|
|
30796
|
+
// WS-046: gives permission decisions something to key on — the mode being
|
|
30797
|
+
// activated. Permission semantics are unchanged.
|
|
30798
|
+
subjectKey: "mode",
|
|
30207
30799
|
mutating: true,
|
|
30208
30800
|
timeoutMs: 5e3,
|
|
30209
30801
|
capabilities: ["session.mode"],
|
|
@@ -30942,14 +31534,14 @@ function createGlobalPsSlashCommand() {
|
|
|
30942
31534
|
|
|
30943
31535
|
// src/skill.ts
|
|
30944
31536
|
import * as fs35 from "node:fs/promises";
|
|
30945
|
-
import * as
|
|
31537
|
+
import * as path40 from "node:path";
|
|
30946
31538
|
import {
|
|
30947
31539
|
missingRequiredRuntimeTools,
|
|
30948
31540
|
missingRuntimeCapabilities,
|
|
30949
31541
|
runtimeToolReferencesFromText
|
|
30950
31542
|
} from "@wrongstack/core/agent-catalog";
|
|
30951
31543
|
import { SKILL_LIMITS, stripFrontmatter } from "@wrongstack/core/skills";
|
|
30952
|
-
import { ToolValidationError as
|
|
31544
|
+
import { ToolValidationError as ToolValidationError13 } from "@wrongstack/core/types";
|
|
30953
31545
|
var MAX_BODY_CHARS = SKILL_LIMITS.MAX_SKILL_BODY_CHARS;
|
|
30954
31546
|
var MAX_RESOURCE_CHARS = SKILL_LIMITS.MAX_RESOURCE_CHARS;
|
|
30955
31547
|
var MAX_LISTED_RESOURCES = SKILL_LIMITS.MAX_LISTED_RESOURCES;
|
|
@@ -30982,11 +31574,11 @@ function makeSkillTool(skillLoader) {
|
|
|
30982
31574
|
async execute(input, ctx) {
|
|
30983
31575
|
const name = input?.name?.trim();
|
|
30984
31576
|
if (!name) {
|
|
30985
|
-
throw new
|
|
31577
|
+
throw new ToolValidationError13({ message: "skill: name is required", field: "name" });
|
|
30986
31578
|
}
|
|
30987
31579
|
const manifest = await skillLoader.find(name);
|
|
30988
31580
|
if (!manifest) {
|
|
30989
|
-
throw new
|
|
31581
|
+
throw new ToolValidationError13({
|
|
30990
31582
|
message: `skill "${name}" not found \u2014 use /skill to list available skills`,
|
|
30991
31583
|
field: "name"
|
|
30992
31584
|
});
|
|
@@ -30998,7 +31590,7 @@ function makeSkillTool(skillLoader) {
|
|
|
30998
31590
|
);
|
|
30999
31591
|
const missingTools = missingRequiredRuntimeTools(manifest.requiredTools, availableToolNames);
|
|
31000
31592
|
if (missingCapabilities.length > 0 || missingTools.length > 0) {
|
|
31001
|
-
throw new
|
|
31593
|
+
throw new ToolValidationError13({
|
|
31002
31594
|
message: `skill "${name}" is unavailable in this runtime; ` + [
|
|
31003
31595
|
missingCapabilities.length > 0 ? `missing capabilities: ${missingCapabilities.join(", ")}` : "",
|
|
31004
31596
|
missingTools.length > 0 ? `missing tools: ${missingTools.join(", ")}` : ""
|
|
@@ -31006,7 +31598,7 @@ function makeSkillTool(skillLoader) {
|
|
|
31006
31598
|
field: "name"
|
|
31007
31599
|
});
|
|
31008
31600
|
}
|
|
31009
|
-
const dir =
|
|
31601
|
+
const dir = path40.dirname(manifest.path);
|
|
31010
31602
|
let loadedResource;
|
|
31011
31603
|
if (input.resource?.trim()) {
|
|
31012
31604
|
loadedResource = await loadResource(dir, input.resource.trim());
|
|
@@ -31016,12 +31608,7 @@ function makeSkillTool(skillLoader) {
|
|
|
31016
31608
|
runtimeToolReferencesFromText(raw),
|
|
31017
31609
|
availableToolNames
|
|
31018
31610
|
);
|
|
31019
|
-
|
|
31020
|
-
throw new ToolValidationError10({
|
|
31021
|
-
message: `skill "${name}" references unregistered tools: ${missingBodyTools.join(", ")}`,
|
|
31022
|
-
field: "name"
|
|
31023
|
-
});
|
|
31024
|
-
}
|
|
31611
|
+
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
31612
|
const body = stripFrontmatter(raw).trim().slice(0, MAX_BODY_CHARS);
|
|
31026
31613
|
const resources = loadedResource ? [] : await listResources(dir);
|
|
31027
31614
|
try {
|
|
@@ -31038,44 +31625,48 @@ function makeSkillTool(skillLoader) {
|
|
|
31038
31625
|
body,
|
|
31039
31626
|
resources,
|
|
31040
31627
|
dir,
|
|
31041
|
-
loadedResource
|
|
31628
|
+
loadedResource,
|
|
31629
|
+
warning
|
|
31042
31630
|
};
|
|
31043
31631
|
},
|
|
31044
31632
|
serialize(output) {
|
|
31633
|
+
const warningLine = output.warning ? `
|
|
31634
|
+
|
|
31635
|
+
${output.warning}` : "";
|
|
31045
31636
|
if (output.loadedResource) {
|
|
31046
31637
|
const lr = output.loadedResource;
|
|
31047
31638
|
const note = lr.truncated ? ` (truncated to ${lr.content.length} chars of ${lr.bytes} B)` : "";
|
|
31048
31639
|
return `# Resource: ${output.name}/${lr.rel}
|
|
31049
31640
|
(abs path: ${lr.absPath})${note}
|
|
31050
31641
|
|
|
31051
|
-
${lr.content}`;
|
|
31642
|
+
${lr.content}${warningLine}`;
|
|
31052
31643
|
}
|
|
31053
31644
|
const head = `# Skill: ${output.name}
|
|
31054
31645
|
${output.description}
|
|
31055
31646
|
|
|
31056
31647
|
${output.body}`;
|
|
31057
|
-
if (output.resources.length === 0) return head
|
|
31648
|
+
if (output.resources.length === 0) return `${head}${warningLine}`;
|
|
31058
31649
|
const listing = output.resources.map((r) => `- ${r.path} (${r.bytes} B)`).join("\n");
|
|
31059
31650
|
return `${head}
|
|
31060
31651
|
|
|
31061
31652
|
## Bundled resources (load on demand)
|
|
31062
31653
|
Load any with: \`skill({ name: "${output.name}", resource: "<path>" })\`. Run scripts via bash using their abs path under ${output.dir}.
|
|
31063
|
-
${listing}`;
|
|
31654
|
+
${listing}${warningLine}`;
|
|
31064
31655
|
}
|
|
31065
31656
|
};
|
|
31066
31657
|
}
|
|
31067
31658
|
async function loadResource(skillDir, rel) {
|
|
31068
31659
|
const norm = rel.replace(/\\/g, "/");
|
|
31069
|
-
if (
|
|
31070
|
-
throw new
|
|
31660
|
+
if (path40.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
|
|
31661
|
+
throw new ToolValidationError13({
|
|
31071
31662
|
message: `skill: invalid resource path "${rel}"`,
|
|
31072
31663
|
field: "resource"
|
|
31073
31664
|
});
|
|
31074
31665
|
}
|
|
31075
|
-
const absPath =
|
|
31076
|
-
const root =
|
|
31077
|
-
if (absPath !== root && !absPath.startsWith(root +
|
|
31078
|
-
throw new
|
|
31666
|
+
const absPath = path40.resolve(skillDir, rel);
|
|
31667
|
+
const root = path40.resolve(skillDir);
|
|
31668
|
+
if (absPath !== root && !absPath.startsWith(root + path40.sep)) {
|
|
31669
|
+
throw new ToolValidationError13({
|
|
31079
31670
|
message: `skill: resource "${rel}" escapes the skill directory`,
|
|
31080
31671
|
field: "resource"
|
|
31081
31672
|
});
|
|
@@ -31086,13 +31677,13 @@ async function loadResource(skillDir, rel) {
|
|
|
31086
31677
|
realRoot = await fs35.realpath(root);
|
|
31087
31678
|
realPath = await fs35.realpath(absPath);
|
|
31088
31679
|
} catch {
|
|
31089
|
-
throw new
|
|
31680
|
+
throw new ToolValidationError13({
|
|
31090
31681
|
message: `skill: resource "${rel}" not readable`,
|
|
31091
31682
|
field: "resource"
|
|
31092
31683
|
});
|
|
31093
31684
|
}
|
|
31094
|
-
if (realPath !== realRoot && !realPath.startsWith(realRoot +
|
|
31095
|
-
throw new
|
|
31685
|
+
if (realPath !== realRoot && !realPath.startsWith(realRoot + path40.sep)) {
|
|
31686
|
+
throw new ToolValidationError13({
|
|
31096
31687
|
message: `skill: resource "${rel}" resolves outside the skill directory`,
|
|
31097
31688
|
field: "resource"
|
|
31098
31689
|
});
|
|
@@ -31101,7 +31692,7 @@ async function loadResource(skillDir, rel) {
|
|
|
31101
31692
|
try {
|
|
31102
31693
|
buf = await fs35.readFile(realPath);
|
|
31103
31694
|
} catch {
|
|
31104
|
-
throw new
|
|
31695
|
+
throw new ToolValidationError13({
|
|
31105
31696
|
message: `skill: resource "${rel}" not readable`,
|
|
31106
31697
|
field: "resource"
|
|
31107
31698
|
});
|
|
@@ -31134,7 +31725,7 @@ async function walk(root, dir, out) {
|
|
|
31134
31725
|
}
|
|
31135
31726
|
for (const e of entries) {
|
|
31136
31727
|
if (out.length >= MAX_LISTED_RESOURCES) return;
|
|
31137
|
-
const fullPath =
|
|
31728
|
+
const fullPath = path40.join(dir, e.name);
|
|
31138
31729
|
let isDir = e.isDirectory();
|
|
31139
31730
|
if (e.isSymbolicLink()) {
|
|
31140
31731
|
try {
|
|
@@ -31149,9 +31740,9 @@ async function walk(root, dir, out) {
|
|
|
31149
31740
|
} else if (e.isFile()) {
|
|
31150
31741
|
if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
|
|
31151
31742
|
try {
|
|
31152
|
-
const
|
|
31153
|
-
const rel =
|
|
31154
|
-
out.push({ path: rel, bytes:
|
|
31743
|
+
const stat20 = await fs35.stat(fullPath);
|
|
31744
|
+
const rel = path40.relative(root, fullPath).split(path40.sep).join("/");
|
|
31745
|
+
out.push({ path: rel, bytes: stat20.size });
|
|
31155
31746
|
} catch {
|
|
31156
31747
|
}
|
|
31157
31748
|
}
|
|
@@ -31244,9 +31835,15 @@ var TOOL_ICON_MAP = {
|
|
|
31244
31835
|
"codebase-index": "index",
|
|
31245
31836
|
"codebase-search": "index",
|
|
31246
31837
|
"codebase-stats": "index",
|
|
31838
|
+
"codebase-incoming-calls": "index",
|
|
31839
|
+
"codebase-outgoing-calls": "index",
|
|
31840
|
+
"dead-code-scan": "index",
|
|
31247
31841
|
codebase_index: "index",
|
|
31248
31842
|
codebase_search: "index",
|
|
31249
31843
|
codebase_stats: "index",
|
|
31844
|
+
codebase_incoming_calls: "index",
|
|
31845
|
+
codebase_outgoing_calls: "index",
|
|
31846
|
+
dead_code_scan: "index",
|
|
31250
31847
|
// Data
|
|
31251
31848
|
json: "json",
|
|
31252
31849
|
parse: "json",
|