@wrongstack/tools 0.305.0 → 0.306.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_shell-pick.d.ts +4 -5
- package/dist/_util.d.ts +22 -5
- package/dist/audit.d.ts +0 -1
- package/dist/audit.js +135 -46
- package/dist/bash.js +61 -37
- package/dist/browser/index.js +29 -9
- package/dist/browser/types.d.ts +7 -1
- package/dist/builtin.js +1280 -728
- package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
- package/dist/codebase-index/index.d.ts +1 -0
- package/dist/codebase-index/index.js +225 -153
- package/dist/codebase-index/project-server-endpoint.d.ts +1 -2
- package/dist/codebase-index/project-server.js +19 -20
- package/dist/diff.d.ts +5 -0
- package/dist/diff.js +78 -12
- package/dist/document.js +18 -6
- package/dist/edit.js +69 -16
- package/dist/exec.js +44 -22
- package/dist/fetch.js +13 -1
- package/dist/format.d.ts +4 -2
- package/dist/format.js +81 -31
- package/dist/glob.js +12 -4
- package/dist/grep.d.ts +2 -0
- package/dist/grep.js +15 -4
- package/dist/index.js +1357 -765
- package/dist/install.js +96 -37
- package/dist/kanban-tool-types.d.ts +6 -1
- package/dist/kanban.js +60 -0
- package/dist/languages/index.js +28 -13
- package/dist/lint.js +28 -13
- package/dist/logs.d.ts +0 -1
- package/dist/logs.js +44 -13
- package/dist/memory.d.ts +8 -0
- package/dist/memory.js +23 -3
- package/dist/mode.d.ts +1 -1
- package/dist/mode.js +3 -0
- package/dist/next-steps.d.ts +2 -3
- package/dist/next-steps.js +3 -3
- package/dist/outdated.d.ts +0 -3
- package/dist/outdated.js +89 -48
- package/dist/pack.js +1280 -728
- package/dist/plan.js +76 -3
- package/dist/process-registry.d.ts +8 -2
- package/dist/process-registry.js +28 -13
- package/dist/ps-slash.js +22 -12
- package/dist/read.js +10 -4
- package/dist/replace.d.ts +4 -0
- package/dist/replace.js +104 -7
- package/dist/search.d.ts +6 -0
- package/dist/search.js +47 -26
- package/dist/session-kanban.d.ts +25 -0
- package/dist/session-kanban.js +16 -0
- package/dist/skill.d.ts +6 -0
- package/dist/skill.js +9 -10
- package/dist/task.js +66 -2
- package/dist/test.js +28 -13
- package/dist/todo.js +64 -2
- package/dist/tool-icons.js +4 -2
- package/dist/tool-summary.d.ts +1 -1
- package/dist/tool-summary.js +76 -1
- package/dist/tool-tier.js +1280 -728
- package/dist/tree.js +9 -10
- package/dist/typecheck.d.ts +0 -2
- package/dist/typecheck.js +98 -31
- package/dist/write.js +58 -10
- package/package.json +4 -3
package/dist/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
|
|
|
@@ -11170,7 +11302,6 @@ import * as fs12 from "node:fs";
|
|
|
11170
11302
|
import * as os5 from "node:os";
|
|
11171
11303
|
import * as path17 from "node:path";
|
|
11172
11304
|
import { fileURLToPath } from "node:url";
|
|
11173
|
-
import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
|
|
11174
11305
|
|
|
11175
11306
|
// src/codebase-index/writer.ts
|
|
11176
11307
|
import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
|
|
@@ -14105,12 +14236,12 @@ function projectIndexServerBuildId(entrypoint) {
|
|
|
14105
14236
|
const cleanHref = href.split(/[?#]/, 1)[0] ?? href;
|
|
14106
14237
|
const file = cleanHref.startsWith("file:") ? fileURLToPath(cleanHref) : path17.resolve(cleanHref);
|
|
14107
14238
|
try {
|
|
14108
|
-
const
|
|
14109
|
-
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) {
|
|
14110
14241
|
return buildIdCache.buildId;
|
|
14111
14242
|
}
|
|
14112
14243
|
const buildId = createHash4("sha256").update(fs12.readFileSync(file)).digest("hex").slice(0, 24);
|
|
14113
|
-
buildIdCache = { file, mtimeMs:
|
|
14244
|
+
buildIdCache = { file, mtimeMs: stat20.mtimeMs, size: stat20.size, buildId };
|
|
14114
14245
|
return buildId;
|
|
14115
14246
|
} catch {
|
|
14116
14247
|
return `unreadable:${path17.basename(file)}`;
|
|
@@ -14252,8 +14383,8 @@ function isProjectIndexServerHealth(value) {
|
|
|
14252
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";
|
|
14253
14384
|
}
|
|
14254
14385
|
function delay(ms) {
|
|
14255
|
-
return new Promise((
|
|
14256
|
-
const timer = setTimeout(
|
|
14386
|
+
return new Promise((resolve18) => {
|
|
14387
|
+
const timer = setTimeout(resolve18, ms);
|
|
14257
14388
|
timer.unref?.();
|
|
14258
14389
|
});
|
|
14259
14390
|
}
|
|
@@ -14457,7 +14588,7 @@ var ProjectServerConnection = class {
|
|
|
14457
14588
|
return Promise.reject(new Error("codebase-index server connection is not available"));
|
|
14458
14589
|
}
|
|
14459
14590
|
const id = this.nextId++;
|
|
14460
|
-
return new Promise((
|
|
14591
|
+
return new Promise((resolve18, reject) => {
|
|
14461
14592
|
const timer = setTimeout(() => {
|
|
14462
14593
|
const entry = this.pending.get(id);
|
|
14463
14594
|
if (!entry) return;
|
|
@@ -14480,7 +14611,7 @@ var ProjectServerConnection = class {
|
|
|
14480
14611
|
entry.reject(cancellationError(signal));
|
|
14481
14612
|
} : void 0;
|
|
14482
14613
|
this.pending.set(id, {
|
|
14483
|
-
resolve:
|
|
14614
|
+
resolve: resolve18,
|
|
14484
14615
|
reject,
|
|
14485
14616
|
timer,
|
|
14486
14617
|
signal,
|
|
@@ -14549,7 +14680,7 @@ var ProjectServerConnection = class {
|
|
|
14549
14680
|
this.binaryBuffer = [];
|
|
14550
14681
|
this.useBinary = false;
|
|
14551
14682
|
this.textDecoder = null;
|
|
14552
|
-
return new Promise((
|
|
14683
|
+
return new Promise((resolve18, reject) => {
|
|
14553
14684
|
const socket = net3.createConnection(this.endpoint);
|
|
14554
14685
|
this.socket = socket;
|
|
14555
14686
|
const timer = setTimeout(() => {
|
|
@@ -14561,7 +14692,7 @@ var ProjectServerConnection = class {
|
|
|
14561
14692
|
clearTimeout(timer);
|
|
14562
14693
|
this.connectResolve = null;
|
|
14563
14694
|
this.connectReject = null;
|
|
14564
|
-
|
|
14695
|
+
resolve18();
|
|
14565
14696
|
};
|
|
14566
14697
|
const finishReject = (error) => {
|
|
14567
14698
|
clearTimeout(timer);
|
|
@@ -14584,7 +14715,7 @@ var ProjectServerConnection = class {
|
|
|
14584
14715
|
this.onBinaryData(socket, chunk);
|
|
14585
14716
|
return;
|
|
14586
14717
|
}
|
|
14587
|
-
if (!this.textDecoder) this.textDecoder = new
|
|
14718
|
+
if (!this.textDecoder) this.textDecoder = new StringDecoder2("utf8");
|
|
14588
14719
|
this.buffer += this.textDecoder.write(chunk);
|
|
14589
14720
|
while (true) {
|
|
14590
14721
|
const newline = this.buffer.indexOf("\n");
|
|
@@ -15609,9 +15740,9 @@ var ParserWorkerPool = class {
|
|
|
15609
15740
|
for (let i = 0; i < files.length; i++) {
|
|
15610
15741
|
chunks[i % workerCount].push(files[i]);
|
|
15611
15742
|
}
|
|
15612
|
-
return new Promise((
|
|
15743
|
+
return new Promise((resolve18, reject) => {
|
|
15613
15744
|
this.pending.set(batchId, {
|
|
15614
|
-
resolve:
|
|
15745
|
+
resolve: resolve18,
|
|
15615
15746
|
reject,
|
|
15616
15747
|
accumulated: [],
|
|
15617
15748
|
expectedWorkers: workerCount,
|
|
@@ -15642,10 +15773,10 @@ var ParserWorkerPool = class {
|
|
|
15642
15773
|
await Promise.allSettled(
|
|
15643
15774
|
workers.map(
|
|
15644
15775
|
(w) => Promise.race([
|
|
15645
|
-
new Promise((
|
|
15646
|
-
w.once("exit", () =>
|
|
15776
|
+
new Promise((resolve18) => {
|
|
15777
|
+
w.once("exit", () => resolve18());
|
|
15647
15778
|
}),
|
|
15648
|
-
new Promise((
|
|
15779
|
+
new Promise((resolve18) => setTimeout(() => resolve18(), 2e3))
|
|
15649
15780
|
]).then(() => {
|
|
15650
15781
|
if (!w.threadId) return;
|
|
15651
15782
|
return w.terminate().catch(() => {
|
|
@@ -15709,7 +15840,7 @@ function shouldUseParserWorkerPool(candidateFileCount, parseBatchCount) {
|
|
|
15709
15840
|
return !isFrugalPerf() && candidateFileCount >= WORKER_POOL_THRESHOLD && parseBatchCount > 1;
|
|
15710
15841
|
}
|
|
15711
15842
|
function yieldEventLoop() {
|
|
15712
|
-
return new Promise((
|
|
15843
|
+
return new Promise((resolve18) => setImmediate(resolve18));
|
|
15713
15844
|
}
|
|
15714
15845
|
function throwIfAborted(signal) {
|
|
15715
15846
|
if (!signal?.aborted) return;
|
|
@@ -15737,7 +15868,7 @@ function normalizeComparablePath(value) {
|
|
|
15737
15868
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
15738
15869
|
}
|
|
15739
15870
|
function gitOutput(projectRoot, args) {
|
|
15740
|
-
return new Promise((
|
|
15871
|
+
return new Promise((resolve18, reject) => {
|
|
15741
15872
|
execFile(
|
|
15742
15873
|
"git",
|
|
15743
15874
|
["-C", projectRoot, ...args],
|
|
@@ -15748,7 +15879,7 @@ function gitOutput(projectRoot, args) {
|
|
|
15748
15879
|
},
|
|
15749
15880
|
(error, stdout) => {
|
|
15750
15881
|
if (error) reject(error);
|
|
15751
|
-
else
|
|
15882
|
+
else resolve18(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
|
|
15752
15883
|
}
|
|
15753
15884
|
);
|
|
15754
15885
|
});
|
|
@@ -15980,9 +16111,9 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15980
16111
|
const statReadParse = await Promise.allSettled(
|
|
15981
16112
|
batchFiles.map(
|
|
15982
16113
|
async (file) => {
|
|
15983
|
-
let
|
|
16114
|
+
let stat20;
|
|
15984
16115
|
try {
|
|
15985
|
-
|
|
16116
|
+
stat20 = await fs18.stat(file, statOpts);
|
|
15986
16117
|
} catch (e) {
|
|
15987
16118
|
if (isAbortError(e)) throw e;
|
|
15988
16119
|
return {
|
|
@@ -15994,21 +16125,21 @@ async function runIndexerWithStore(store, opts) {
|
|
|
15994
16125
|
missing: isMissingPathError(e)
|
|
15995
16126
|
};
|
|
15996
16127
|
}
|
|
15997
|
-
if (!
|
|
16128
|
+
if (!stat20.isFile()) return { file, stat: stat20, lang: "", parsed: null };
|
|
15998
16129
|
const lang = detectLang(file);
|
|
15999
|
-
if (!lang) return { file, stat:
|
|
16000
|
-
if (
|
|
16130
|
+
if (!lang) return { file, stat: stat20, lang: "", parsed: null };
|
|
16131
|
+
if (stat20.size > MAX_INDEX_FILE_BYTES) {
|
|
16001
16132
|
return {
|
|
16002
16133
|
file,
|
|
16003
|
-
stat:
|
|
16134
|
+
stat: stat20,
|
|
16004
16135
|
lang,
|
|
16005
16136
|
parsed: null,
|
|
16006
|
-
error: `file too large (${
|
|
16137
|
+
error: `file too large (${stat20.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
16007
16138
|
};
|
|
16008
16139
|
}
|
|
16009
16140
|
const meta = existingMeta.get(file);
|
|
16010
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
16011
|
-
return { file, stat:
|
|
16141
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat20.mtimeMs)) {
|
|
16142
|
+
return { file, stat: stat20, lang, parsed: null, skippedMeta: meta };
|
|
16012
16143
|
}
|
|
16013
16144
|
let content;
|
|
16014
16145
|
try {
|
|
@@ -16017,7 +16148,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16017
16148
|
if (isAbortError(e)) throw e;
|
|
16018
16149
|
return {
|
|
16019
16150
|
file,
|
|
16020
|
-
stat:
|
|
16151
|
+
stat: stat20,
|
|
16021
16152
|
lang,
|
|
16022
16153
|
parsed: null,
|
|
16023
16154
|
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
@@ -16027,15 +16158,15 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16027
16158
|
if (!force && meta && meta.contentHash && contentHash === meta.contentHash) {
|
|
16028
16159
|
return {
|
|
16029
16160
|
file,
|
|
16030
|
-
stat:
|
|
16161
|
+
stat: stat20,
|
|
16031
16162
|
lang,
|
|
16032
16163
|
parsed: null,
|
|
16033
16164
|
content,
|
|
16034
16165
|
contentHash,
|
|
16035
|
-
skippedMeta: { ...meta, mtimeMs: Math.floor(
|
|
16166
|
+
skippedMeta: { ...meta, mtimeMs: Math.floor(stat20.mtimeMs) }
|
|
16036
16167
|
};
|
|
16037
16168
|
}
|
|
16038
|
-
return { file, stat:
|
|
16169
|
+
return { file, stat: stat20, lang, parsed: null, content, contentHash };
|
|
16039
16170
|
}
|
|
16040
16171
|
)
|
|
16041
16172
|
);
|
|
@@ -16114,7 +16245,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16114
16245
|
filesFailed++;
|
|
16115
16246
|
continue;
|
|
16116
16247
|
}
|
|
16117
|
-
const { stat:
|
|
16248
|
+
const { stat: stat20, lang, parsed } = result;
|
|
16118
16249
|
if (result.skippedMeta) {
|
|
16119
16250
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
16120
16251
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -16138,7 +16269,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16138
16269
|
store.upsertFile({
|
|
16139
16270
|
file,
|
|
16140
16271
|
lang,
|
|
16141
|
-
mtimeMs: Math.floor(
|
|
16272
|
+
mtimeMs: Math.floor(stat20.mtimeMs),
|
|
16142
16273
|
symbolCount: 0,
|
|
16143
16274
|
lastIndexed: Date.now(),
|
|
16144
16275
|
contentHash: result.contentHash ?? ""
|
|
@@ -16152,7 +16283,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16152
16283
|
store.replaceEmptyFile({
|
|
16153
16284
|
file,
|
|
16154
16285
|
lang,
|
|
16155
|
-
mtimeMs: Math.floor(
|
|
16286
|
+
mtimeMs: Math.floor(stat20.mtimeMs),
|
|
16156
16287
|
symbolCount: 0,
|
|
16157
16288
|
lastIndexed: Date.now(),
|
|
16158
16289
|
contentHash: result.contentHash ?? ""
|
|
@@ -16166,7 +16297,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
16166
16297
|
lang,
|
|
16167
16298
|
symbols: parsed.symbols,
|
|
16168
16299
|
refs: parsed.refs ?? [],
|
|
16169
|
-
mtimeMs: Math.floor(
|
|
16300
|
+
mtimeMs: Math.floor(stat20.mtimeMs),
|
|
16170
16301
|
symbolCount: parsed.symbols.length,
|
|
16171
16302
|
contentHash: result.contentHash ?? ""
|
|
16172
16303
|
});
|
|
@@ -16517,7 +16648,7 @@ function callIndexOp(op, args, opts) {
|
|
|
16517
16648
|
opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
|
|
16518
16649
|
);
|
|
16519
16650
|
}
|
|
16520
|
-
return new Promise((
|
|
16651
|
+
return new Promise((resolve18, reject) => {
|
|
16521
16652
|
const id = nextRpcId++;
|
|
16522
16653
|
const timer = setTimeout(() => {
|
|
16523
16654
|
pending.delete(id);
|
|
@@ -16539,7 +16670,7 @@ function callIndexOp(op, args, opts) {
|
|
|
16539
16670
|
pending.set(id, {
|
|
16540
16671
|
resolve: (v) => {
|
|
16541
16672
|
cleanup();
|
|
16542
|
-
|
|
16673
|
+
resolve18(v);
|
|
16543
16674
|
},
|
|
16544
16675
|
reject: (e) => {
|
|
16545
16676
|
cleanup();
|
|
@@ -16807,6 +16938,160 @@ function ensureCodebaseIndexServer(options) {
|
|
|
16807
16938
|
}
|
|
16808
16939
|
|
|
16809
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;
|
|
16810
17095
|
var codebaseIndexTool = {
|
|
16811
17096
|
name: "codebase-index",
|
|
16812
17097
|
category: "Project",
|
|
@@ -16832,12 +17117,23 @@ var codebaseIndexTool = {
|
|
|
16832
17117
|
},
|
|
16833
17118
|
langs: {
|
|
16834
17119
|
type: "array",
|
|
16835
|
-
items: { type: "string" },
|
|
16836
|
-
description:
|
|
17120
|
+
items: { type: "string", enum: [...INDEXABLE_LANG_IDS] },
|
|
17121
|
+
description: `Limit reindex to specific languages: ${INDEXABLE_LANG_IDS.join(", ")}`
|
|
16837
17122
|
}
|
|
16838
17123
|
}
|
|
16839
17124
|
},
|
|
16840
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
|
+
}
|
|
16841
17137
|
if (isIndexing()) {
|
|
16842
17138
|
return {
|
|
16843
17139
|
filesIndexed: 0,
|
|
@@ -16859,23 +17155,32 @@ var codebaseIndexTool = {
|
|
|
16859
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.`
|
|
16860
17156
|
};
|
|
16861
17157
|
}
|
|
16862
|
-
|
|
17158
|
+
const result = await runStartupIndex({
|
|
16863
17159
|
projectRoot: ctx.projectRoot,
|
|
16864
17160
|
force: input.force ?? false,
|
|
16865
17161
|
langs: input.langs,
|
|
16866
17162
|
indexDir: codebaseIndexDirOverride(ctx),
|
|
16867
17163
|
signal: execOpts?.signal
|
|
16868
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;
|
|
16869
17173
|
}
|
|
16870
17174
|
};
|
|
16871
17175
|
|
|
16872
17176
|
// src/codebase-index/codebase-incoming-calls-tool.ts
|
|
17177
|
+
import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
|
|
16873
17178
|
var codebaseIncomingCallsTool = {
|
|
16874
17179
|
name: "codebase-incoming-calls",
|
|
16875
17180
|
category: "Project",
|
|
16876
17181
|
icon: "index",
|
|
16877
|
-
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.
|
|
16878
|
-
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.',
|
|
16879
17184
|
permission: "auto",
|
|
16880
17185
|
mutating: false,
|
|
16881
17186
|
capabilities: ["fs.read"],
|
|
@@ -16927,16 +17232,27 @@ var codebaseIncomingCallsTool = {
|
|
|
16927
17232
|
}
|
|
16928
17233
|
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
|
|
16929
17234
|
const transitive = input.transitive === true;
|
|
16930
|
-
|
|
16931
|
-
|
|
16932
|
-
|
|
16933
|
-
|
|
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 {
|
|
16934
17249
|
symbol: input.symbol,
|
|
16935
|
-
|
|
16936
|
-
|
|
16937
|
-
|
|
16938
|
-
}
|
|
16939
|
-
|
|
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;
|
|
16940
17256
|
if (!symbolFound) {
|
|
16941
17257
|
let hasPersistedIndex = state.ready;
|
|
16942
17258
|
if (!hasPersistedIndex) {
|
|
@@ -16981,12 +17297,13 @@ var codebaseIncomingCallsTool = {
|
|
|
16981
17297
|
};
|
|
16982
17298
|
|
|
16983
17299
|
// src/codebase-index/codebase-outgoing-calls-tool.ts
|
|
17300
|
+
import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
|
|
16984
17301
|
var codebaseOutgoingCallsTool = {
|
|
16985
17302
|
name: "codebase-outgoing-calls",
|
|
16986
17303
|
category: "Project",
|
|
16987
17304
|
icon: "index",
|
|
16988
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.",
|
|
16989
|
-
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.',
|
|
16990
17307
|
permission: "auto",
|
|
16991
17308
|
mutating: false,
|
|
16992
17309
|
capabilities: ["fs.read"],
|
|
@@ -17038,16 +17355,27 @@ var codebaseOutgoingCallsTool = {
|
|
|
17038
17355
|
}
|
|
17039
17356
|
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 50), 200));
|
|
17040
17357
|
const transitive = input.transitive === true;
|
|
17041
|
-
|
|
17042
|
-
|
|
17043
|
-
|
|
17044
|
-
|
|
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 {
|
|
17045
17372
|
symbol: input.symbol,
|
|
17046
|
-
|
|
17047
|
-
|
|
17048
|
-
|
|
17049
|
-
}
|
|
17050
|
-
|
|
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;
|
|
17051
17379
|
if (!symbolFound) {
|
|
17052
17380
|
let hasPersistedIndex = state.ready;
|
|
17053
17381
|
if (!hasPersistedIndex) {
|
|
@@ -17091,132 +17419,6 @@ var codebaseOutgoingCallsTool = {
|
|
|
17091
17419
|
}
|
|
17092
17420
|
};
|
|
17093
17421
|
|
|
17094
|
-
// src/codebase-index/codebase-search-tool.ts
|
|
17095
|
-
var codebaseSearchTool = {
|
|
17096
|
-
name: "codebase-search",
|
|
17097
|
-
category: "Project",
|
|
17098
|
-
icon: "index",
|
|
17099
|
-
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).",
|
|
17100
|
-
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.",
|
|
17101
|
-
permission: "auto",
|
|
17102
|
-
mutating: false,
|
|
17103
|
-
capabilities: ["fs.read"],
|
|
17104
|
-
// The index host has its own 30s read watchdog. Leave enough headroom for
|
|
17105
|
-
// worker teardown and structured timeout reporting.
|
|
17106
|
-
timeoutMs: 35e3,
|
|
17107
|
-
inputSchema: {
|
|
17108
|
-
type: "object",
|
|
17109
|
-
properties: {
|
|
17110
|
-
query: {
|
|
17111
|
-
type: "string",
|
|
17112
|
-
description: "Search query \u2014 searches symbol names, signatures, and doc comments"
|
|
17113
|
-
},
|
|
17114
|
-
kind: {
|
|
17115
|
-
type: "string",
|
|
17116
|
-
enum: [
|
|
17117
|
-
"class",
|
|
17118
|
-
"interface",
|
|
17119
|
-
"enum",
|
|
17120
|
-
"type",
|
|
17121
|
-
"function",
|
|
17122
|
-
"method",
|
|
17123
|
-
"var",
|
|
17124
|
-
"const",
|
|
17125
|
-
"let",
|
|
17126
|
-
"property",
|
|
17127
|
-
"parameter",
|
|
17128
|
-
"namespace",
|
|
17129
|
-
"object",
|
|
17130
|
-
"literal",
|
|
17131
|
-
"schema",
|
|
17132
|
-
"struct",
|
|
17133
|
-
"trait",
|
|
17134
|
-
"impl",
|
|
17135
|
-
"static",
|
|
17136
|
-
"mod"
|
|
17137
|
-
],
|
|
17138
|
-
description: "Filter by indexed symbol kind"
|
|
17139
|
-
},
|
|
17140
|
-
lang: {
|
|
17141
|
-
type: "string",
|
|
17142
|
-
enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
|
|
17143
|
-
description: "Filter by indexed language"
|
|
17144
|
-
},
|
|
17145
|
-
lspKind: {
|
|
17146
|
-
type: "integer",
|
|
17147
|
-
description: "Filter by LSP SymbolKind number (e.g. 5=Class, 12=Function, 11=Interface, 10=Enum)"
|
|
17148
|
-
},
|
|
17149
|
-
file: {
|
|
17150
|
-
type: "string",
|
|
17151
|
-
description: "Filter to files matching this path substring"
|
|
17152
|
-
},
|
|
17153
|
-
limit: {
|
|
17154
|
-
type: "integer",
|
|
17155
|
-
description: "Maximum results to return (default 20, max 100)",
|
|
17156
|
-
minimum: 1,
|
|
17157
|
-
maximum: 100
|
|
17158
|
-
},
|
|
17159
|
-
preferLsp: {
|
|
17160
|
-
type: "boolean",
|
|
17161
|
-
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."
|
|
17162
|
-
}
|
|
17163
|
-
},
|
|
17164
|
-
required: ["query"]
|
|
17165
|
-
},
|
|
17166
|
-
async execute(input, ctx, execOpts) {
|
|
17167
|
-
const state = getIndexState();
|
|
17168
|
-
if (state.indexing && !state.ready) {
|
|
17169
|
-
return {
|
|
17170
|
-
results: [],
|
|
17171
|
-
total: 0,
|
|
17172
|
-
query: input.query,
|
|
17173
|
-
indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
|
|
17174
|
-
};
|
|
17175
|
-
}
|
|
17176
|
-
if (state.lastError) {
|
|
17177
|
-
const circuit = state.circuit;
|
|
17178
|
-
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.";
|
|
17179
|
-
return {
|
|
17180
|
-
results: [],
|
|
17181
|
-
total: 0,
|
|
17182
|
-
query: input.query,
|
|
17183
|
-
indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
|
|
17184
|
-
};
|
|
17185
|
-
}
|
|
17186
|
-
const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
|
|
17187
|
-
const { results, total } = await searchCodebaseIndex(
|
|
17188
|
-
{
|
|
17189
|
-
projectRoot: ctx.projectRoot,
|
|
17190
|
-
indexDir: codebaseIndexDirOverride(ctx),
|
|
17191
|
-
query: input.query,
|
|
17192
|
-
kind: input.kind?.toLowerCase(),
|
|
17193
|
-
lang: input.lang?.toLowerCase(),
|
|
17194
|
-
file: input.file,
|
|
17195
|
-
lspKind: input.lspKind,
|
|
17196
|
-
limit
|
|
17197
|
-
},
|
|
17198
|
-
{ signal: execOpts?.signal }
|
|
17199
|
-
);
|
|
17200
|
-
let hasPersistedIndex = state.ready || total > 0;
|
|
17201
|
-
if (!hasPersistedIndex) {
|
|
17202
|
-
try {
|
|
17203
|
-
const stats = await codebaseIndexStats(
|
|
17204
|
-
{ projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
|
|
17205
|
-
{ signal: execOpts?.signal }
|
|
17206
|
-
);
|
|
17207
|
-
hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
|
|
17208
|
-
} catch {
|
|
17209
|
-
}
|
|
17210
|
-
}
|
|
17211
|
-
return {
|
|
17212
|
-
results,
|
|
17213
|
-
total,
|
|
17214
|
-
query: input.query,
|
|
17215
|
-
...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
|
|
17216
|
-
};
|
|
17217
|
-
}
|
|
17218
|
-
};
|
|
17219
|
-
|
|
17220
17422
|
// src/codebase-index/codebase-stats-tool.ts
|
|
17221
17423
|
var codebaseStatsTool = {
|
|
17222
17424
|
name: "codebase-stats",
|
|
@@ -17310,9 +17512,9 @@ import * as path25 from "node:path";
|
|
|
17310
17512
|
var deadCodeScanTool = {
|
|
17311
17513
|
name: "dead-code-scan",
|
|
17312
17514
|
category: "Project",
|
|
17313
|
-
icon: "
|
|
17515
|
+
icon: "index",
|
|
17314
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).",
|
|
17315
|
-
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).",
|
|
17316
17518
|
permission: "auto",
|
|
17317
17519
|
mutating: false,
|
|
17318
17520
|
capabilities: ["fs.read"],
|
|
@@ -18075,12 +18277,14 @@ import { statSync as statSync3 } from "node:fs";
|
|
|
18075
18277
|
import * as fs22 from "node:fs/promises";
|
|
18076
18278
|
import * as path27 from "node:path";
|
|
18077
18279
|
import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
|
|
18280
|
+
import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
|
|
18078
18281
|
var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
|
|
18282
|
+
var MAX_GIT_DIFF_CHARS = 1e5;
|
|
18079
18283
|
var diffTool = {
|
|
18080
18284
|
name: "diff",
|
|
18081
18285
|
category: "Filesystem",
|
|
18082
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.",
|
|
18083
|
-
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).',
|
|
18084
18288
|
permission: "auto",
|
|
18085
18289
|
mutating: false,
|
|
18086
18290
|
maxOutputBytes: 262144,
|
|
@@ -18113,11 +18317,12 @@ var diffTool = {
|
|
|
18113
18317
|
mode: {
|
|
18114
18318
|
type: "string",
|
|
18115
18319
|
enum: ["unified", "side-by-side", "stat"],
|
|
18116
|
-
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.'
|
|
18117
18321
|
},
|
|
18118
18322
|
context: {
|
|
18119
18323
|
type: "integer",
|
|
18120
|
-
|
|
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."
|
|
18121
18326
|
}
|
|
18122
18327
|
}
|
|
18123
18328
|
},
|
|
@@ -18130,16 +18335,31 @@ var diffTool = {
|
|
|
18130
18335
|
};
|
|
18131
18336
|
async function gitDiff(input, ctx, signal) {
|
|
18132
18337
|
if (input.a?.startsWith("-")) {
|
|
18133
|
-
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
|
+
});
|
|
18134
18342
|
}
|
|
18135
18343
|
if (input.b?.startsWith("-")) {
|
|
18136
|
-
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
|
+
});
|
|
18137
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;
|
|
18138
18353
|
const gitDir = findGitDir(ctx.cwd);
|
|
18139
18354
|
if (!gitDir) {
|
|
18140
|
-
return { diff: "", files: [], truncated: false, mode:
|
|
18355
|
+
return { diff: "", files: [], truncated: false, mode: effectiveMode };
|
|
18141
18356
|
}
|
|
18142
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
|
+
}
|
|
18143
18363
|
if (input.staged) args.push("--staged");
|
|
18144
18364
|
if (input.a) args.push(input.a);
|
|
18145
18365
|
if (input.b) args.push(input.b);
|
|
@@ -18148,19 +18368,30 @@ async function gitDiff(input, ctx, signal) {
|
|
|
18148
18368
|
args.push("--", ...files.map((f) => f.trim()));
|
|
18149
18369
|
}
|
|
18150
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
|
+
}
|
|
18151
18381
|
return {
|
|
18152
|
-
diff
|
|
18382
|
+
diff,
|
|
18153
18383
|
files: [],
|
|
18154
|
-
truncated
|
|
18155
|
-
mode:
|
|
18384
|
+
truncated,
|
|
18385
|
+
mode: effectiveMode,
|
|
18386
|
+
note: sideBySideNote
|
|
18156
18387
|
};
|
|
18157
18388
|
}
|
|
18158
18389
|
function findGitDir(cwd) {
|
|
18159
18390
|
let dir = cwd;
|
|
18160
18391
|
for (let i = 0; i < 20; i++) {
|
|
18161
18392
|
try {
|
|
18162
|
-
const
|
|
18163
|
-
if (
|
|
18393
|
+
const stat20 = statSync3(path27.join(dir, ".git"));
|
|
18394
|
+
if (stat20.isDirectory()) return dir;
|
|
18164
18395
|
} catch {
|
|
18165
18396
|
}
|
|
18166
18397
|
const parent = path27.dirname(dir);
|
|
@@ -18170,7 +18401,7 @@ function findGitDir(cwd) {
|
|
|
18170
18401
|
return null;
|
|
18171
18402
|
}
|
|
18172
18403
|
function runGit(args, cwd, signal) {
|
|
18173
|
-
return new Promise((
|
|
18404
|
+
return new Promise((resolve18) => {
|
|
18174
18405
|
let stdout = "";
|
|
18175
18406
|
let stderr = "";
|
|
18176
18407
|
const child = spawn7("git", args, {
|
|
@@ -18186,8 +18417,8 @@ function runGit(args, cwd, signal) {
|
|
|
18186
18417
|
child.stderr?.on("data", (c) => {
|
|
18187
18418
|
stderr += c.toString();
|
|
18188
18419
|
});
|
|
18189
|
-
child.on("close", (code) =>
|
|
18190
|
-
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 }));
|
|
18191
18422
|
});
|
|
18192
18423
|
}
|
|
18193
18424
|
async function fileDiff(input, ctx, _signal) {
|
|
@@ -18198,19 +18429,19 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
18198
18429
|
diff: "No files specified",
|
|
18199
18430
|
files: [],
|
|
18200
18431
|
truncated: false,
|
|
18201
|
-
mode:
|
|
18432
|
+
mode: "dump"
|
|
18202
18433
|
};
|
|
18203
18434
|
}
|
|
18204
18435
|
const results = [];
|
|
18205
18436
|
let truncated = false;
|
|
18206
18437
|
for (const file of files) {
|
|
18207
|
-
const absPath =
|
|
18208
|
-
const
|
|
18209
|
-
if (!
|
|
18210
|
-
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) {
|
|
18211
18442
|
truncated = true;
|
|
18212
18443
|
results.push(
|
|
18213
|
-
`--- ${file} (skipped: ${
|
|
18444
|
+
`--- ${file} (skipped: ${stat20.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
|
|
18214
18445
|
);
|
|
18215
18446
|
continue;
|
|
18216
18447
|
}
|
|
@@ -18222,7 +18453,10 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
18222
18453
|
diff: results.join("\n\n"),
|
|
18223
18454
|
files,
|
|
18224
18455
|
truncated,
|
|
18225
|
-
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
|
|
18226
18460
|
};
|
|
18227
18461
|
}
|
|
18228
18462
|
function formatWithLineNumbers(file, lines) {
|
|
@@ -18235,11 +18469,12 @@ ${numbered}`;
|
|
|
18235
18469
|
// src/document.ts
|
|
18236
18470
|
init_util();
|
|
18237
18471
|
import * as fs23 from "node:fs/promises";
|
|
18472
|
+
import * as path28 from "node:path";
|
|
18238
18473
|
var documentTool = {
|
|
18239
18474
|
name: "document",
|
|
18240
18475
|
category: "Project",
|
|
18241
|
-
description: "DEPRECATED \u2014
|
|
18242
|
-
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).",
|
|
18243
18478
|
permission: "auto",
|
|
18244
18479
|
mutating: false,
|
|
18245
18480
|
timeoutMs: 3e4,
|
|
@@ -18275,7 +18510,11 @@ var documentTool = {
|
|
|
18275
18510
|
const results = [];
|
|
18276
18511
|
let filesProcessed = 0;
|
|
18277
18512
|
let itemsDocumented = 0;
|
|
18278
|
-
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)] : [];
|
|
18279
18518
|
for (const absPath of fileList) {
|
|
18280
18519
|
try {
|
|
18281
18520
|
const content = await fs23.readFile(absPath, "utf8");
|
|
@@ -18308,14 +18547,21 @@ var documentTool = {
|
|
|
18308
18547
|
};
|
|
18309
18548
|
}
|
|
18310
18549
|
};
|
|
18311
|
-
async function resolveFiles(filesInput, cwd) {
|
|
18312
|
-
const files =
|
|
18550
|
+
async function resolveFiles(filesInput, cwd, ctx) {
|
|
18551
|
+
const files = filesInput.split(",");
|
|
18313
18552
|
const resolved = [];
|
|
18314
18553
|
for (const f of files) {
|
|
18315
|
-
const
|
|
18554
|
+
const entry = f.trim();
|
|
18555
|
+
if (!entry) continue;
|
|
18556
|
+
let absPath;
|
|
18316
18557
|
try {
|
|
18317
|
-
|
|
18318
|
-
|
|
18558
|
+
absPath = ensureInsideRoot(path28.resolve(cwd, entry), ctx);
|
|
18559
|
+
} catch {
|
|
18560
|
+
continue;
|
|
18561
|
+
}
|
|
18562
|
+
try {
|
|
18563
|
+
const stat20 = await fs23.stat(absPath);
|
|
18564
|
+
if (stat20.isFile()) resolved.push(absPath);
|
|
18319
18565
|
} catch {
|
|
18320
18566
|
}
|
|
18321
18567
|
}
|
|
@@ -18385,7 +18631,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
|
|
|
18385
18631
|
// src/e2e.ts
|
|
18386
18632
|
init_util();
|
|
18387
18633
|
import { open, readdir as readdir7 } from "node:fs/promises";
|
|
18388
|
-
import * as
|
|
18634
|
+
import * as path29 from "node:path";
|
|
18389
18635
|
async function readBoundedText(filePath, maxBytes) {
|
|
18390
18636
|
let handle;
|
|
18391
18637
|
try {
|
|
@@ -18435,8 +18681,8 @@ var MAX_PACKAGE_BYTES = 512 * 1024;
|
|
|
18435
18681
|
var MAX_CONFIG_BYTES = 512 * 1024;
|
|
18436
18682
|
var MAX_SPEC_SAMPLES = 20;
|
|
18437
18683
|
function relativePath(root, target) {
|
|
18438
|
-
const value =
|
|
18439
|
-
return value.split(
|
|
18684
|
+
const value = path29.relative(root, target) || ".";
|
|
18685
|
+
return value.split(path29.sep).join("/");
|
|
18440
18686
|
}
|
|
18441
18687
|
function escapeRegExp(value) {
|
|
18442
18688
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -18508,7 +18754,7 @@ async function scanWorkspace(root, maxDepth, signal) {
|
|
|
18508
18754
|
}
|
|
18509
18755
|
for (const entry of entries) {
|
|
18510
18756
|
signal.throwIfAborted();
|
|
18511
|
-
const absolutePath =
|
|
18757
|
+
const absolutePath = path29.join(current.directory, entry.name);
|
|
18512
18758
|
if (entry.isFile()) {
|
|
18513
18759
|
if (entry.name === "package.json") result.packageFiles.push(absolutePath);
|
|
18514
18760
|
const framework = CONFIG_NAMES.get(entry.name);
|
|
@@ -18569,9 +18815,9 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
|
|
|
18569
18815
|
if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
|
|
18570
18816
|
if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
|
|
18571
18817
|
if (directory === scanRoot) break;
|
|
18572
|
-
const parent =
|
|
18573
|
-
const relativeParent =
|
|
18574
|
-
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)) {
|
|
18575
18821
|
break;
|
|
18576
18822
|
}
|
|
18577
18823
|
directory = parent;
|
|
@@ -18616,7 +18862,7 @@ function isSpec(framework, filename) {
|
|
|
18616
18862
|
return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
|
|
18617
18863
|
}
|
|
18618
18864
|
async function collectSpecs(root, framework, testDirectory, signal) {
|
|
18619
|
-
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")];
|
|
18620
18866
|
const samples = [];
|
|
18621
18867
|
let count = 0;
|
|
18622
18868
|
let scanned = 0;
|
|
@@ -18635,7 +18881,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
|
|
|
18635
18881
|
}
|
|
18636
18882
|
for (const entry of entries) {
|
|
18637
18883
|
signal.throwIfAborted();
|
|
18638
|
-
const target =
|
|
18884
|
+
const target = path29.join(directory, entry.name);
|
|
18639
18885
|
if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
|
|
18640
18886
|
else if (entry.isFile() && isSpec(framework, entry.name)) {
|
|
18641
18887
|
count += 1;
|
|
@@ -18655,7 +18901,7 @@ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
|
|
|
18655
18901
|
const found = packagesByDirectory.get(directory);
|
|
18656
18902
|
if (found) return found;
|
|
18657
18903
|
if (directory === scanRoot) return void 0;
|
|
18658
|
-
const parent =
|
|
18904
|
+
const parent = path29.dirname(directory);
|
|
18659
18905
|
if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
|
|
18660
18906
|
directory = parent;
|
|
18661
18907
|
}
|
|
@@ -18666,13 +18912,13 @@ async function discoverE2EProjects(root, options) {
|
|
|
18666
18912
|
const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
|
|
18667
18913
|
(info) => Boolean(info)
|
|
18668
18914
|
);
|
|
18669
|
-
const packagesByDirectory = new Map(packages.map((info) => [
|
|
18915
|
+
const packagesByDirectory = new Map(packages.map((info) => [path29.dirname(info.path), info]));
|
|
18670
18916
|
const candidates = /* @__PURE__ */ new Map();
|
|
18671
18917
|
for (const config of scan.configs) {
|
|
18672
18918
|
if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
|
|
18673
18919
|
continue;
|
|
18674
18920
|
}
|
|
18675
|
-
const projectRoot =
|
|
18921
|
+
const projectRoot = path29.dirname(config.absolutePath);
|
|
18676
18922
|
candidates.set(`${config.framework}:${projectRoot}`, {
|
|
18677
18923
|
framework: config.framework,
|
|
18678
18924
|
root: projectRoot,
|
|
@@ -18680,7 +18926,7 @@ async function discoverE2EProjects(root, options) {
|
|
|
18680
18926
|
});
|
|
18681
18927
|
}
|
|
18682
18928
|
for (const info of packages) {
|
|
18683
|
-
const projectRoot =
|
|
18929
|
+
const projectRoot = path29.dirname(info.path);
|
|
18684
18930
|
for (const framework of frameworkFromPackage(info)) {
|
|
18685
18931
|
if (options.framework && options.framework !== "all" && framework !== options.framework)
|
|
18686
18932
|
continue;
|
|
@@ -18696,10 +18942,10 @@ async function discoverE2EProjects(root, options) {
|
|
|
18696
18942
|
const scripts = matchingScripts(info, candidate.framework);
|
|
18697
18943
|
const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
|
|
18698
18944
|
const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
|
|
18699
|
-
const resolvedTestDirectory = testDirectory ?
|
|
18700
|
-
const relativeTestDirectory = resolvedTestDirectory ?
|
|
18945
|
+
const resolvedTestDirectory = testDirectory ? path29.resolve(candidate.root, testDirectory) : void 0;
|
|
18946
|
+
const relativeTestDirectory = resolvedTestDirectory ? path29.relative(root, resolvedTestDirectory) : void 0;
|
|
18701
18947
|
const unsafeTestDirectory = Boolean(
|
|
18702
|
-
relativeTestDirectory && (relativeTestDirectory.startsWith("..") ||
|
|
18948
|
+
relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path29.isAbsolute(relativeTestDirectory))
|
|
18703
18949
|
);
|
|
18704
18950
|
const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
|
|
18705
18951
|
const warnings = [];
|
|
@@ -18807,7 +19053,7 @@ import {
|
|
|
18807
19053
|
toStyle,
|
|
18808
19054
|
unifiedDiff
|
|
18809
19055
|
} from "@wrongstack/core/utils";
|
|
18810
|
-
import { ToolValidationError } from "@wrongstack/core/types";
|
|
19056
|
+
import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
|
|
18811
19057
|
|
|
18812
19058
|
// src/_edit-match.ts
|
|
18813
19059
|
var TIER_LABEL = {
|
|
@@ -19035,7 +19281,7 @@ function prefixSimilarity(a, b) {
|
|
|
19035
19281
|
}
|
|
19036
19282
|
|
|
19037
19283
|
// src/_syntax-check.ts
|
|
19038
|
-
import * as
|
|
19284
|
+
import * as path30 from "node:path";
|
|
19039
19285
|
var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
19040
19286
|
var MAX_CHECK_CHARS = 15e5;
|
|
19041
19287
|
var MAX_ERRORS = 5;
|
|
@@ -19059,15 +19305,15 @@ async function checkSyntax(filePath, content, previousContent) {
|
|
|
19059
19305
|
return { errors, preExisting };
|
|
19060
19306
|
}
|
|
19061
19307
|
function isJsoncFile(filePath) {
|
|
19062
|
-
const base =
|
|
19308
|
+
const base = path30.basename(filePath).toLowerCase();
|
|
19063
19309
|
if (base.endsWith(".jsonc")) return true;
|
|
19064
19310
|
if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
|
|
19065
|
-
const dir =
|
|
19311
|
+
const dir = path30.basename(path30.dirname(filePath)).toLowerCase();
|
|
19066
19312
|
return dir === ".vscode";
|
|
19067
19313
|
}
|
|
19068
19314
|
async function parseErrors(filePath, content) {
|
|
19069
19315
|
if (content.length > MAX_CHECK_CHARS) return void 0;
|
|
19070
|
-
const ext =
|
|
19316
|
+
const ext = path30.extname(filePath).toLowerCase();
|
|
19071
19317
|
if (ext === ".json" || ext === ".jsonc") {
|
|
19072
19318
|
try {
|
|
19073
19319
|
JSON.parse(content);
|
|
@@ -19094,7 +19340,7 @@ async function parseErrors(filePath, content) {
|
|
|
19094
19340
|
ts2.ScriptKind.JSX
|
|
19095
19341
|
);
|
|
19096
19342
|
const sourceFile = ts2.createSourceFile(
|
|
19097
|
-
|
|
19343
|
+
path30.basename(filePath),
|
|
19098
19344
|
content,
|
|
19099
19345
|
ts2.ScriptTarget.Latest,
|
|
19100
19346
|
/* setParentNodes */
|
|
@@ -19121,6 +19367,7 @@ function formatDiag(ts2, diag, content, sourceFile) {
|
|
|
19121
19367
|
|
|
19122
19368
|
// src/edit.ts
|
|
19123
19369
|
init_util();
|
|
19370
|
+
var MAX_DIFF_BYTES = 262144;
|
|
19124
19371
|
var editTool = {
|
|
19125
19372
|
name: "edit",
|
|
19126
19373
|
category: "Filesystem",
|
|
@@ -19131,46 +19378,62 @@ var editTool = {
|
|
|
19131
19378
|
useInstead: ["write", "patch"]
|
|
19132
19379
|
},
|
|
19133
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",
|
|
19134
19384
|
mutating: true,
|
|
19135
19385
|
capabilities: ["fs.write"],
|
|
19136
19386
|
icon: "edit",
|
|
19137
19387
|
timeoutMs: 5e3,
|
|
19388
|
+
maxOutputBytes: 262144,
|
|
19138
19389
|
inputSchema: {
|
|
19139
19390
|
type: "object",
|
|
19140
19391
|
properties: {
|
|
19141
|
-
path: {
|
|
19142
|
-
|
|
19143
|
-
|
|
19144
|
-
|
|
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
|
+
}
|
|
19145
19408
|
},
|
|
19146
19409
|
required: ["path", "old_string", "new_string"]
|
|
19147
19410
|
},
|
|
19148
19411
|
async execute(input, ctx, opts) {
|
|
19149
19412
|
if (!input?.path) {
|
|
19150
|
-
throw new
|
|
19413
|
+
throw new ToolValidationError3({ message: "edit: path is required", field: "path" });
|
|
19151
19414
|
}
|
|
19152
19415
|
if (input.old_string === void 0) {
|
|
19153
|
-
throw new
|
|
19416
|
+
throw new ToolValidationError3({
|
|
19154
19417
|
message: "edit: old_string is required",
|
|
19155
19418
|
field: "old_string"
|
|
19156
19419
|
});
|
|
19157
19420
|
}
|
|
19158
19421
|
if (input.new_string === void 0) {
|
|
19159
|
-
throw new
|
|
19422
|
+
throw new ToolValidationError3({
|
|
19160
19423
|
message: "edit: new_string is required",
|
|
19161
19424
|
field: "new_string"
|
|
19162
19425
|
});
|
|
19163
19426
|
}
|
|
19164
19427
|
if (input.old_string === "") {
|
|
19165
|
-
throw new
|
|
19428
|
+
throw new ToolValidationError3({
|
|
19166
19429
|
message: "edit: old_string cannot be empty",
|
|
19167
19430
|
field: "old_string"
|
|
19168
19431
|
});
|
|
19169
19432
|
}
|
|
19170
19433
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
19171
|
-
const
|
|
19434
|
+
const stat20 = await fs24.stat(absPath).catch((err) => {
|
|
19172
19435
|
if (err.code === "ENOENT") {
|
|
19173
|
-
throw new
|
|
19436
|
+
throw new ToolValidationError3({
|
|
19174
19437
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
19175
19438
|
field: "path",
|
|
19176
19439
|
context: { exists: false }
|
|
@@ -19178,8 +19441,8 @@ var editTool = {
|
|
|
19178
19441
|
}
|
|
19179
19442
|
throw err;
|
|
19180
19443
|
});
|
|
19181
|
-
if (!
|
|
19182
|
-
throw new
|
|
19444
|
+
if (!stat20.isFile()) {
|
|
19445
|
+
throw new ToolValidationError3({
|
|
19183
19446
|
message: `edit: "${input.path}" is not a regular file`,
|
|
19184
19447
|
field: "path"
|
|
19185
19448
|
});
|
|
@@ -19192,7 +19455,7 @@ var editTool = {
|
|
|
19192
19455
|
const lastReadHash = ctx.lastReadHash?.(absPath);
|
|
19193
19456
|
if (lastReadHash !== void 0) {
|
|
19194
19457
|
if (lastReadHash !== originalHash) {
|
|
19195
|
-
throw new
|
|
19458
|
+
throw new ToolValidationError3({
|
|
19196
19459
|
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
19197
19460
|
field: "path",
|
|
19198
19461
|
context: { reason: "external_modification" }
|
|
@@ -19201,15 +19464,15 @@ var editTool = {
|
|
|
19201
19464
|
} else {
|
|
19202
19465
|
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
19203
19466
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
19204
|
-
throw new
|
|
19467
|
+
throw new ToolValidationError3({
|
|
19205
19468
|
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
19206
19469
|
field: "path",
|
|
19207
19470
|
context: { reason: "external_modification" }
|
|
19208
19471
|
});
|
|
19209
19472
|
}
|
|
19210
19473
|
}
|
|
19211
|
-
if (autoRead && updated.mtimeMs >
|
|
19212
|
-
throw new
|
|
19474
|
+
if (autoRead && updated.mtimeMs > stat20.mtimeMs + mtimeTolerance) {
|
|
19475
|
+
throw new ToolValidationError3({
|
|
19213
19476
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
19214
19477
|
field: "path",
|
|
19215
19478
|
context: { reason: "auto_read_race" }
|
|
@@ -19221,6 +19484,9 @@ var editTool = {
|
|
|
19221
19484
|
const oldLf = normalizeToLf(input.old_string);
|
|
19222
19485
|
const newLf = normalizeToLf(input.new_string);
|
|
19223
19486
|
if (oldLf === newLf) {
|
|
19487
|
+
if (!fileLf.includes(oldLf)) {
|
|
19488
|
+
throw noMatchError(input.path, fileLf, oldLf);
|
|
19489
|
+
}
|
|
19224
19490
|
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
|
|
19225
19491
|
return {
|
|
19226
19492
|
path: absPath,
|
|
@@ -19234,26 +19500,20 @@ var editTool = {
|
|
|
19234
19500
|
const ladder = findLadderMatches(fileLf, oldLf);
|
|
19235
19501
|
if (!ladder) {
|
|
19236
19502
|
opts?.signal?.throwIfAborted();
|
|
19237
|
-
|
|
19238
|
-
throw new ToolValidationError({
|
|
19239
|
-
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
|
|
19240
|
-
${hint.snippet}
|
|
19241
|
-
Compare this against your old_string and retry with the file's actual text.` : ""}`,
|
|
19242
|
-
field: "old_string"
|
|
19243
|
-
});
|
|
19503
|
+
throw noMatchError(input.path, fileLf, oldLf);
|
|
19244
19504
|
}
|
|
19245
19505
|
const { tier, matches } = ladder;
|
|
19246
19506
|
const count = matches.length;
|
|
19247
19507
|
if (ladder.ambiguous) {
|
|
19248
19508
|
const lines = matches.map((m) => m.startLine);
|
|
19249
|
-
throw new
|
|
19509
|
+
throw new ToolValidationError3({
|
|
19250
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.`,
|
|
19251
19511
|
field: "old_string",
|
|
19252
19512
|
context: { occurrences: count, matchTier: tier }
|
|
19253
19513
|
});
|
|
19254
19514
|
}
|
|
19255
19515
|
if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
|
|
19256
|
-
throw new
|
|
19516
|
+
throw new ToolValidationError3({
|
|
19257
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.`,
|
|
19258
19518
|
field: "old_string",
|
|
19259
19519
|
context: { matchTier: tier }
|
|
@@ -19261,7 +19521,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
19261
19521
|
}
|
|
19262
19522
|
if (count > 1 && !input.replace_all) {
|
|
19263
19523
|
const lines = matches.map((m) => m.startLine);
|
|
19264
|
-
throw new
|
|
19524
|
+
throw new ToolValidationError3({
|
|
19265
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.`,
|
|
19266
19526
|
field: "old_string",
|
|
19267
19527
|
context: { occurrences: count, matchTier: tier }
|
|
@@ -19302,16 +19562,20 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
19302
19562
|
after: newFile
|
|
19303
19563
|
});
|
|
19304
19564
|
opts?.signal?.throwIfAborted();
|
|
19305
|
-
const diff =
|
|
19306
|
-
|
|
19307
|
-
|
|
19308
|
-
|
|
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;
|
|
19309
19573
|
const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
|
|
19310
19574
|
let syntaxNote;
|
|
19311
19575
|
if (syntax && syntax.errors.length > 0) {
|
|
19312
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.`;
|
|
19313
19577
|
}
|
|
19314
|
-
const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
|
|
19578
|
+
const notes = [autoReadNote, tierNote, diffNote, syntaxNote].filter(Boolean);
|
|
19315
19579
|
return {
|
|
19316
19580
|
path: absPath,
|
|
19317
19581
|
replacements: input.replace_all ? count : 1,
|
|
@@ -19322,6 +19586,15 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
19322
19586
|
};
|
|
19323
19587
|
}
|
|
19324
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
|
+
}
|
|
19325
19598
|
|
|
19326
19599
|
// src/exec.ts
|
|
19327
19600
|
import { spawn as spawn8 } from "node:child_process";
|
|
@@ -19330,14 +19603,14 @@ import {
|
|
|
19330
19603
|
emitProcessOutput as emitProcessOutput3,
|
|
19331
19604
|
emitProcessStarted as emitProcessStarted3
|
|
19332
19605
|
} from "@wrongstack/core/observability";
|
|
19333
|
-
import { toErrorMessage as
|
|
19606
|
+
import { toErrorMessage as toErrorMessage6 } from "@wrongstack/core/utils/error";
|
|
19334
19607
|
init_output_spool();
|
|
19335
19608
|
init_util();
|
|
19336
19609
|
init_win32_resolve();
|
|
19337
19610
|
|
|
19338
19611
|
// src/exec-kill-guard.ts
|
|
19339
19612
|
import * as os8 from "node:os";
|
|
19340
|
-
import * as
|
|
19613
|
+
import * as path31 from "node:path";
|
|
19341
19614
|
var isWin3 = os8.platform() === "win32";
|
|
19342
19615
|
async function checkExecKillCommand(cmd, args) {
|
|
19343
19616
|
if (!cmd) return { blocked: false };
|
|
@@ -19550,7 +19823,7 @@ async function checkKillTarget(target) {
|
|
|
19550
19823
|
reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
|
|
19551
19824
|
};
|
|
19552
19825
|
}
|
|
19553
|
-
const currentImage =
|
|
19826
|
+
const currentImage = path31.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
|
|
19554
19827
|
const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
|
|
19555
19828
|
if (targetsNodeRuntime && currentImage === "node") {
|
|
19556
19829
|
return {
|
|
@@ -20201,6 +20474,7 @@ function getExecAllowlist() {
|
|
|
20201
20474
|
var MAX_ARGS = 20;
|
|
20202
20475
|
var MAX_OUTPUT2 = 2e5;
|
|
20203
20476
|
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
20477
|
+
var MAX_TIMEOUT_MS = 6e5;
|
|
20204
20478
|
var BLOCKED_ARG_PATTERNS = {
|
|
20205
20479
|
python: [],
|
|
20206
20480
|
// git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
|
|
@@ -20326,8 +20600,8 @@ var SAFE_DANGER = { level: "safe", reasons: [] };
|
|
|
20326
20600
|
var execTool = {
|
|
20327
20601
|
name: "exec",
|
|
20328
20602
|
category: "Shell",
|
|
20329
|
-
description: "Execute a
|
|
20330
|
-
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.",
|
|
20331
20605
|
selection: {
|
|
20332
20606
|
doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
|
|
20333
20607
|
useInstead: ["bash"]
|
|
@@ -20343,7 +20617,13 @@ var execTool = {
|
|
|
20343
20617
|
subjectKey: "command",
|
|
20344
20618
|
mutating: true,
|
|
20345
20619
|
riskTier: "standard",
|
|
20346
|
-
|
|
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,
|
|
20347
20627
|
capabilities: ["shell.restricted"],
|
|
20348
20628
|
icon: "terminal",
|
|
20349
20629
|
inputSchema: {
|
|
@@ -20364,7 +20644,7 @@ var execTool = {
|
|
|
20364
20644
|
},
|
|
20365
20645
|
timeout: {
|
|
20366
20646
|
type: "integer",
|
|
20367
|
-
description: "Per-command timeout in milliseconds."
|
|
20647
|
+
description: "Per-command timeout in milliseconds (default 30000, max 600000)."
|
|
20368
20648
|
}
|
|
20369
20649
|
},
|
|
20370
20650
|
required: ["command"]
|
|
@@ -20408,7 +20688,7 @@ var execTool = {
|
|
|
20408
20688
|
};
|
|
20409
20689
|
}
|
|
20410
20690
|
const args = (input.args ?? []).slice(0, MAX_ARGS);
|
|
20411
|
-
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));
|
|
20412
20692
|
const danger = detectDanger(cmd, args, dangerBypass);
|
|
20413
20693
|
const killCheck = await checkExecKillCommand(cmd, args);
|
|
20414
20694
|
if (killCheck.blocked) {
|
|
@@ -20436,15 +20716,16 @@ var execTool = {
|
|
|
20436
20716
|
danger
|
|
20437
20717
|
};
|
|
20438
20718
|
}
|
|
20719
|
+
const defaultCwd = ctx.workingDir ?? ctx.cwd;
|
|
20439
20720
|
let cwd;
|
|
20440
20721
|
try {
|
|
20441
|
-
cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(
|
|
20722
|
+
cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(defaultCwd, ctx);
|
|
20442
20723
|
} catch {
|
|
20443
20724
|
return {
|
|
20444
20725
|
command: cmd,
|
|
20445
20726
|
args,
|
|
20446
20727
|
stdout: "",
|
|
20447
|
-
stderr: `cwd "${input.cwd ??
|
|
20728
|
+
stderr: `cwd "${input.cwd ?? defaultCwd}" resolves outside project root`,
|
|
20448
20729
|
exitCode: 1,
|
|
20449
20730
|
truncated: false,
|
|
20450
20731
|
allowed: false,
|
|
@@ -20456,7 +20737,7 @@ var execTool = {
|
|
|
20456
20737
|
}
|
|
20457
20738
|
};
|
|
20458
20739
|
function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
20459
|
-
return new Promise((
|
|
20740
|
+
return new Promise((resolve18) => {
|
|
20460
20741
|
let stdout = "";
|
|
20461
20742
|
let stderr = "";
|
|
20462
20743
|
let killed = false;
|
|
@@ -20464,7 +20745,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
20464
20745
|
const finish = (result) => {
|
|
20465
20746
|
if (resolvedOnce.value) return;
|
|
20466
20747
|
resolvedOnce.value = true;
|
|
20467
|
-
|
|
20748
|
+
resolve18(result);
|
|
20468
20749
|
};
|
|
20469
20750
|
const startedAt = Date.now();
|
|
20470
20751
|
let stdoutBytes = 0;
|
|
@@ -20516,7 +20797,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
20516
20797
|
command: cmd,
|
|
20517
20798
|
args,
|
|
20518
20799
|
stdout: "",
|
|
20519
|
-
stderr: `spawn failed: ${
|
|
20800
|
+
stderr: `spawn failed: ${toErrorMessage6(err)}`,
|
|
20520
20801
|
exitCode: 1,
|
|
20521
20802
|
truncated: false,
|
|
20522
20803
|
allowed: true,
|
|
@@ -20619,7 +20900,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
20619
20900
|
}
|
|
20620
20901
|
|
|
20621
20902
|
// src/fetch.ts
|
|
20622
|
-
import { FetchError as FetchError2, ToolError, ToolValidationError as
|
|
20903
|
+
import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
|
|
20623
20904
|
import TurndownService from "turndown";
|
|
20624
20905
|
|
|
20625
20906
|
// src/_fetch-guard.ts
|
|
@@ -20629,7 +20910,7 @@ import {
|
|
|
20629
20910
|
isPrivateIPv4 as isPrivateIPv42,
|
|
20630
20911
|
isPrivateIPv6 as isPrivateIPv62
|
|
20631
20912
|
} from "@wrongstack/core/utils";
|
|
20632
|
-
import { FetchError, ToolValidationError as
|
|
20913
|
+
import { FetchError, ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
20633
20914
|
import { Agent, fetch as undiciFetch } from "undici";
|
|
20634
20915
|
var nativeGlobalFetch = globalThis.fetch;
|
|
20635
20916
|
var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
|
|
@@ -20700,13 +20981,13 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
20700
20981
|
for (; ; ) {
|
|
20701
20982
|
const parsed = new URL(currentUrl);
|
|
20702
20983
|
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
20703
|
-
throw new
|
|
20984
|
+
throw new ToolValidationError4({
|
|
20704
20985
|
message: `fetch: redirect to unsupported protocol "${parsed.protocol}"`,
|
|
20705
20986
|
field: "url"
|
|
20706
20987
|
});
|
|
20707
20988
|
}
|
|
20708
20989
|
if (parsed.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
20709
|
-
throw new
|
|
20990
|
+
throw new ToolValidationError4({
|
|
20710
20991
|
message: "fetch: redirect to http:// blocked (HTTPS required by default)",
|
|
20711
20992
|
field: "url"
|
|
20712
20993
|
});
|
|
@@ -20722,6 +21003,10 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
20722
21003
|
if (res.status < 300 || res.status > 399) {
|
|
20723
21004
|
return res;
|
|
20724
21005
|
}
|
|
21006
|
+
try {
|
|
21007
|
+
await res.body?.cancel();
|
|
21008
|
+
} catch {
|
|
21009
|
+
}
|
|
20725
21010
|
redirectCount++;
|
|
20726
21011
|
if (redirectCount > maxRedirects) {
|
|
20727
21012
|
throw new FetchError({
|
|
@@ -20745,7 +21030,7 @@ async function assertNotPrivate(hostname4) {
|
|
|
20745
21030
|
if (ALLOW_PRIVATE) return;
|
|
20746
21031
|
const host = hostname4.startsWith("[") && hostname4.endsWith("]") ? hostname4.slice(1, -1) : hostname4;
|
|
20747
21032
|
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
20748
|
-
throw new
|
|
21033
|
+
throw new ToolValidationError4({
|
|
20749
21034
|
message: "fetch: blocked localhost target",
|
|
20750
21035
|
field: "url"
|
|
20751
21036
|
});
|
|
@@ -20753,14 +21038,14 @@ async function assertNotPrivate(hostname4) {
|
|
|
20753
21038
|
const ipVersion = net4.isIP(host);
|
|
20754
21039
|
if (ipVersion === 4) {
|
|
20755
21040
|
if (isPrivateIPv42(host)) {
|
|
20756
|
-
throw new
|
|
21041
|
+
throw new ToolValidationError4({
|
|
20757
21042
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
20758
21043
|
field: "url"
|
|
20759
21044
|
});
|
|
20760
21045
|
}
|
|
20761
21046
|
} else if (ipVersion === 6) {
|
|
20762
21047
|
if (isPrivateIPv62(host)) {
|
|
20763
|
-
throw new
|
|
21048
|
+
throw new ToolValidationError4({
|
|
20764
21049
|
message: `fetch: blocked private/loopback address "${host}"`,
|
|
20765
21050
|
field: "url"
|
|
20766
21051
|
});
|
|
@@ -20771,14 +21056,14 @@ async function assertNotPrivate(hostname4) {
|
|
|
20771
21056
|
for (const r of records) {
|
|
20772
21057
|
const bad = r.family === 4 ? isPrivateIPv42(r.address) : isPrivateIPv62(r.address);
|
|
20773
21058
|
if (bad) {
|
|
20774
|
-
throw new
|
|
21059
|
+
throw new ToolValidationError4({
|
|
20775
21060
|
message: `fetch: resolved to private address ${r.address}`,
|
|
20776
21061
|
field: "url"
|
|
20777
21062
|
});
|
|
20778
21063
|
}
|
|
20779
21064
|
}
|
|
20780
21065
|
} catch (err) {
|
|
20781
|
-
if (err instanceof
|
|
21066
|
+
if (err instanceof ToolValidationError4) throw err;
|
|
20782
21067
|
}
|
|
20783
21068
|
}
|
|
20784
21069
|
}
|
|
@@ -20795,6 +21080,8 @@ TD.addRule("stripDangerousElements", {
|
|
|
20795
21080
|
filter: ["script", "style", "noscript"],
|
|
20796
21081
|
replacement: () => ""
|
|
20797
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()));
|
|
20798
21085
|
var MAX_BYTES = 131072;
|
|
20799
21086
|
var TIMEOUT_MS = 2e4;
|
|
20800
21087
|
var combineSignals = (signals) => AbortSignal.any(signals);
|
|
@@ -20824,7 +21111,7 @@ var fetchTool = {
|
|
|
20824
21111
|
format: {
|
|
20825
21112
|
type: "string",
|
|
20826
21113
|
enum: ["markdown", "text", "raw"],
|
|
20827
|
-
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).'
|
|
20828
21115
|
}
|
|
20829
21116
|
},
|
|
20830
21117
|
required: ["url"]
|
|
@@ -20853,20 +21140,26 @@ var fetchTool = {
|
|
|
20853
21140
|
},
|
|
20854
21141
|
async *executeStream(input, ctx, opts) {
|
|
20855
21142
|
if (!input?.url) {
|
|
20856
|
-
throw new
|
|
21143
|
+
throw new ToolValidationError5({
|
|
20857
21144
|
message: "fetch: url is required",
|
|
20858
21145
|
field: "url"
|
|
20859
21146
|
});
|
|
20860
21147
|
}
|
|
20861
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
|
+
}
|
|
20862
21155
|
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
20863
|
-
throw new
|
|
21156
|
+
throw new ToolValidationError5({
|
|
20864
21157
|
message: `fetch: unsupported protocol "${u.protocol}"`,
|
|
20865
21158
|
field: "url"
|
|
20866
21159
|
});
|
|
20867
21160
|
}
|
|
20868
21161
|
if (u.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
20869
|
-
throw new
|
|
21162
|
+
throw new ToolValidationError5({
|
|
20870
21163
|
message: "fetch: http:// blocked (HTTPS required by default)",
|
|
20871
21164
|
field: "url"
|
|
20872
21165
|
});
|
|
@@ -21056,8 +21349,9 @@ var formatTool = {
|
|
|
21056
21349
|
type: "final",
|
|
21057
21350
|
output: {
|
|
21058
21351
|
fixer: bridge.language,
|
|
21059
|
-
|
|
21060
|
-
|
|
21352
|
+
// Language-bridge runs don't report per-file counts.
|
|
21353
|
+
files_checked: void 0,
|
|
21354
|
+
files_changed: void 0,
|
|
21061
21355
|
output: normalizeCommandOutput(run.output || run.error || ""),
|
|
21062
21356
|
truncated: run.truncated
|
|
21063
21357
|
}
|
|
@@ -21084,11 +21378,14 @@ var formatTool = {
|
|
|
21084
21378
|
text: `Running ${detected}\u2026`,
|
|
21085
21379
|
data: { fixer: detected, check: !!input.check }
|
|
21086
21380
|
};
|
|
21087
|
-
const
|
|
21088
|
-
|
|
21089
|
-
if (
|
|
21090
|
-
|
|
21091
|
-
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);
|
|
21092
21389
|
}
|
|
21093
21390
|
const result = yield* spawnStream({
|
|
21094
21391
|
cmd: detected,
|
|
@@ -21097,39 +21394,70 @@ var formatTool = {
|
|
|
21097
21394
|
signal: opts.signal,
|
|
21098
21395
|
maxBytes: 1e5
|
|
21099
21396
|
});
|
|
21100
|
-
const
|
|
21397
|
+
const combinedOut = `${result.stdout}
|
|
21398
|
+
${result.stderr}`;
|
|
21399
|
+
const counts = parseFormatterCounts(detected, combinedOut);
|
|
21101
21400
|
yield {
|
|
21102
21401
|
type: "final",
|
|
21103
21402
|
output: {
|
|
21104
21403
|
fixer: detected,
|
|
21105
|
-
files_checked:
|
|
21106
|
-
files_changed: changed,
|
|
21404
|
+
files_checked: counts.checked,
|
|
21405
|
+
files_changed: counts.changed,
|
|
21107
21406
|
output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
|
|
21108
21407
|
truncated: result.truncated
|
|
21109
21408
|
}
|
|
21110
21409
|
};
|
|
21111
21410
|
}
|
|
21112
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
|
+
}
|
|
21113
21421
|
async function detectFixer(cwd) {
|
|
21114
|
-
const
|
|
21115
|
-
|
|
21116
|
-
await stat19(`${cwd}/biome.json`);
|
|
21117
|
-
return "biome";
|
|
21118
|
-
} catch {
|
|
21422
|
+
const fs36 = await import("node:fs/promises");
|
|
21423
|
+
const exists = async (file) => {
|
|
21119
21424
|
try {
|
|
21120
|
-
await
|
|
21121
|
-
return
|
|
21425
|
+
await fs36.stat(`${cwd}/${file}`);
|
|
21426
|
+
return true;
|
|
21122
21427
|
} catch {
|
|
21123
|
-
return
|
|
21428
|
+
return false;
|
|
21124
21429
|
}
|
|
21430
|
+
};
|
|
21431
|
+
if (await exists("biome.json") || await exists("biome.jsonc")) return "biome";
|
|
21432
|
+
const PRETTIER_CONFIGS = [
|
|
21433
|
+
".prettierrc",
|
|
21434
|
+
".prettierrc.json",
|
|
21435
|
+
".prettierrc.yml",
|
|
21436
|
+
".prettierrc.yaml",
|
|
21437
|
+
".prettierrc.js",
|
|
21438
|
+
".prettierrc.cjs",
|
|
21439
|
+
".prettierrc.mjs",
|
|
21440
|
+
"prettier.config.js",
|
|
21441
|
+
"prettier.config.cjs",
|
|
21442
|
+
"prettier.config.mjs"
|
|
21443
|
+
];
|
|
21444
|
+
for (const cfg of PRETTIER_CONFIGS) {
|
|
21445
|
+
if (await exists(cfg)) return "prettier";
|
|
21446
|
+
}
|
|
21447
|
+
try {
|
|
21448
|
+
const raw = await fs36.readFile(`${cwd}/package.json`, "utf8");
|
|
21449
|
+
const pkg = JSON.parse(raw);
|
|
21450
|
+
if (pkg["prettier"] !== void 0) return "prettier";
|
|
21451
|
+
} catch {
|
|
21125
21452
|
}
|
|
21453
|
+
return "biome";
|
|
21126
21454
|
}
|
|
21127
21455
|
|
|
21128
21456
|
// src/git.ts
|
|
21129
21457
|
init_util();
|
|
21130
21458
|
import { spawn as spawn9 } from "node:child_process";
|
|
21131
21459
|
import { statSync as statSync4 } from "node:fs";
|
|
21132
|
-
import { dirname as
|
|
21460
|
+
import { dirname as dirname13, resolve as resolve14, sep as sep6 } from "node:path";
|
|
21133
21461
|
import { assessCommitSafety } from "@wrongstack/core/coordination";
|
|
21134
21462
|
import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
|
|
21135
21463
|
var TIMEOUT_MS2 = 3e4;
|
|
@@ -21290,8 +21618,8 @@ function validateWorktreeInput(input, projectRoot) {
|
|
|
21290
21618
|
return reject(`unsafe worktree path: ${input.worktreePath}`);
|
|
21291
21619
|
}
|
|
21292
21620
|
if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
|
|
21293
|
-
const root =
|
|
21294
|
-
const abs =
|
|
21621
|
+
const root = resolve14(projectRoot);
|
|
21622
|
+
const abs = resolve14(root, input.worktreePath);
|
|
21295
21623
|
if (abs !== root && !abs.startsWith(root + sep6)) {
|
|
21296
21624
|
return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
|
|
21297
21625
|
}
|
|
@@ -21303,12 +21631,12 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
21303
21631
|
let dir = cwd;
|
|
21304
21632
|
for (let i = 0; i < 20; i++) {
|
|
21305
21633
|
try {
|
|
21306
|
-
const
|
|
21307
|
-
if (
|
|
21634
|
+
const stat20 = statSync4(`${dir}/.git`);
|
|
21635
|
+
if (stat20.isDirectory() || stat20.isFile()) return dir;
|
|
21308
21636
|
} catch {
|
|
21309
21637
|
}
|
|
21310
21638
|
if (dir === root) break;
|
|
21311
|
-
const parent =
|
|
21639
|
+
const parent = dirname13(dir);
|
|
21312
21640
|
if (parent === dir) break;
|
|
21313
21641
|
dir = parent;
|
|
21314
21642
|
}
|
|
@@ -21385,7 +21713,7 @@ function buildArgs(input) {
|
|
|
21385
21713
|
}
|
|
21386
21714
|
}
|
|
21387
21715
|
function runGit2(args, cwd, signal) {
|
|
21388
|
-
return new Promise((
|
|
21716
|
+
return new Promise((resolve18) => {
|
|
21389
21717
|
let stdout = "";
|
|
21390
21718
|
let stderr = "";
|
|
21391
21719
|
const child = spawn9("git", args, {
|
|
@@ -21406,7 +21734,7 @@ function runGit2(args, cwd, signal) {
|
|
|
21406
21734
|
}
|
|
21407
21735
|
});
|
|
21408
21736
|
child.on("error", (err) => {
|
|
21409
|
-
|
|
21737
|
+
resolve18({
|
|
21410
21738
|
command: args[0],
|
|
21411
21739
|
stdout: normalizeCommandOutput(stdout),
|
|
21412
21740
|
stderr: err.message,
|
|
@@ -21415,7 +21743,7 @@ function runGit2(args, cwd, signal) {
|
|
|
21415
21743
|
});
|
|
21416
21744
|
});
|
|
21417
21745
|
child.on("close", (code) => {
|
|
21418
|
-
|
|
21746
|
+
resolve18({
|
|
21419
21747
|
command: args[0],
|
|
21420
21748
|
stdout: normalizeCommandOutput(stdout),
|
|
21421
21749
|
stderr: normalizeCommandOutput(stderr),
|
|
@@ -21428,8 +21756,9 @@ function runGit2(args, cwd, signal) {
|
|
|
21428
21756
|
|
|
21429
21757
|
// src/glob.ts
|
|
21430
21758
|
import * as fs25 from "node:fs/promises";
|
|
21431
|
-
import * as
|
|
21759
|
+
import * as path32 from "node:path";
|
|
21432
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";
|
|
21433
21762
|
|
|
21434
21763
|
// src/_concurrency.ts
|
|
21435
21764
|
async function mapWithConcurrency2(items, limit, fn) {
|
|
@@ -21455,7 +21784,7 @@ var WALK_CONCURRENCY = 16;
|
|
|
21455
21784
|
var globTool = {
|
|
21456
21785
|
name: "glob",
|
|
21457
21786
|
category: "Filesystem",
|
|
21458
|
-
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.",
|
|
21459
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.",
|
|
21460
21789
|
selection: {
|
|
21461
21790
|
doNotUseWhen: "you need to search inside file contents.",
|
|
@@ -21466,7 +21795,7 @@ var globTool = {
|
|
|
21466
21795
|
capabilities: ["fs.read"],
|
|
21467
21796
|
icon: "folder",
|
|
21468
21797
|
maxOutputBytes: 65536,
|
|
21469
|
-
timeoutMs:
|
|
21798
|
+
timeoutMs: 15e3,
|
|
21470
21799
|
inputSchema: {
|
|
21471
21800
|
type: "object",
|
|
21472
21801
|
properties: {
|
|
@@ -21480,13 +21809,20 @@ var globTool = {
|
|
|
21480
21809
|
},
|
|
21481
21810
|
limit: {
|
|
21482
21811
|
type: "integer",
|
|
21483
|
-
|
|
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."
|
|
21484
21815
|
}
|
|
21485
21816
|
},
|
|
21486
21817
|
required: ["pattern"]
|
|
21487
21818
|
},
|
|
21488
21819
|
async execute(input, ctx, opts) {
|
|
21489
|
-
if (!input?.pattern)
|
|
21820
|
+
if (!input?.pattern) {
|
|
21821
|
+
throw new ToolValidationError6({
|
|
21822
|
+
message: "glob: pattern is required",
|
|
21823
|
+
field: "pattern"
|
|
21824
|
+
});
|
|
21825
|
+
}
|
|
21490
21826
|
const signal = opts?.signal;
|
|
21491
21827
|
const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
|
|
21492
21828
|
const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
|
|
@@ -21531,7 +21867,7 @@ var globTool = {
|
|
|
21531
21867
|
const name = e.name;
|
|
21532
21868
|
if (DEFAULT_IGNORE2.has(name)) continue;
|
|
21533
21869
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
21534
|
-
const full =
|
|
21870
|
+
const full = path32.join(dir, name);
|
|
21535
21871
|
if (e.isDirectory()) {
|
|
21536
21872
|
if (isGitIgnored(rel, true)) continue;
|
|
21537
21873
|
subdirs.push({ full, rel });
|
|
@@ -21581,8 +21917,8 @@ var globTool = {
|
|
|
21581
21917
|
// src/grep.ts
|
|
21582
21918
|
import { spawn as spawn10 } from "node:child_process";
|
|
21583
21919
|
import * as fs26 from "node:fs/promises";
|
|
21584
|
-
import * as
|
|
21585
|
-
import { ToolValidationError as
|
|
21920
|
+
import * as path33 from "node:path";
|
|
21921
|
+
import { ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
|
|
21586
21922
|
import {
|
|
21587
21923
|
buildChildEnv as buildChildEnv5,
|
|
21588
21924
|
compileGlob as compileGlob3,
|
|
@@ -21786,7 +22122,7 @@ var grepTool = {
|
|
|
21786
22122
|
},
|
|
21787
22123
|
async *executeStream(input, ctx, opts) {
|
|
21788
22124
|
if (!input?.pattern) {
|
|
21789
|
-
throw new
|
|
22125
|
+
throw new ToolValidationError7({
|
|
21790
22126
|
message: "grep: pattern is required",
|
|
21791
22127
|
field: "pattern"
|
|
21792
22128
|
});
|
|
@@ -21796,12 +22132,12 @@ var grepTool = {
|
|
|
21796
22132
|
const limit = Math.max(1, Math.min(input.limit ?? 200, 2e3));
|
|
21797
22133
|
const validation = compileUserRegex(input.pattern, input.case_insensitive ? "i" : "");
|
|
21798
22134
|
if (!validation.ok) {
|
|
21799
|
-
throw new
|
|
22135
|
+
throw new ToolValidationError7({
|
|
21800
22136
|
message: `grep: ${validation.reason}`,
|
|
21801
22137
|
field: "pattern"
|
|
21802
22138
|
});
|
|
21803
22139
|
}
|
|
21804
|
-
const rgAvailable = await detectRg(
|
|
22140
|
+
const rgAvailable = await detectRg();
|
|
21805
22141
|
if (rgAvailable) {
|
|
21806
22142
|
try {
|
|
21807
22143
|
yield* runRgStream(input, base, mode, limit, opts.signal);
|
|
@@ -21814,16 +22150,23 @@ var grepTool = {
|
|
|
21814
22150
|
yield { type: "final", output: out };
|
|
21815
22151
|
}
|
|
21816
22152
|
};
|
|
21817
|
-
|
|
21818
|
-
|
|
22153
|
+
var rgAvailabilityCache;
|
|
22154
|
+
function detectRg() {
|
|
22155
|
+
rgAvailabilityCache ??= new Promise((resolve18) => {
|
|
21819
22156
|
try {
|
|
21820
|
-
const p = spawn10("rg", ["--version"], {
|
|
21821
|
-
|
|
21822
|
-
|
|
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));
|
|
21823
22165
|
} catch {
|
|
21824
|
-
|
|
22166
|
+
resolve18(false);
|
|
21825
22167
|
}
|
|
21826
22168
|
});
|
|
22169
|
+
return rgAvailabilityCache;
|
|
21827
22170
|
}
|
|
21828
22171
|
async function* runRgStream(input, base, mode, limit, signal) {
|
|
21829
22172
|
const args = ["--no-heading"];
|
|
@@ -21837,7 +22180,7 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
21837
22180
|
for (const ignored of DEFAULT_IGNORE3) {
|
|
21838
22181
|
args.push("--glob", `!${ignored}/**`, "--glob", `!**/${ignored}/**`);
|
|
21839
22182
|
}
|
|
21840
|
-
const gitignorePath =
|
|
22183
|
+
const gitignorePath = path33.join(base, ".gitignore");
|
|
21841
22184
|
if (await fs26.access(gitignorePath).then(() => true, () => false)) {
|
|
21842
22185
|
args.push("--ignore-file", gitignorePath);
|
|
21843
22186
|
}
|
|
@@ -22009,7 +22352,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
22009
22352
|
const flags = input.case_insensitive ? "i" : "";
|
|
22010
22353
|
const compiled = compileUserRegex(input.pattern, flags);
|
|
22011
22354
|
if (!compiled.ok) {
|
|
22012
|
-
throw new
|
|
22355
|
+
throw new ToolValidationError7({
|
|
22013
22356
|
message: `grep: ${compiled.reason}`,
|
|
22014
22357
|
field: "pattern"
|
|
22015
22358
|
});
|
|
@@ -22027,8 +22370,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
22027
22370
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
22028
22371
|
if (globRe) globRe.lastIndex = 0;
|
|
22029
22372
|
try {
|
|
22030
|
-
const
|
|
22031
|
-
if (!
|
|
22373
|
+
const stat20 = await fs26.stat(full);
|
|
22374
|
+
if (!stat20.isFile() || stat20.size > maxBytes || stopped || signal.aborted) return;
|
|
22032
22375
|
const file = await fs26.open(full, "r");
|
|
22033
22376
|
try {
|
|
22034
22377
|
let bytesReadTotal = 0;
|
|
@@ -22119,7 +22462,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
22119
22462
|
if (DEFAULT_IGNORE3.has(e.name)) continue;
|
|
22120
22463
|
if (e.isSymbolicLink()) continue;
|
|
22121
22464
|
const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
|
|
22122
|
-
const full =
|
|
22465
|
+
const full = path33.join(dir, e.name);
|
|
22123
22466
|
if (e.isDirectory()) {
|
|
22124
22467
|
if (isGitIgnored(rel, true)) continue;
|
|
22125
22468
|
subdirs.push({ full, rel });
|
|
@@ -22239,26 +22582,14 @@ var installTool = {
|
|
|
22239
22582
|
return;
|
|
22240
22583
|
}
|
|
22241
22584
|
}
|
|
22242
|
-
const pkgManager = await detectPackageManager(cwd);
|
|
22585
|
+
const pkgManager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
22243
22586
|
yield { type: "log", text: `Resolving with ${pkgManager}\u2026`, data: { phase: "resolve" } };
|
|
22244
|
-
const save = input.save === "dev" ? "-D" : input.save === "optional" ? "-O" : "";
|
|
22245
22587
|
const globalFlag = input.global ? ["-g"] : [];
|
|
22246
22588
|
const ignoreScripts = input.lifecycleScripts !== true;
|
|
22247
|
-
const args = [];
|
|
22248
|
-
if (input.dry_run) args.push("--dry-run");
|
|
22249
|
-
if (ignoreScripts) args.push("--ignore-scripts");
|
|
22250
|
-
if (pkgManager === "pnpm") {
|
|
22251
|
-
if (save) args.push(save);
|
|
22252
|
-
args.push("add", ...globalFlag);
|
|
22253
|
-
} else if (pkgManager === "yarn") {
|
|
22254
|
-
args.push("add", ...globalFlag);
|
|
22255
|
-
} else {
|
|
22256
|
-
args.push("install", ...globalFlag);
|
|
22257
|
-
}
|
|
22258
22589
|
const pkgList = input.packages ? (Array.isArray(input.packages) ? input.packages : input.packages.split(",")).map(
|
|
22259
22590
|
(p) => p.trim()
|
|
22260
22591
|
) : [];
|
|
22261
|
-
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;
|
|
22262
22593
|
for (const pkg of pkgList) {
|
|
22263
22594
|
if (!PKG_NAME_RE.test(pkg) || pkg.startsWith("-") || pkg.length > 200) {
|
|
22264
22595
|
yield {
|
|
@@ -22274,7 +22605,34 @@ var installTool = {
|
|
|
22274
22605
|
return;
|
|
22275
22606
|
}
|
|
22276
22607
|
}
|
|
22277
|
-
|
|
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);
|
|
22278
22636
|
yield {
|
|
22279
22637
|
type: "log",
|
|
22280
22638
|
text: `Fetching ${pkgList.length || "all"} packages\u2026`,
|
|
@@ -22355,9 +22713,9 @@ var JsonFileTooLargeError = class extends Error {
|
|
|
22355
22713
|
};
|
|
22356
22714
|
async function readJsonFileBounded(filePath, ctx) {
|
|
22357
22715
|
const resolved = await safeResolveReal(filePath, ctx);
|
|
22358
|
-
const
|
|
22359
|
-
if (
|
|
22360
|
-
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);
|
|
22361
22719
|
}
|
|
22362
22720
|
return fs27.readFile(resolved, "utf8");
|
|
22363
22721
|
}
|
|
@@ -22763,60 +23121,60 @@ function jmespathSearch(data, query) {
|
|
|
22763
23121
|
}
|
|
22764
23122
|
function validateJsonSchema(data, schema) {
|
|
22765
23123
|
const errors = [];
|
|
22766
|
-
function check(value, s,
|
|
23124
|
+
function check(value, s, path41) {
|
|
22767
23125
|
if (s["type"]) {
|
|
22768
23126
|
const expectedType = s["type"];
|
|
22769
23127
|
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
22770
23128
|
if (expectedType === "integer") {
|
|
22771
|
-
if (!Number.isInteger(value)) errors.push(`${
|
|
23129
|
+
if (!Number.isInteger(value)) errors.push(`${path41}: expected integer, got ${actualType}`);
|
|
22772
23130
|
} else if (expectedType !== actualType) {
|
|
22773
|
-
errors.push(`${
|
|
23131
|
+
errors.push(`${path41}: expected ${expectedType}, got ${actualType}`);
|
|
22774
23132
|
}
|
|
22775
23133
|
}
|
|
22776
23134
|
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
22777
23135
|
try {
|
|
22778
23136
|
new URL(value);
|
|
22779
23137
|
} catch {
|
|
22780
|
-
errors.push(`${
|
|
23138
|
+
errors.push(`${path41}: not a valid URI`);
|
|
22781
23139
|
}
|
|
22782
23140
|
}
|
|
22783
23141
|
if (typeof value === "string" && s["pattern"]) {
|
|
22784
23142
|
const compiled = compileUserRegex(s["pattern"], "");
|
|
22785
23143
|
if (!compiled.ok) {
|
|
22786
|
-
errors.push(`${
|
|
23144
|
+
errors.push(`${path41}: invalid schema pattern \u2014 ${compiled.reason}`);
|
|
22787
23145
|
} else if (!compiled.regex.test(capSubject(value))) {
|
|
22788
|
-
errors.push(`${
|
|
23146
|
+
errors.push(`${path41}: does not match pattern ${s["pattern"]}`);
|
|
22789
23147
|
}
|
|
22790
23148
|
}
|
|
22791
23149
|
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
22792
|
-
errors.push(`${
|
|
23150
|
+
errors.push(`${path41}: string too short (min ${s["minLength"]})`);
|
|
22793
23151
|
}
|
|
22794
23152
|
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
22795
|
-
errors.push(`${
|
|
23153
|
+
errors.push(`${path41}: string too long (max ${s["maxLength"]})`);
|
|
22796
23154
|
}
|
|
22797
23155
|
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
22798
|
-
errors.push(`${
|
|
23156
|
+
errors.push(`${path41}: below minimum ${s["minimum"]}`);
|
|
22799
23157
|
}
|
|
22800
23158
|
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
22801
|
-
errors.push(`${
|
|
23159
|
+
errors.push(`${path41}: above maximum ${s["maximum"]}`);
|
|
22802
23160
|
}
|
|
22803
23161
|
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
22804
23162
|
for (let i = 0; i < value.length; i++) {
|
|
22805
|
-
check(value[i], s["items"], `${
|
|
23163
|
+
check(value[i], s["items"], `${path41}[${i}]`);
|
|
22806
23164
|
}
|
|
22807
23165
|
}
|
|
22808
23166
|
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
22809
23167
|
const props = s["properties"];
|
|
22810
23168
|
for (const [k, propSchema] of Object.entries(props)) {
|
|
22811
|
-
check(value[k], propSchema, `${
|
|
23169
|
+
check(value[k], propSchema, `${path41}.${k}`);
|
|
22812
23170
|
}
|
|
22813
23171
|
}
|
|
22814
23172
|
}
|
|
22815
23173
|
check(data, schema, "$");
|
|
22816
23174
|
return { valid: errors.length === 0, errors };
|
|
22817
23175
|
}
|
|
22818
|
-
function simpleQuery(data,
|
|
22819
|
-
const parts =
|
|
23176
|
+
function simpleQuery(data, path41) {
|
|
23177
|
+
const parts = path41.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
22820
23178
|
let current = data;
|
|
22821
23179
|
for (const part of parts) {
|
|
22822
23180
|
if (current === null || current === void 0) return void 0;
|
|
@@ -23727,6 +24085,18 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
23727
24085
|
},
|
|
23728
24086
|
transitionAction: { type: "string" },
|
|
23729
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
|
+
},
|
|
23730
24100
|
attachmentUrl: { type: "string" },
|
|
23731
24101
|
attachmentTitle: { type: "string" },
|
|
23732
24102
|
attachmentType: {
|
|
@@ -23909,7 +24279,7 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
23909
24279
|
|
|
23910
24280
|
// src/session-kanban.ts
|
|
23911
24281
|
import { watch } from "node:fs";
|
|
23912
|
-
import { basename as basename13, dirname as
|
|
24282
|
+
import { basename as basename13, dirname as dirname14 } from "node:path";
|
|
23913
24283
|
import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
|
|
23914
24284
|
import {
|
|
23915
24285
|
loadPlan,
|
|
@@ -24527,7 +24897,7 @@ function attachSessionKanbanMirror(context) {
|
|
|
24527
24897
|
const configureWatcher = () => {
|
|
24528
24898
|
const planPath = context.meta["plan.path"];
|
|
24529
24899
|
const taskPath = context.meta["task.path"];
|
|
24530
|
-
const candidate = typeof planPath === "string" && planPath ?
|
|
24900
|
+
const candidate = typeof planPath === "string" && planPath ? dirname14(planPath) : typeof taskPath === "string" && taskPath ? dirname14(taskPath) : "";
|
|
24531
24901
|
if (!candidate || candidate === watchedDir) return;
|
|
24532
24902
|
watcher?.close();
|
|
24533
24903
|
watcher = null;
|
|
@@ -24618,9 +24988,21 @@ async function rebindSessionKanbanTask(context) {
|
|
|
24618
24988
|
context.setCurrentKanbanTask(best.taskId, best.boardId);
|
|
24619
24989
|
return { boardId: best.boardId, taskId: best.taskId };
|
|
24620
24990
|
}
|
|
24991
|
+
var degradationReason;
|
|
24621
24992
|
async function hydrateSessionKanban(context) {
|
|
24622
24993
|
const id = context.session?.id ?? "";
|
|
24623
24994
|
if (!id) return null;
|
|
24995
|
+
try {
|
|
24996
|
+
const board = await hydrateSessionKanbanBoard(context, id);
|
|
24997
|
+
degradationReason = void 0;
|
|
24998
|
+
return board;
|
|
24999
|
+
} catch (error) {
|
|
25000
|
+
degradationReason = error instanceof Error ? error.message : String(error);
|
|
25001
|
+
fireAndForget("hydrate", Promise.reject(error));
|
|
25002
|
+
return null;
|
|
25003
|
+
}
|
|
25004
|
+
}
|
|
25005
|
+
async function hydrateSessionKanbanBoard(context, id) {
|
|
24624
25006
|
await rebindSessionKanbanTask(context);
|
|
24625
25007
|
await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
|
|
24626
25008
|
if (context.projectRoot) {
|
|
@@ -24843,6 +25225,9 @@ var kanbanTool = {
|
|
|
24843
25225
|
description: KANBAN_TOOL_DESCRIPTION,
|
|
24844
25226
|
usageHint: KANBAN_TOOL_USAGE_HINT,
|
|
24845
25227
|
permission: "confirm",
|
|
25228
|
+
// WS-046: gives permission decisions something to key on.
|
|
25229
|
+
// The action performed; kanban has no single file or path subject.
|
|
25230
|
+
subjectKey: "action",
|
|
24846
25231
|
mutating: true,
|
|
24847
25232
|
capabilities: ["fs.write"],
|
|
24848
25233
|
icon: "task",
|
|
@@ -25284,6 +25669,7 @@ var kanbanTool = {
|
|
|
25284
25669
|
actor: input.author,
|
|
25285
25670
|
comment: input.transitionComment,
|
|
25286
25671
|
...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
|
|
25672
|
+
...input.tickChecks !== void 0 ? { tickChecks: input.tickChecks } : {},
|
|
25287
25673
|
...input.attachmentUrl !== void 0 ? {
|
|
25288
25674
|
attachment: {
|
|
25289
25675
|
url: input.attachmentUrl,
|
|
@@ -25676,8 +26062,52 @@ var kanbanTool = {
|
|
|
25676
26062
|
} catch (err) {
|
|
25677
26063
|
return fail(stripLifecycleIssues(err instanceof Error ? err.message : String(err)));
|
|
25678
26064
|
}
|
|
26065
|
+
},
|
|
26066
|
+
serialize(output, input) {
|
|
26067
|
+
return serializeKanbanOutput(output, input);
|
|
25679
26068
|
}
|
|
25680
26069
|
};
|
|
26070
|
+
var KANBAN_BOARD_TRANSCRIPT_BYTE_CAP = 16384;
|
|
26071
|
+
var KANBAN_FULL_BOARD_ACTIONS = /* @__PURE__ */ new Set([
|
|
26072
|
+
"get_board",
|
|
26073
|
+
"export_markdown",
|
|
26074
|
+
"export_task_graph"
|
|
26075
|
+
]);
|
|
26076
|
+
function serializeKanbanOutput(output, input) {
|
|
26077
|
+
const action = input && typeof input === "object" ? input.action : void 0;
|
|
26078
|
+
const board = output.board;
|
|
26079
|
+
if (board) {
|
|
26080
|
+
const keepFull = typeof action === "string" && KANBAN_FULL_BOARD_ACTIONS.has(action);
|
|
26081
|
+
let boardBytes = 0;
|
|
26082
|
+
if (!keepFull) {
|
|
26083
|
+
try {
|
|
26084
|
+
boardBytes = Buffer.byteLength(JSON.stringify(board), "utf8");
|
|
26085
|
+
} catch {
|
|
26086
|
+
boardBytes = 0;
|
|
26087
|
+
}
|
|
26088
|
+
}
|
|
26089
|
+
if (!keepFull && boardBytes > KANBAN_BOARD_TRANSCRIPT_BYTE_CAP) {
|
|
26090
|
+
const columns = {};
|
|
26091
|
+
for (const column of board.columns) {
|
|
26092
|
+
columns[column.title || column.id] = board.tasks.filter(
|
|
26093
|
+
(task) => task.columnId === column.id
|
|
26094
|
+
).length;
|
|
26095
|
+
}
|
|
26096
|
+
const compact = {
|
|
26097
|
+
...output,
|
|
26098
|
+
board: {
|
|
26099
|
+
id: board.id,
|
|
26100
|
+
title: board.title,
|
|
26101
|
+
columns,
|
|
26102
|
+
totalTasks: board.tasks.length,
|
|
26103
|
+
note: `Full board (${boardBytes} bytes) omitted from the transcript; use get_board to load it.`
|
|
26104
|
+
}
|
|
26105
|
+
};
|
|
26106
|
+
return JSON.stringify(compact, null, 2);
|
|
26107
|
+
}
|
|
26108
|
+
}
|
|
26109
|
+
return JSON.stringify(output, null, 2);
|
|
26110
|
+
}
|
|
25681
26111
|
|
|
25682
26112
|
// src/builtin.ts
|
|
25683
26113
|
init_execute_tool();
|
|
@@ -25792,11 +26222,11 @@ var lintTool = {
|
|
|
25792
26222
|
}
|
|
25793
26223
|
};
|
|
25794
26224
|
async function detectLinter(cwd) {
|
|
25795
|
-
const { stat:
|
|
26225
|
+
const { stat: stat20 } = await import("node:fs/promises");
|
|
25796
26226
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
25797
26227
|
for (const f of checks) {
|
|
25798
26228
|
try {
|
|
25799
|
-
await
|
|
26229
|
+
await stat20(`${cwd}/${f}`);
|
|
25800
26230
|
if (f.includes("biome")) return "biome";
|
|
25801
26231
|
if (f.includes("eslint")) return "eslint";
|
|
25802
26232
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -25813,11 +26243,12 @@ init_util();
|
|
|
25813
26243
|
var logsTool = {
|
|
25814
26244
|
name: "logs",
|
|
25815
26245
|
category: "Logs",
|
|
25816
|
-
description: "Read
|
|
25817
|
-
usageHint: "DEBUGGING TOOL \u2014 USE CAREFULLY IN AUTONOMOUS MODE:\n\n- Prefer `path` for local files or `service` for containers
|
|
26246
|
+
description: "Read logs from files or Docker containers. Useful for debugging running applications.",
|
|
26247
|
+
usageHint: "DEBUGGING TOOL \u2014 USE CAREFULLY IN AUTONOMOUS MODE:\n\n- Prefer `path` for local files or `service` for Docker containers.\n- Always use `filter` (regex) when possible to reduce noise and token usage.\n- `since` narrows Docker logs to a recent window.",
|
|
25818
26248
|
permission: "confirm",
|
|
25819
26249
|
mutating: false,
|
|
25820
26250
|
timeoutMs: 3e4,
|
|
26251
|
+
maxOutputBytes: 262144,
|
|
25821
26252
|
capabilities: ["shell.restricted"],
|
|
25822
26253
|
icon: "logs",
|
|
25823
26254
|
inputSchema: {
|
|
@@ -25825,7 +26256,7 @@ var logsTool = {
|
|
|
25825
26256
|
properties: {
|
|
25826
26257
|
service: {
|
|
25827
26258
|
type: "string",
|
|
25828
|
-
description: "
|
|
26259
|
+
description: "Docker container name (passed to `docker logs`)"
|
|
25829
26260
|
},
|
|
25830
26261
|
path: {
|
|
25831
26262
|
type: "string",
|
|
@@ -25837,10 +26268,6 @@ var logsTool = {
|
|
|
25837
26268
|
minimum: 0,
|
|
25838
26269
|
maximum: 1e4
|
|
25839
26270
|
},
|
|
25840
|
-
stream: {
|
|
25841
|
-
type: "boolean",
|
|
25842
|
-
description: "Stream logs continuously (like tail -f) (default: false)"
|
|
25843
|
-
},
|
|
25844
26271
|
filter: {
|
|
25845
26272
|
type: "string",
|
|
25846
26273
|
description: "Regex pattern to filter log lines"
|
|
@@ -25848,7 +26275,7 @@ var logsTool = {
|
|
|
25848
26275
|
since: {
|
|
25849
26276
|
type: "string",
|
|
25850
26277
|
enum: ["1h", "6h", "24h", "all"],
|
|
25851
|
-
description:
|
|
26278
|
+
description: 'Only show Docker logs since duration (ignored for files; "all" = no limit)'
|
|
25852
26279
|
},
|
|
25853
26280
|
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
25854
26281
|
}
|
|
@@ -25865,10 +26292,10 @@ var logsTool = {
|
|
|
25865
26292
|
filterRe = compiled.regex;
|
|
25866
26293
|
}
|
|
25867
26294
|
if (input.service) {
|
|
25868
|
-
return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal);
|
|
26295
|
+
return await dockerLogs(input.service, lines, filterRe, cwd, opts.signal, input.since);
|
|
25869
26296
|
}
|
|
25870
26297
|
if (input.path) {
|
|
25871
|
-
return await fileLogs(
|
|
26298
|
+
return await fileLogs(await safeResolveReal(input.path, ctx), lines, filterRe);
|
|
25872
26299
|
}
|
|
25873
26300
|
return {
|
|
25874
26301
|
source: "none",
|
|
@@ -25882,7 +26309,7 @@ var logsTool = {
|
|
|
25882
26309
|
async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
25883
26310
|
const args = ["logs"];
|
|
25884
26311
|
if (lines > 0) args.push("--tail", String(lines));
|
|
25885
|
-
if (since) {
|
|
26312
|
+
if (since && since !== "all") {
|
|
25886
26313
|
const sinceMap = { "1h": "1h", "6h": "6h", "24h": "24h" };
|
|
25887
26314
|
args.push("--since", sinceMap[since] ?? "1h");
|
|
25888
26315
|
}
|
|
@@ -25896,7 +26323,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25896
26323
|
};
|
|
25897
26324
|
}
|
|
25898
26325
|
args.push("--timestamps", service);
|
|
25899
|
-
return new Promise((
|
|
26326
|
+
return new Promise((resolve18) => {
|
|
25900
26327
|
let stdout = "";
|
|
25901
26328
|
let stderr = "";
|
|
25902
26329
|
const MAX = 2e5;
|
|
@@ -25912,7 +26339,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25912
26339
|
if (settled) return;
|
|
25913
26340
|
settled = true;
|
|
25914
26341
|
clearTimeout(timer);
|
|
25915
|
-
|
|
26342
|
+
resolve18(result);
|
|
25916
26343
|
};
|
|
25917
26344
|
const child = spawn11("docker", args, { cwd, signal, env: buildChildEnv6(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
25918
26345
|
const timer = setTimeout(() => {
|
|
@@ -25947,7 +26374,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
25947
26374
|
}
|
|
25948
26375
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
25949
26376
|
var MAX_TAIL_LINES = 1e5;
|
|
25950
|
-
async function fileLogs(
|
|
26377
|
+
async function fileLogs(path41, lines, filterRe) {
|
|
25951
26378
|
const { createInterface } = await import("node:readline");
|
|
25952
26379
|
const { createReadStream: createReadStream2 } = await import("node:fs");
|
|
25953
26380
|
const entries = [];
|
|
@@ -25956,7 +26383,7 @@ async function fileLogs(path40, lines, filterRe, stream) {
|
|
|
25956
26383
|
let writeIdx = 0;
|
|
25957
26384
|
let totalLines = 0;
|
|
25958
26385
|
const rl = createInterface({
|
|
25959
|
-
input: createReadStream2(
|
|
26386
|
+
input: createReadStream2(path41),
|
|
25960
26387
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
25961
26388
|
});
|
|
25962
26389
|
for await (const line of rl) {
|
|
@@ -25977,11 +26404,11 @@ async function fileLogs(path40, lines, filterRe, stream) {
|
|
|
25977
26404
|
if (parsed) entries.push(parsed);
|
|
25978
26405
|
}
|
|
25979
26406
|
return {
|
|
25980
|
-
source:
|
|
26407
|
+
source: path41,
|
|
25981
26408
|
entries,
|
|
25982
26409
|
total: entries.length,
|
|
25983
26410
|
truncated: totalLines > effLines,
|
|
25984
|
-
stream_mode:
|
|
26411
|
+
stream_mode: false
|
|
25985
26412
|
};
|
|
25986
26413
|
}
|
|
25987
26414
|
function parseLogLines(output, filterRe) {
|
|
@@ -26057,25 +26484,12 @@ var outdatedTool = {
|
|
|
26057
26484
|
inputSchema: {
|
|
26058
26485
|
type: "object",
|
|
26059
26486
|
properties: {
|
|
26060
|
-
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
26061
|
-
format: {
|
|
26062
|
-
type: "string",
|
|
26063
|
-
enum: ["list", "table"],
|
|
26064
|
-
description: "Output format (default: list)"
|
|
26065
|
-
},
|
|
26066
|
-
include_deprecated: {
|
|
26067
|
-
type: "boolean",
|
|
26068
|
-
description: "Include deprecated packages (default: false)"
|
|
26069
|
-
},
|
|
26070
|
-
check: {
|
|
26071
|
-
type: "string",
|
|
26072
|
-
description: "Specific package(s) to check (comma-separated)"
|
|
26073
|
-
}
|
|
26487
|
+
cwd: { type: "string", description: "Working directory (default: cwd)" }
|
|
26074
26488
|
}
|
|
26075
26489
|
},
|
|
26076
26490
|
async execute(input, ctx, opts) {
|
|
26077
26491
|
const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
|
|
26078
|
-
const manager = await detectPackageManager(cwd);
|
|
26492
|
+
const manager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
26079
26493
|
if (manager === "npm") {
|
|
26080
26494
|
try {
|
|
26081
26495
|
const { detectNonJsEcosystem: detectNonJsEcosystem2 } = await Promise.resolve().then(() => (init_legacy_bridge(), legacy_bridge_exports));
|
|
@@ -26128,13 +26542,11 @@ var outdatedTool = {
|
|
|
26128
26542
|
}
|
|
26129
26543
|
}
|
|
26130
26544
|
const args = ["outdated", "--json"];
|
|
26131
|
-
if (input.format === "table") args.push("--table");
|
|
26132
|
-
if (input.include_deprecated) args.push("--include", "deprecated");
|
|
26133
26545
|
return runOutdated(manager, args, cwd, opts.signal);
|
|
26134
26546
|
}
|
|
26135
26547
|
};
|
|
26136
26548
|
function runOutdated(manager, args, cwd, signal) {
|
|
26137
|
-
return new Promise((
|
|
26549
|
+
return new Promise((resolve18) => {
|
|
26138
26550
|
let stdout = "";
|
|
26139
26551
|
let stderr = "";
|
|
26140
26552
|
const MAX = 1e5;
|
|
@@ -26159,10 +26571,10 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
26159
26571
|
});
|
|
26160
26572
|
child.on("close", (code) => {
|
|
26161
26573
|
const result = parseOutdatedOutput(stdout, code ?? 0);
|
|
26162
|
-
|
|
26574
|
+
resolve18(result);
|
|
26163
26575
|
});
|
|
26164
26576
|
child.on("error", (e) => {
|
|
26165
|
-
|
|
26577
|
+
resolve18({
|
|
26166
26578
|
exit_code: 1,
|
|
26167
26579
|
packages: [],
|
|
26168
26580
|
total: 0,
|
|
@@ -26183,27 +26595,39 @@ function parseOutdatedOutput(json2, exitCode) {
|
|
|
26183
26595
|
truncated: false
|
|
26184
26596
|
};
|
|
26185
26597
|
}
|
|
26598
|
+
const truncated = json2.length >= 1e5 || Buffer.byteLength(json2, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
|
|
26599
|
+
let parsedOk = false;
|
|
26186
26600
|
try {
|
|
26187
26601
|
const data = JSON.parse(json2);
|
|
26602
|
+
parsedOk = true;
|
|
26188
26603
|
for (const name of Object.keys(data)) {
|
|
26189
|
-
const info = data[name];
|
|
26604
|
+
const info = data[name] ?? {};
|
|
26605
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
26190
26606
|
packages.push({
|
|
26191
26607
|
name,
|
|
26192
|
-
current: info
|
|
26193
|
-
latest: info
|
|
26194
|
-
wanted: info
|
|
26195
|
-
|
|
26196
|
-
|
|
26608
|
+
current: str(info["current"]) ?? "unknown",
|
|
26609
|
+
latest: str(info["latest"]) ?? "unknown",
|
|
26610
|
+
wanted: str(info["wanted"]) ?? "unknown",
|
|
26611
|
+
// npm calls it `type`; pnpm calls it `dependencyType`.
|
|
26612
|
+
type: str(info["type"]) ?? str(info["dependencyType"]) ?? "unknown",
|
|
26613
|
+
location: str(info["location"]) ?? name
|
|
26197
26614
|
});
|
|
26198
26615
|
}
|
|
26199
26616
|
} catch {
|
|
26617
|
+
}
|
|
26618
|
+
const outdatedFound = parsedOk && exitCode === 1;
|
|
26619
|
+
let output = normalizeCommandOutput(json2);
|
|
26620
|
+
if (outdatedFound) {
|
|
26621
|
+
output = `${output}
|
|
26622
|
+
|
|
26623
|
+
Note: exit code 1 from \`outdated\` means outdated packages were found (expected); treated as success.`;
|
|
26200
26624
|
}
|
|
26201
26625
|
return {
|
|
26202
|
-
exit_code: exitCode,
|
|
26626
|
+
exit_code: outdatedFound ? 0 : exitCode,
|
|
26203
26627
|
packages,
|
|
26204
26628
|
total: packages.length,
|
|
26205
|
-
output
|
|
26206
|
-
truncated
|
|
26629
|
+
output,
|
|
26630
|
+
truncated
|
|
26207
26631
|
};
|
|
26208
26632
|
}
|
|
26209
26633
|
|
|
@@ -26212,8 +26636,8 @@ init_util();
|
|
|
26212
26636
|
import { spawn as spawn13 } from "node:child_process";
|
|
26213
26637
|
import * as fs28 from "node:fs/promises";
|
|
26214
26638
|
import * as os9 from "node:os";
|
|
26215
|
-
import * as
|
|
26216
|
-
import { buildChildEnv as buildChildEnv8, toErrorMessage as
|
|
26639
|
+
import * as path34 from "node:path";
|
|
26640
|
+
import { buildChildEnv as buildChildEnv8, toErrorMessage as toErrorMessage7 } from "@wrongstack/core/utils";
|
|
26217
26641
|
var patchTool = {
|
|
26218
26642
|
name: "patch",
|
|
26219
26643
|
category: "Filesystem",
|
|
@@ -26257,26 +26681,26 @@ var patchTool = {
|
|
|
26257
26681
|
try {
|
|
26258
26682
|
dir = input.directory ? await safeResolveReal(input.directory, ctx) : ctx.cwd;
|
|
26259
26683
|
} catch (err) {
|
|
26260
|
-
return refuse(`patch refused: ${
|
|
26684
|
+
return refuse(`patch refused: ${toErrorMessage7(err)}`);
|
|
26261
26685
|
}
|
|
26262
|
-
const realRoot = await fs28.realpath(ctx.projectRoot).catch(() =>
|
|
26686
|
+
const realRoot = await fs28.realpath(ctx.projectRoot).catch(() => path34.resolve(ctx.projectRoot));
|
|
26263
26687
|
const targets = extractDiffTargets(input.patch);
|
|
26264
26688
|
const resolvedTargets = [];
|
|
26265
26689
|
for (const t of targets) {
|
|
26266
26690
|
const stripped = stripPathComponents(t.raw, strip);
|
|
26267
26691
|
if (!stripped) continue;
|
|
26268
|
-
if (
|
|
26692
|
+
if (path34.isAbsolute(stripped)) {
|
|
26269
26693
|
return refuse(`patch refused: target "${t.raw}" strips to absolute path`);
|
|
26270
26694
|
}
|
|
26271
|
-
const candidate =
|
|
26695
|
+
const candidate = path34.resolve(dir, stripped);
|
|
26272
26696
|
let real;
|
|
26273
26697
|
try {
|
|
26274
26698
|
real = await resolveRealInsideRoot(candidate, ctx);
|
|
26275
26699
|
} catch (err) {
|
|
26276
|
-
return refuse(`patch refused: target "${t.raw}" ${
|
|
26700
|
+
return refuse(`patch refused: target "${t.raw}" ${toErrorMessage7(err)}`);
|
|
26277
26701
|
}
|
|
26278
|
-
const rel =
|
|
26279
|
-
if (rel.startsWith("..") ||
|
|
26702
|
+
const rel = path34.relative(realRoot, real);
|
|
26703
|
+
if (rel.startsWith("..") || path34.isAbsolute(rel)) {
|
|
26280
26704
|
return refuse(`patch refused: target "${t.raw}" resolves outside project root`);
|
|
26281
26705
|
}
|
|
26282
26706
|
resolvedTargets.push({ raw: t.raw, deleted: t.deleted, abs: real });
|
|
@@ -26290,11 +26714,11 @@ var patchTool = {
|
|
|
26290
26714
|
beforeContents.set(target.abs, await readTextForTracking(target.abs));
|
|
26291
26715
|
}
|
|
26292
26716
|
}
|
|
26293
|
-
const tmpDir = await fs28.mkdtemp(
|
|
26717
|
+
const tmpDir = await fs28.mkdtemp(path34.join(os9.tmpdir(), ".wstack_patch_"));
|
|
26294
26718
|
try {
|
|
26295
26719
|
await fs28.chmod(tmpDir, 448).catch(() => {
|
|
26296
26720
|
});
|
|
26297
|
-
const patchFile =
|
|
26721
|
+
const patchFile = path34.join(tmpDir, "in.diff");
|
|
26298
26722
|
await fs28.writeFile(patchFile, input.patch, { mode: 384 });
|
|
26299
26723
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
26300
26724
|
const result = await runPatch(args, dir, opts.signal, {
|
|
@@ -26307,8 +26731,8 @@ var patchTool = {
|
|
|
26307
26731
|
for (const target of resolvedTargets) {
|
|
26308
26732
|
const abs = target.abs;
|
|
26309
26733
|
const before = beforeContents.get(abs) ?? null;
|
|
26310
|
-
const
|
|
26311
|
-
if (!
|
|
26734
|
+
const stat20 = await fs28.stat(abs).catch(() => null);
|
|
26735
|
+
if (!stat20?.isFile()) {
|
|
26312
26736
|
if (beforeExisted.has(abs)) {
|
|
26313
26737
|
touched.push(abs);
|
|
26314
26738
|
ctx.session?.recordFileChange?.({
|
|
@@ -26323,7 +26747,7 @@ var patchTool = {
|
|
|
26323
26747
|
const after = await readTextForTracking(abs);
|
|
26324
26748
|
if (after === null || after === before) continue;
|
|
26325
26749
|
touched.push(abs);
|
|
26326
|
-
ctx.recordRead?.(abs,
|
|
26750
|
+
ctx.recordRead?.(abs, stat20.mtimeMs, "write", sha256hex(after));
|
|
26327
26751
|
ctx.session?.recordFileChange?.({
|
|
26328
26752
|
path: abs,
|
|
26329
26753
|
action: before === null ? "created" : "modified",
|
|
@@ -26334,7 +26758,7 @@ var patchTool = {
|
|
|
26334
26758
|
}
|
|
26335
26759
|
if (result.exitCode !== 0) {
|
|
26336
26760
|
if (!dryRun) {
|
|
26337
|
-
const partial = touched.length > 0 ? ` ${touched.length} file(s) were still modified on disk and have been recorded for rewind: ${touched.map((p) =>
|
|
26761
|
+
const partial = touched.length > 0 ? ` ${touched.length} file(s) were still modified on disk and have been recorded for rewind: ${touched.map((p) => path34.relative(realRoot, p) || p).join(", ")}.` : "";
|
|
26338
26762
|
return {
|
|
26339
26763
|
applied: touched.length,
|
|
26340
26764
|
rejected: 1,
|
|
@@ -26342,7 +26766,7 @@ var patchTool = {
|
|
|
26342
26766
|
// success path (which returns GNU patch's dir-relative names).
|
|
26343
26767
|
// `touched` entries are realpaths from resolveRealInsideRoot, and
|
|
26344
26768
|
// realRoot is also a realpath, so path.relative is like-for-like.
|
|
26345
|
-
files: touched.map((p) =>
|
|
26769
|
+
files: touched.map((p) => path34.relative(realRoot, p) || p),
|
|
26346
26770
|
dry_run: dryRun,
|
|
26347
26771
|
message: `patch failed: ${result.stderr || result.stdout}${partial}`
|
|
26348
26772
|
};
|
|
@@ -26358,7 +26782,7 @@ var patchTool = {
|
|
|
26358
26782
|
}
|
|
26359
26783
|
const patched = result.engine === "git" ? [
|
|
26360
26784
|
...new Set(
|
|
26361
|
-
resolvedTargets.map((target) =>
|
|
26785
|
+
resolvedTargets.map((target) => path34.relative(dir, target.abs) || target.abs)
|
|
26362
26786
|
)
|
|
26363
26787
|
] : extractPatchedFiles(result.stdout);
|
|
26364
26788
|
return {
|
|
@@ -26377,8 +26801,8 @@ var patchTool = {
|
|
|
26377
26801
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
26378
26802
|
async function readTextForTracking(absPath) {
|
|
26379
26803
|
try {
|
|
26380
|
-
const
|
|
26381
|
-
if (!
|
|
26804
|
+
const stat20 = await fs28.stat(absPath);
|
|
26805
|
+
if (!stat20.isFile() || stat20.size > MAX_TRACKING_BYTES) return null;
|
|
26382
26806
|
const buf = await fs28.readFile(absPath);
|
|
26383
26807
|
if (buf.includes(0)) return null;
|
|
26384
26808
|
return buf.toString("utf8");
|
|
@@ -26463,7 +26887,7 @@ function runPatch(args, cwd, signal, fallback) {
|
|
|
26463
26887
|
});
|
|
26464
26888
|
}
|
|
26465
26889
|
function runPatchProcess(command, args, cwd, signal) {
|
|
26466
|
-
return new Promise((
|
|
26890
|
+
return new Promise((resolve18) => {
|
|
26467
26891
|
let stdout = "";
|
|
26468
26892
|
let stderr = "";
|
|
26469
26893
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
@@ -26482,11 +26906,11 @@ function runPatchProcess(command, args, cwd, signal) {
|
|
|
26482
26906
|
});
|
|
26483
26907
|
child.on(
|
|
26484
26908
|
"close",
|
|
26485
|
-
(code) =>
|
|
26909
|
+
(code) => resolve18({ exitCode: code ?? 1, stdout, stderr, unavailable: false })
|
|
26486
26910
|
);
|
|
26487
26911
|
child.on(
|
|
26488
26912
|
"error",
|
|
26489
|
-
(e) =>
|
|
26913
|
+
(e) => resolve18({
|
|
26490
26914
|
exitCode: 1,
|
|
26491
26915
|
stdout: "",
|
|
26492
26916
|
stderr: e.message,
|
|
@@ -26864,7 +27288,8 @@ var todoTool = {
|
|
|
26864
27288
|
}
|
|
26865
27289
|
for (const planId of completedPlanIds) {
|
|
26866
27290
|
if (pendingPlanIds.has(planId)) continue;
|
|
26867
|
-
const
|
|
27291
|
+
const meta = ctx.meta;
|
|
27292
|
+
const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
|
|
26868
27293
|
if (typeof planPath !== "string" || !planPath) continue;
|
|
26869
27294
|
try {
|
|
26870
27295
|
const plan = await loadPlan2(planPath);
|
|
@@ -26877,7 +27302,8 @@ var todoTool = {
|
|
|
26877
27302
|
}
|
|
26878
27303
|
for (const taskId of completedTaskIds) {
|
|
26879
27304
|
if (pendingTaskIds.has(taskId)) continue;
|
|
26880
|
-
const
|
|
27305
|
+
const meta = ctx.meta;
|
|
27306
|
+
const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
|
|
26881
27307
|
if (typeof taskPath !== "string" || !taskPath) continue;
|
|
26882
27308
|
try {
|
|
26883
27309
|
const file = await loadTasks3(taskPath);
|
|
@@ -26993,7 +27419,16 @@ var planTool = {
|
|
|
26993
27419
|
sessionPlanPath.lastIndexOf("/"),
|
|
26994
27420
|
sessionPlanPath.lastIndexOf("\\")
|
|
26995
27421
|
);
|
|
26996
|
-
|
|
27422
|
+
if (lastSep < 0) {
|
|
27423
|
+
return {
|
|
27424
|
+
ok: false,
|
|
27425
|
+
message: `Cannot derive the project-scoped plan path: session plan path "${sessionPlanPath}" has no directory component.`,
|
|
27426
|
+
plan: "",
|
|
27427
|
+
count: 0,
|
|
27428
|
+
open: 0
|
|
27429
|
+
};
|
|
27430
|
+
}
|
|
27431
|
+
planPath = sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json";
|
|
26997
27432
|
}
|
|
26998
27433
|
} else {
|
|
26999
27434
|
planPath = sessionPlanPath;
|
|
@@ -27186,6 +27621,7 @@ var planTool = {
|
|
|
27186
27621
|
open: 0
|
|
27187
27622
|
};
|
|
27188
27623
|
}
|
|
27624
|
+
ctx.meta["plan.path.resolved"] = planPath;
|
|
27189
27625
|
await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
|
|
27190
27626
|
if (todosToReplace) {
|
|
27191
27627
|
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
@@ -27218,6 +27654,7 @@ var planTool = {
|
|
|
27218
27654
|
});
|
|
27219
27655
|
return f;
|
|
27220
27656
|
});
|
|
27657
|
+
ctx.meta["task.path.resolved"] = taskPath;
|
|
27221
27658
|
return mkResult(
|
|
27222
27659
|
plan,
|
|
27223
27660
|
true,
|
|
@@ -27251,14 +27688,14 @@ function mkResult(plan, ok, message, todos) {
|
|
|
27251
27688
|
// src/read.ts
|
|
27252
27689
|
init_util();
|
|
27253
27690
|
import * as fs29 from "node:fs/promises";
|
|
27254
|
-
import { FsError, ToolValidationError as
|
|
27255
|
-
import { toErrorMessage as
|
|
27691
|
+
import { FsError, ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
|
|
27692
|
+
import { toErrorMessage as toErrorMessage8 } from "@wrongstack/core/utils";
|
|
27256
27693
|
var ADVANCED_MODE_META_KEY = "tools.read.advancedMode";
|
|
27257
27694
|
var MAX_BYTES2 = 5 * 1024 * 1024;
|
|
27258
27695
|
var readTool = {
|
|
27259
27696
|
name: "read",
|
|
27260
27697
|
category: "Filesystem",
|
|
27261
|
-
description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed
|
|
27698
|
+
description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed in the form `N\u2192content` (line number, then a `\u2192` separator, then the raw line). The `N\u2192` prefix is display-only \u2014 always strip it before reusing the text, e.g. never include it in `edit.old_string`. When advanced mode is on or `includeSymbols` is set, the result also includes a `symbols` field listing codebase-index symbol names, kinds, and line numbers for the file (not file content).",
|
|
27262
27699
|
usageHint: "FOUNDATIONAL TOOL \u2014 call this before almost any edit operation.\n\nBest practices:\n- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\n- Use `offset` + `limit` for very large files instead of reading everything at once.\n- Default limit is generous (2000 lines) but can be increased.\n- The output format is designed to be directly usable as context for `edit` operations.\n- Set `includeSymbols: true` to also receive the codebase-index symbol listing for the file.\n- Enable advanced mode (`ctx.meta['tools.read.advancedMode'] = true`) to auto-inject symbols on every read.",
|
|
27263
27700
|
selection: {
|
|
27264
27701
|
doNotUseWhen: "you need to search many files for matching content.",
|
|
@@ -27279,11 +27716,13 @@ var readTool = {
|
|
|
27279
27716
|
},
|
|
27280
27717
|
offset: {
|
|
27281
27718
|
type: "integer",
|
|
27719
|
+
minimum: 1,
|
|
27282
27720
|
description: "1-based starting line number. Use together with `limit` for large files."
|
|
27283
27721
|
},
|
|
27284
27722
|
limit: {
|
|
27285
27723
|
type: "integer",
|
|
27286
|
-
|
|
27724
|
+
minimum: 0,
|
|
27725
|
+
description: "Maximum number of lines to return (default 2000). Values above 5000 are clamped to 5000 \u2014 page with `offset` for more."
|
|
27287
27726
|
},
|
|
27288
27727
|
mode: {
|
|
27289
27728
|
type: "string",
|
|
@@ -27299,16 +27738,16 @@ var readTool = {
|
|
|
27299
27738
|
},
|
|
27300
27739
|
async execute(input, ctx, execOpts) {
|
|
27301
27740
|
if (!input?.path) {
|
|
27302
|
-
throw new
|
|
27741
|
+
throw new ToolValidationError8({
|
|
27303
27742
|
message: "read: path is required",
|
|
27304
27743
|
field: "path"
|
|
27305
27744
|
});
|
|
27306
27745
|
}
|
|
27307
27746
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
27308
27747
|
const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
|
|
27309
|
-
let
|
|
27748
|
+
let stat20;
|
|
27310
27749
|
try {
|
|
27311
|
-
|
|
27750
|
+
stat20 = await fs29.stat(absPath);
|
|
27312
27751
|
} catch (err) {
|
|
27313
27752
|
const code = err.code;
|
|
27314
27753
|
if (code === "ENOENT") {
|
|
@@ -27320,14 +27759,14 @@ var readTool = {
|
|
|
27320
27759
|
});
|
|
27321
27760
|
}
|
|
27322
27761
|
throw new FsError({
|
|
27323
|
-
message: `read: failed to stat "${input.path}": ${
|
|
27762
|
+
message: `read: failed to stat "${input.path}": ${toErrorMessage8(err)}`,
|
|
27324
27763
|
code: "FS_READ_FAILED",
|
|
27325
27764
|
path: absPath,
|
|
27326
27765
|
context: { errno: code },
|
|
27327
27766
|
cause: err
|
|
27328
27767
|
});
|
|
27329
27768
|
}
|
|
27330
|
-
if (!
|
|
27769
|
+
if (!stat20.isFile()) {
|
|
27331
27770
|
throw new FsError({
|
|
27332
27771
|
message: `read: "${input.path}" is not a regular file`,
|
|
27333
27772
|
code: "FS_READ_FAILED",
|
|
@@ -27335,23 +27774,23 @@ var readTool = {
|
|
|
27335
27774
|
context: { reason: "not-a-regular-file" }
|
|
27336
27775
|
});
|
|
27337
27776
|
}
|
|
27338
|
-
if (
|
|
27777
|
+
if (stat20.size > MAX_BYTES2) {
|
|
27339
27778
|
throw new FsError({
|
|
27340
|
-
message: `read: file too large (${
|
|
27779
|
+
message: `read: file too large (${stat20.size} bytes, limit ${MAX_BYTES2})`,
|
|
27341
27780
|
code: "FS_READ_FAILED",
|
|
27342
27781
|
path: absPath,
|
|
27343
|
-
context: { size:
|
|
27782
|
+
context: { size: stat20.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
27344
27783
|
});
|
|
27345
27784
|
}
|
|
27346
27785
|
const offset = Math.max(1, input.offset ?? 1);
|
|
27347
27786
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
27348
27787
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
27349
27788
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
27350
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
27351
|
-
ctx.recordRead(absPath,
|
|
27789
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat20.mtimeMs, offset, requestedEnd)) {
|
|
27790
|
+
ctx.recordRead(absPath, stat20.mtimeMs);
|
|
27352
27791
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27353
27792
|
return {
|
|
27354
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
27793
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat20.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
27355
27794
|
total_lines: prior.totalLines,
|
|
27356
27795
|
encoding: "utf8",
|
|
27357
27796
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -27362,18 +27801,23 @@ var readTool = {
|
|
|
27362
27801
|
}
|
|
27363
27802
|
const buf = await fs29.readFile(absPath);
|
|
27364
27803
|
if (isBinaryBuffer(buf)) {
|
|
27365
|
-
throw new
|
|
27804
|
+
throw new FsError({
|
|
27805
|
+
message: `read: "${input.path}" appears to be binary`,
|
|
27806
|
+
code: "FS_READ_FAILED",
|
|
27807
|
+
path: absPath,
|
|
27808
|
+
context: { reason: "binary" }
|
|
27809
|
+
});
|
|
27366
27810
|
}
|
|
27367
27811
|
const text = buf.toString("utf8");
|
|
27368
27812
|
const contentHash = sha256hex(text);
|
|
27369
27813
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
27370
27814
|
const total = allLines.length;
|
|
27371
27815
|
if (input.mode === "summary") {
|
|
27372
|
-
ctx.recordRead(absPath,
|
|
27373
|
-
rememberReadRange(ctx, absPath,
|
|
27816
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
|
|
27817
|
+
rememberReadRange(ctx, absPath, stat20.mtimeMs, total, 1, Math.min(total, 200));
|
|
27374
27818
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27375
27819
|
return {
|
|
27376
|
-
text: summarizeFile(input.path,
|
|
27820
|
+
text: summarizeFile(input.path, stat20.size, allLines),
|
|
27377
27821
|
total_lines: total,
|
|
27378
27822
|
encoding: "utf8",
|
|
27379
27823
|
truncated: total > 200,
|
|
@@ -27385,8 +27829,8 @@ var readTool = {
|
|
|
27385
27829
|
};
|
|
27386
27830
|
}
|
|
27387
27831
|
if (limit === 0) {
|
|
27388
|
-
ctx.recordRead(absPath,
|
|
27389
|
-
rememberReadRange(ctx, absPath,
|
|
27832
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
|
|
27833
|
+
rememberReadRange(ctx, absPath, stat20.mtimeMs, total, 1, 0);
|
|
27390
27834
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27391
27835
|
return {
|
|
27392
27836
|
text: "",
|
|
@@ -27398,8 +27842,8 @@ var readTool = {
|
|
|
27398
27842
|
};
|
|
27399
27843
|
}
|
|
27400
27844
|
if (offset > total) {
|
|
27401
|
-
ctx.recordRead(absPath,
|
|
27402
|
-
rememberReadRange(ctx, absPath,
|
|
27845
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
|
|
27846
|
+
rememberReadRange(ctx, absPath, stat20.mtimeMs, total, total + 1, total + 1);
|
|
27403
27847
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27404
27848
|
return {
|
|
27405
27849
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
@@ -27414,8 +27858,8 @@ var readTool = {
|
|
|
27414
27858
|
const truncated = offset - 1 + slice.length < total;
|
|
27415
27859
|
const width = String(offset + slice.length - 1).length;
|
|
27416
27860
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
27417
|
-
ctx.recordRead(absPath,
|
|
27418
|
-
rememberReadRange(ctx, absPath,
|
|
27861
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "user", contentHash);
|
|
27862
|
+
rememberReadRange(ctx, absPath, stat20.mtimeMs, total, offset, offset + slice.length - 1);
|
|
27419
27863
|
const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
27420
27864
|
return {
|
|
27421
27865
|
text: numbered,
|
|
@@ -27523,8 +27967,8 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
27523
27967
|
// src/replace.ts
|
|
27524
27968
|
import { spawn as spawn14 } from "node:child_process";
|
|
27525
27969
|
import * as fs30 from "node:fs/promises";
|
|
27526
|
-
import * as
|
|
27527
|
-
import { ToolValidationError as
|
|
27970
|
+
import * as path35 from "node:path";
|
|
27971
|
+
import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
|
|
27528
27972
|
import {
|
|
27529
27973
|
atomicWrite as atomicWrite3,
|
|
27530
27974
|
buildChildEnv as buildChildEnv9,
|
|
@@ -27536,12 +27980,13 @@ import {
|
|
|
27536
27980
|
unifiedDiff as unifiedDiff2
|
|
27537
27981
|
} from "@wrongstack/core/utils";
|
|
27538
27982
|
init_util();
|
|
27983
|
+
var MAX_DIFF_BYTES2 = 262144;
|
|
27539
27984
|
var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
27540
27985
|
var replaceTool = {
|
|
27541
27986
|
name: "replace",
|
|
27542
27987
|
category: "Transform",
|
|
27543
27988
|
description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool. Dry-run is ON by default \u2014 set `dry_run: false` to apply changes.",
|
|
27544
|
-
usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
|
|
27989
|
+
usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\n5. `replacement` supports regex substitutions: `$1`\u2013`$9` insert capture groups, `$&` inserts the whole match, and `$$` inserts a literal dollar sign.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
|
|
27545
27990
|
permission: "confirm",
|
|
27546
27991
|
// WS-046: gives permission decisions something to key on.
|
|
27547
27992
|
// The file scope being rewritten, not the pattern: a trust rule should say
|
|
@@ -27551,11 +27996,15 @@ var replaceTool = {
|
|
|
27551
27996
|
capabilities: ["fs.write"],
|
|
27552
27997
|
icon: "edit",
|
|
27553
27998
|
timeoutMs: 3e4,
|
|
27999
|
+
maxOutputBytes: 262144,
|
|
27554
28000
|
inputSchema: {
|
|
27555
28001
|
type: "object",
|
|
27556
28002
|
properties: {
|
|
27557
28003
|
pattern: { type: "string", description: "Regex pattern to match" },
|
|
27558
|
-
replacement: {
|
|
28004
|
+
replacement: {
|
|
28005
|
+
type: "string",
|
|
28006
|
+
description: "Replacement string. Supports `$1`\u2013`$9` (capture groups), `$&` (whole match), and `$$` (literal dollar sign) \u2014 same semantics as JavaScript String.replace."
|
|
28007
|
+
},
|
|
27559
28008
|
files: {
|
|
27560
28009
|
type: "string",
|
|
27561
28010
|
description: "File(s) to target: single path, comma-separated list, or glob pattern"
|
|
@@ -27571,19 +28020,19 @@ var replaceTool = {
|
|
|
27571
28020
|
},
|
|
27572
28021
|
async execute(input, ctx) {
|
|
27573
28022
|
if (!input?.pattern) {
|
|
27574
|
-
throw new
|
|
28023
|
+
throw new ToolValidationError9({
|
|
27575
28024
|
message: "replace: pattern is required",
|
|
27576
28025
|
field: "pattern"
|
|
27577
28026
|
});
|
|
27578
28027
|
}
|
|
27579
28028
|
if (input.replacement === void 0) {
|
|
27580
|
-
throw new
|
|
28029
|
+
throw new ToolValidationError9({
|
|
27581
28030
|
message: "replace: replacement is required",
|
|
27582
28031
|
field: "replacement"
|
|
27583
28032
|
});
|
|
27584
28033
|
}
|
|
27585
28034
|
if (!input?.files) {
|
|
27586
|
-
throw new
|
|
28035
|
+
throw new ToolValidationError9({
|
|
27587
28036
|
message: "replace: files is required",
|
|
27588
28037
|
field: "files"
|
|
27589
28038
|
});
|
|
@@ -27591,7 +28040,7 @@ var replaceTool = {
|
|
|
27591
28040
|
const replaceAll = input.replace_all ?? true;
|
|
27592
28041
|
const compiled = compileUserRegex(input.pattern, "g");
|
|
27593
28042
|
if (!compiled.ok) {
|
|
27594
|
-
throw new
|
|
28043
|
+
throw new ToolValidationError9({
|
|
27595
28044
|
message: `replace: ${compiled.reason}`,
|
|
27596
28045
|
field: "pattern"
|
|
27597
28046
|
});
|
|
@@ -27604,6 +28053,9 @@ var replaceTool = {
|
|
|
27604
28053
|
const realRoot = await fs30.realpath(ctx.projectRoot).catch(() => ctx.projectRoot);
|
|
27605
28054
|
const results = [];
|
|
27606
28055
|
let totalReplacements = 0;
|
|
28056
|
+
let diffBytesUsed = 0;
|
|
28057
|
+
let diffsOmitted = 0;
|
|
28058
|
+
let diffsTruncated = 0;
|
|
27607
28059
|
for (const absPath of fileList) {
|
|
27608
28060
|
const lstat2 = await fs30.lstat(absPath).catch((err) => {
|
|
27609
28061
|
if (err.code === "ENOENT") return null;
|
|
@@ -27617,10 +28069,10 @@ var replaceTool = {
|
|
|
27617
28069
|
} catch {
|
|
27618
28070
|
continue;
|
|
27619
28071
|
}
|
|
27620
|
-
const rel =
|
|
27621
|
-
if (rel.startsWith("..") ||
|
|
27622
|
-
const
|
|
27623
|
-
if (!
|
|
28072
|
+
const rel = path35.relative(realRoot, realPath);
|
|
28073
|
+
if (rel.startsWith("..") || path35.isAbsolute(rel)) continue;
|
|
28074
|
+
const stat20 = await fs30.stat(realPath).catch(() => null);
|
|
28075
|
+
if (!stat20?.isFile()) continue;
|
|
27624
28076
|
let content;
|
|
27625
28077
|
try {
|
|
27626
28078
|
const buf = await fs30.readFile(realPath);
|
|
@@ -27639,13 +28091,13 @@ var replaceTool = {
|
|
|
27639
28091
|
let newContentLf = contentLf;
|
|
27640
28092
|
for (let i = matches.length - 1; i >= 0; i--) {
|
|
27641
28093
|
const m = expectDefined8(matches[i]);
|
|
27642
|
-
newContentLf = newContentLf.slice(0, m.index) + input.replacement + newContentLf.slice(expectDefined8(m.index) + m[0].length);
|
|
28094
|
+
newContentLf = newContentLf.slice(0, m.index) + expandReplacement(input.replacement, m) + newContentLf.slice(expectDefined8(m.index) + m[0].length);
|
|
27643
28095
|
}
|
|
27644
28096
|
re.lastIndex = 0;
|
|
27645
28097
|
totalReplacements += count;
|
|
27646
28098
|
if (!dryRun) {
|
|
27647
28099
|
const newContent = toStyle2(newContentLf, style);
|
|
27648
|
-
await atomicWrite3(realPath, newContent, { mode:
|
|
28100
|
+
await atomicWrite3(realPath, newContent, { mode: stat20.mode & 511 });
|
|
27649
28101
|
const written = await fs30.stat(realPath).catch(() => null);
|
|
27650
28102
|
if (written) {
|
|
27651
28103
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
@@ -27657,24 +28109,76 @@ var replaceTool = {
|
|
|
27657
28109
|
after: newContent
|
|
27658
28110
|
});
|
|
27659
28111
|
}
|
|
27660
|
-
|
|
28112
|
+
let diff = dryRun || matches.length > 0 ? unifiedDiff2(content, toStyle2(newContentLf, style), {
|
|
27661
28113
|
fromFile: absPath,
|
|
27662
28114
|
toFile: absPath
|
|
27663
28115
|
}) : void 0;
|
|
28116
|
+
if (diff !== void 0) {
|
|
28117
|
+
const remaining = MAX_DIFF_BYTES2 - diffBytesUsed;
|
|
28118
|
+
if (remaining <= 0) {
|
|
28119
|
+
diff = void 0;
|
|
28120
|
+
diffsOmitted++;
|
|
28121
|
+
} else {
|
|
28122
|
+
const capped = truncateDiffPayload(diff, remaining);
|
|
28123
|
+
if (capped.truncated) diffsTruncated++;
|
|
28124
|
+
diff = capped.text;
|
|
28125
|
+
diffBytesUsed += Buffer.byteLength(diff, "utf8");
|
|
28126
|
+
}
|
|
28127
|
+
}
|
|
27664
28128
|
results.push({
|
|
27665
28129
|
path: absPath,
|
|
27666
28130
|
replacements: matches.length,
|
|
27667
28131
|
diff
|
|
27668
28132
|
});
|
|
27669
28133
|
}
|
|
28134
|
+
const overBudget = diffsOmitted > 0 || diffsTruncated > 0;
|
|
27670
28135
|
return {
|
|
27671
28136
|
files_modified: results.length,
|
|
27672
28137
|
total_replacements: totalReplacements,
|
|
27673
28138
|
results,
|
|
27674
|
-
dry_run: dryRun
|
|
28139
|
+
dry_run: dryRun,
|
|
28140
|
+
note: overBudget ? `Diff payload exceeded the 256 KiB output budget: ${diffsTruncated} diff(s) truncated, ${diffsOmitted} diff(s) omitted. Replacement counts are complete; use the read tool to inspect individual files.` : void 0
|
|
27675
28141
|
};
|
|
27676
28142
|
}
|
|
27677
28143
|
};
|
|
28144
|
+
function expandReplacement(template, match) {
|
|
28145
|
+
if (!template.includes("$")) return template;
|
|
28146
|
+
let out = "";
|
|
28147
|
+
for (let i = 0; i < template.length; i++) {
|
|
28148
|
+
const ch = template[i];
|
|
28149
|
+
if (ch !== "$") {
|
|
28150
|
+
out += ch;
|
|
28151
|
+
continue;
|
|
28152
|
+
}
|
|
28153
|
+
const next = template[i + 1];
|
|
28154
|
+
if (next === "$") {
|
|
28155
|
+
out += "$";
|
|
28156
|
+
i++;
|
|
28157
|
+
} else if (next === "&") {
|
|
28158
|
+
out += match[0];
|
|
28159
|
+
i++;
|
|
28160
|
+
} else if (next !== void 0 && next >= "1" && next <= "9") {
|
|
28161
|
+
const idx = next.charCodeAt(0) - 48;
|
|
28162
|
+
if (idx < match.length) {
|
|
28163
|
+
out += match[idx] ?? "";
|
|
28164
|
+
i++;
|
|
28165
|
+
} else {
|
|
28166
|
+
out += "$";
|
|
28167
|
+
}
|
|
28168
|
+
} else {
|
|
28169
|
+
out += "$";
|
|
28170
|
+
}
|
|
28171
|
+
}
|
|
28172
|
+
return out;
|
|
28173
|
+
}
|
|
28174
|
+
function passesExtraGlob(extraGlob, name, full) {
|
|
28175
|
+
extraGlob.lastIndex = 0;
|
|
28176
|
+
const nameMatch = extraGlob.test(name);
|
|
28177
|
+
extraGlob.lastIndex = 0;
|
|
28178
|
+
const fullMatch = extraGlob.test(full);
|
|
28179
|
+
extraGlob.lastIndex = 0;
|
|
28180
|
+
return nameMatch || fullMatch;
|
|
28181
|
+
}
|
|
27678
28182
|
async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
27679
28183
|
const base = ctx.cwd;
|
|
27680
28184
|
const normalized = filesInput.trim();
|
|
@@ -27685,8 +28189,9 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
27685
28189
|
const resolved = [];
|
|
27686
28190
|
for (const p of parts) {
|
|
27687
28191
|
const absPath = safeResolve(p, ctx);
|
|
27688
|
-
|
|
27689
|
-
|
|
28192
|
+
if (extraGlob && !passesExtraGlob(extraGlob, path35.basename(absPath), absPath)) continue;
|
|
28193
|
+
const stat20 = await fs30.stat(absPath).catch(() => null);
|
|
28194
|
+
if (stat20?.isFile()) {
|
|
27690
28195
|
resolved.push(absPath);
|
|
27691
28196
|
}
|
|
27692
28197
|
}
|
|
@@ -27697,26 +28202,32 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
27697
28202
|
if (rgAvailable) {
|
|
27698
28203
|
try {
|
|
27699
28204
|
const { promise } = spawnRgFind(pattern, base);
|
|
27700
|
-
|
|
28205
|
+
const files = await promise;
|
|
28206
|
+
if (extraGlob) {
|
|
28207
|
+
return files.filter((f) => passesExtraGlob(extraGlob, path35.basename(f), f));
|
|
28208
|
+
}
|
|
28209
|
+
return files;
|
|
27701
28210
|
} catch {
|
|
27702
28211
|
}
|
|
27703
28212
|
}
|
|
27704
28213
|
return await globNative(pattern, base, extraGlob);
|
|
27705
28214
|
}
|
|
28215
|
+
var rgAvailabilityCache2;
|
|
27706
28216
|
function checkRg() {
|
|
27707
|
-
|
|
28217
|
+
rgAvailabilityCache2 ??= new Promise((resolve18) => {
|
|
27708
28218
|
try {
|
|
27709
28219
|
const p = spawn14("rg", ["--version"], {
|
|
27710
28220
|
env: buildChildEnv9(),
|
|
27711
28221
|
stdio: "ignore",
|
|
27712
28222
|
windowsHide: true
|
|
27713
28223
|
});
|
|
27714
|
-
p.on("error", () =>
|
|
27715
|
-
p.on("close", (code) =>
|
|
28224
|
+
p.on("error", () => resolve18(false));
|
|
28225
|
+
p.on("close", (code) => resolve18(code === 0));
|
|
27716
28226
|
} catch {
|
|
27717
|
-
|
|
28227
|
+
resolve18(false);
|
|
27718
28228
|
}
|
|
27719
28229
|
});
|
|
28230
|
+
return rgAvailabilityCache2;
|
|
27720
28231
|
}
|
|
27721
28232
|
function spawnRgFind(pattern, base) {
|
|
27722
28233
|
const args = ["--files", "--glob", pattern, base];
|
|
@@ -27739,10 +28250,10 @@ function spawnRgFind(pattern, base) {
|
|
|
27739
28250
|
}
|
|
27740
28251
|
});
|
|
27741
28252
|
return {
|
|
27742
|
-
promise: new Promise((
|
|
28253
|
+
promise: new Promise((resolve18, reject) => {
|
|
27743
28254
|
child.on("error", reject);
|
|
27744
28255
|
child.on("close", () => {
|
|
27745
|
-
|
|
28256
|
+
resolve18(buf.split("\n").filter(Boolean));
|
|
27746
28257
|
});
|
|
27747
28258
|
})
|
|
27748
28259
|
};
|
|
@@ -27759,10 +28270,10 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
27759
28270
|
}
|
|
27760
28271
|
for (const e of entries) {
|
|
27761
28272
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
27762
|
-
const full =
|
|
28273
|
+
const full = path35.join(dir, e.name);
|
|
27763
28274
|
try {
|
|
27764
|
-
const
|
|
27765
|
-
if (
|
|
28275
|
+
const stat20 = await fs30.lstat(full);
|
|
28276
|
+
if (stat20.isSymbolicLink()) continue;
|
|
27766
28277
|
} catch {
|
|
27767
28278
|
continue;
|
|
27768
28279
|
}
|
|
@@ -27786,7 +28297,7 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
27786
28297
|
// src/scaffold.ts
|
|
27787
28298
|
init_util();
|
|
27788
28299
|
import * as fs31 from "node:fs/promises";
|
|
27789
|
-
import * as
|
|
28300
|
+
import * as path36 from "node:path";
|
|
27790
28301
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
|
|
27791
28302
|
var BUILT_IN_TEMPLATES = {
|
|
27792
28303
|
"npm-package": {
|
|
@@ -27937,16 +28448,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
27937
28448
|
let filesCreated = 0;
|
|
27938
28449
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
27939
28450
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
27940
|
-
const joinedPath =
|
|
27941
|
-
const root =
|
|
27942
|
-
const target =
|
|
27943
|
-
const rel =
|
|
27944
|
-
if (rel.startsWith("..") ||
|
|
28451
|
+
const joinedPath = path36.join(cwd, resolvedPath);
|
|
28452
|
+
const root = path36.resolve(ctx.projectRoot);
|
|
28453
|
+
const target = path36.resolve(joinedPath);
|
|
28454
|
+
const rel = path36.relative(root, target);
|
|
28455
|
+
if (rel.startsWith("..") || path36.isAbsolute(rel)) {
|
|
27945
28456
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
27946
28457
|
}
|
|
27947
28458
|
const fullPath = target;
|
|
27948
28459
|
if (!dryRun) {
|
|
27949
|
-
await fs31.mkdir(
|
|
28460
|
+
await fs31.mkdir(path36.dirname(fullPath), { recursive: true });
|
|
27950
28461
|
await atomicWrite4(fullPath, substituteVars(content, name, vars));
|
|
27951
28462
|
}
|
|
27952
28463
|
files.push(resolvedPath);
|
|
@@ -27977,11 +28488,12 @@ function substituteVars(content, name, vars) {
|
|
|
27977
28488
|
}
|
|
27978
28489
|
|
|
27979
28490
|
// src/search.ts
|
|
27980
|
-
import { FetchError as FetchError3, ToolValidationError as
|
|
28491
|
+
import { FetchError as FetchError3, ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
|
|
27981
28492
|
import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
|
|
27982
|
-
import { toErrorMessage as
|
|
28493
|
+
import { toErrorMessage as toErrorMessage9 } from "@wrongstack/core/utils";
|
|
27983
28494
|
var DEFAULT_NUM = 10;
|
|
27984
28495
|
var MAX_RESULTS = 50;
|
|
28496
|
+
var MAX_SNIPPET_CHARS = 300;
|
|
27985
28497
|
var TIMEOUT_MS3 = 15e3;
|
|
27986
28498
|
var CACHE_TTL_MS = 3e5;
|
|
27987
28499
|
var CACHE_MAX_ENTRIES = 200;
|
|
@@ -27989,7 +28501,7 @@ var cache = /* @__PURE__ */ new Map();
|
|
|
27989
28501
|
var searchTool = {
|
|
27990
28502
|
name: "search",
|
|
27991
28503
|
category: "Search",
|
|
27992
|
-
description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL.",
|
|
28504
|
+
description: "Perform a web search and return results with title, URL, and snippet. Use this when you need up-to-date external information that is not in the local codebase. Results are cached (5 min TTL) and deduplicated by URL. google and bing are best-effort HTML scrapes that fall back to duckduckgo when they return nothing usable.",
|
|
27993
28505
|
usageHint: "Good for: API documentation, error messages, library usage examples, current best practices.\n\n- Prefer specific queries over very broad ones.\n- Results go through the guarded fetch system (same protections as the `fetch` tool).\n- Supports duckduckgo (default), google, and bing sources.\n- Set `skip_cache: true` to force a fresh search.\n- This is often better than the model trying to recall outdated knowledge.",
|
|
27994
28506
|
permission: "auto",
|
|
27995
28507
|
mutating: false,
|
|
@@ -28030,7 +28542,7 @@ var searchTool = {
|
|
|
28030
28542
|
},
|
|
28031
28543
|
async *executeStream(input, _ctx, opts) {
|
|
28032
28544
|
if (!input?.query || input.query.trim() === "") {
|
|
28033
|
-
throw new
|
|
28545
|
+
throw new ToolValidationError10({
|
|
28034
28546
|
message: "search: query is required and must be a non-empty string",
|
|
28035
28547
|
field: "query"
|
|
28036
28548
|
});
|
|
@@ -28063,7 +28575,7 @@ var searchTool = {
|
|
|
28063
28575
|
query: input.query,
|
|
28064
28576
|
results: results.slice(0, num),
|
|
28065
28577
|
source: entry.source,
|
|
28066
|
-
truncated: results.length
|
|
28578
|
+
truncated: results.length > num,
|
|
28067
28579
|
cached: true
|
|
28068
28580
|
}
|
|
28069
28581
|
};
|
|
@@ -28075,41 +28587,45 @@ var searchTool = {
|
|
|
28075
28587
|
text: `Querying ${source} for "${input.query}"\u2026`,
|
|
28076
28588
|
data: { source, query: input.query, cached: false }
|
|
28077
28589
|
};
|
|
28078
|
-
let
|
|
28590
|
+
let engine;
|
|
28079
28591
|
let effectiveSource = source;
|
|
28080
28592
|
switch (source) {
|
|
28081
28593
|
case "duckduckgo":
|
|
28082
|
-
|
|
28594
|
+
engine = await duckduckgoSearch(input.query, opts.signal);
|
|
28083
28595
|
break;
|
|
28084
28596
|
case "google":
|
|
28085
|
-
|
|
28597
|
+
engine = await googleSearch(input.query, opts.signal);
|
|
28086
28598
|
break;
|
|
28087
28599
|
case "bing":
|
|
28088
|
-
|
|
28600
|
+
engine = await bingSearch(input.query, opts.signal);
|
|
28089
28601
|
break;
|
|
28090
28602
|
default:
|
|
28091
|
-
throw new
|
|
28603
|
+
throw new ToolValidationError10({
|
|
28092
28604
|
message: `search: unknown source "${source}"`,
|
|
28093
28605
|
field: "source"
|
|
28094
28606
|
});
|
|
28095
28607
|
}
|
|
28096
|
-
let ranked = rankSearchResults(
|
|
28608
|
+
let ranked = rankSearchResults(engine.results, input.query);
|
|
28609
|
+
let engineError = engine.error;
|
|
28097
28610
|
if (source !== "duckduckgo" && shouldFallbackToDuckDuckGo(ranked, input.query)) {
|
|
28098
28611
|
yield {
|
|
28099
28612
|
type: "log",
|
|
28100
28613
|
text: `${source} returned no relevant static results; falling back to duckduckgo`,
|
|
28101
28614
|
data: { source, fallback: "duckduckgo", query: input.query }
|
|
28102
28615
|
};
|
|
28103
|
-
|
|
28104
|
-
ranked = rankSearchResults(
|
|
28616
|
+
const fallback = await duckduckgoSearch(input.query, opts.signal);
|
|
28617
|
+
ranked = rankSearchResults(fallback.results, input.query);
|
|
28618
|
+
engineError = fallback.error;
|
|
28105
28619
|
effectiveSource = "duckduckgo";
|
|
28106
28620
|
}
|
|
28107
28621
|
const finalResults = ranked.slice(0, num);
|
|
28108
|
-
|
|
28109
|
-
|
|
28622
|
+
if (!engineError) {
|
|
28623
|
+
cache.set(cacheKey, { results: ranked, source: effectiveSource, timestamp: Date.now() });
|
|
28624
|
+
pruneCacheEntries();
|
|
28625
|
+
}
|
|
28110
28626
|
yield {
|
|
28111
28627
|
type: "partial_output",
|
|
28112
|
-
text: `${finalResults.length} results from ${effectiveSource}`,
|
|
28628
|
+
text: engineError ? `search failed: ${engineError}` : `${finalResults.length} results from ${effectiveSource}`,
|
|
28113
28629
|
data: { count: finalResults.length, cached: false, source: effectiveSource }
|
|
28114
28630
|
};
|
|
28115
28631
|
yield {
|
|
@@ -28122,8 +28638,9 @@ var searchTool = {
|
|
|
28122
28638
|
snippet: r.snippet
|
|
28123
28639
|
})),
|
|
28124
28640
|
source: effectiveSource,
|
|
28125
|
-
truncated:
|
|
28126
|
-
cached: false
|
|
28641
|
+
truncated: ranked.length > num,
|
|
28642
|
+
cached: false,
|
|
28643
|
+
...engineError ? { error: engineError } : {}
|
|
28127
28644
|
}
|
|
28128
28645
|
};
|
|
28129
28646
|
}
|
|
@@ -28174,18 +28691,18 @@ function shouldFallbackToDuckDuckGo(results, query) {
|
|
|
28174
28691
|
return terms.some((term) => haystack.includes(term));
|
|
28175
28692
|
});
|
|
28176
28693
|
}
|
|
28177
|
-
async function duckduckgoSearch(query,
|
|
28694
|
+
async function duckduckgoSearch(query, signal) {
|
|
28178
28695
|
const encoded = encodeURIComponent(query);
|
|
28179
28696
|
const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
|
|
28180
28697
|
try {
|
|
28181
28698
|
const response = await fetchWithTimeout(url, signal, TIMEOUT_MS3);
|
|
28182
28699
|
const html = await response.text();
|
|
28183
|
-
return parseDuckDuckGo(html,
|
|
28700
|
+
return { results: parseDuckDuckGo(html, MAX_RESULTS) };
|
|
28184
28701
|
} catch (err) {
|
|
28185
28702
|
console.log(
|
|
28186
|
-
JSON.stringify({ level: "debug", event: "search_failed", query, error:
|
|
28703
|
+
JSON.stringify({ level: "debug", event: "search_failed", query, error: toErrorMessage9(err) })
|
|
28187
28704
|
);
|
|
28188
|
-
return
|
|
28705
|
+
return { results: [], error: `duckduckgo unreachable: ${toErrorMessage9(err)}` };
|
|
28189
28706
|
}
|
|
28190
28707
|
}
|
|
28191
28708
|
function takeFrom(iter, max) {
|
|
@@ -28219,7 +28736,7 @@ function parseDuckDuckGo(html, num) {
|
|
|
28219
28736
|
results.push({
|
|
28220
28737
|
title: entry.title ?? "",
|
|
28221
28738
|
url: entry.url ?? "",
|
|
28222
|
-
snippet: snippetMatches[i] ?? "",
|
|
28739
|
+
snippet: capSnippet(snippetMatches[i] ?? ""),
|
|
28223
28740
|
score: 1
|
|
28224
28741
|
});
|
|
28225
28742
|
}
|
|
@@ -28244,11 +28761,15 @@ function normalizeDuckDuckGoUrl(raw) {
|
|
|
28244
28761
|
return raw;
|
|
28245
28762
|
}
|
|
28246
28763
|
}
|
|
28247
|
-
async function googleSearch(query,
|
|
28764
|
+
async function googleSearch(query, signal) {
|
|
28248
28765
|
const encoded = encodeURIComponent(query);
|
|
28249
28766
|
const url = `https://www.google.com/search?q=${encoded}&hl=en`;
|
|
28250
|
-
|
|
28251
|
-
|
|
28767
|
+
try {
|
|
28768
|
+
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
|
|
28769
|
+
return { results: parseGoogleResults(html, MAX_RESULTS) };
|
|
28770
|
+
} catch (err) {
|
|
28771
|
+
return { results: [], error: `google unreachable: ${toErrorMessage9(err)}` };
|
|
28772
|
+
}
|
|
28252
28773
|
}
|
|
28253
28774
|
function parseGoogleResults(html, num) {
|
|
28254
28775
|
const results = [];
|
|
@@ -28271,17 +28792,21 @@ function parseGoogleResults(html, num) {
|
|
|
28271
28792
|
results.push({
|
|
28272
28793
|
title: titles[i] ?? "",
|
|
28273
28794
|
url: urls[i] ?? "",
|
|
28274
|
-
snippet: snippets[i] ?? "",
|
|
28795
|
+
snippet: capSnippet(snippets[i] ?? ""),
|
|
28275
28796
|
score: 1
|
|
28276
28797
|
});
|
|
28277
28798
|
}
|
|
28278
28799
|
return results;
|
|
28279
28800
|
}
|
|
28280
|
-
async function bingSearch(query,
|
|
28801
|
+
async function bingSearch(query, signal) {
|
|
28281
28802
|
const encoded = encodeURIComponent(query);
|
|
28282
28803
|
const url = `https://www.bing.com/search?q=${encoded}`;
|
|
28283
|
-
|
|
28284
|
-
|
|
28804
|
+
try {
|
|
28805
|
+
const html = await fetchWithTimeout(url, signal, TIMEOUT_MS3).then((r) => r.text());
|
|
28806
|
+
return { results: parseBingResults(html, MAX_RESULTS) };
|
|
28807
|
+
} catch (err) {
|
|
28808
|
+
return { results: [], error: `bing unreachable: ${toErrorMessage9(err)}` };
|
|
28809
|
+
}
|
|
28285
28810
|
}
|
|
28286
28811
|
function parseBingResults(html, num) {
|
|
28287
28812
|
const results = [];
|
|
@@ -28294,7 +28819,7 @@ function parseBingResults(html, num) {
|
|
|
28294
28819
|
const title = stripTags(expectDefined9(titleMatch[2]));
|
|
28295
28820
|
if (!href || !title) return [];
|
|
28296
28821
|
const snippetMatch = /<p\b[^>]*class=(["'])[^"']*\b(?:b_paractl|b_lineclamp\d*)\b[^"']*\1[^>]*>([\s\S]*?)<\/p>/i.exec(block) ?? /<p\b[^>]*>([\s\S]*?)<\/p>/i.exec(block);
|
|
28297
|
-
const snippet = snippetMatch ? stripTags(expectDefined9(snippetMatch.at(-1))) : "";
|
|
28822
|
+
const snippet = snippetMatch ? capSnippet(stripTags(expectDefined9(snippetMatch.at(-1)))) : "";
|
|
28298
28823
|
return [{ url: normalizeBingUrl(href), title, snippet, score: 1 }];
|
|
28299
28824
|
}), num);
|
|
28300
28825
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -28361,17 +28886,20 @@ function anySignal(...signals) {
|
|
|
28361
28886
|
function stripTags(html) {
|
|
28362
28887
|
return decodeHtmlEntities(html.replace(/<[^>]+>/g, "")).trim();
|
|
28363
28888
|
}
|
|
28889
|
+
function capSnippet(snippet) {
|
|
28890
|
+
return snippet.length > MAX_SNIPPET_CHARS ? `${snippet.slice(0, MAX_SNIPPET_CHARS - 1)}\u2026` : snippet;
|
|
28891
|
+
}
|
|
28364
28892
|
function decodeHtmlEntities(text) {
|
|
28365
28893
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
28366
28894
|
}
|
|
28367
28895
|
|
|
28368
28896
|
// src/set-working-dir.ts
|
|
28369
28897
|
import * as fs32 from "node:fs/promises";
|
|
28370
|
-
import { toErrorMessage as
|
|
28898
|
+
import { toErrorMessage as toErrorMessage10 } from "@wrongstack/core/utils";
|
|
28371
28899
|
var setWorkingDirTool = {
|
|
28372
28900
|
name: "set_working_dir",
|
|
28373
28901
|
category: "Context",
|
|
28374
|
-
description: "Change the current working directory for
|
|
28902
|
+
description: "Change the current working directory for subsequent file operations and shell tools (`bash`/`exec` spawn in this directory unless given an explicit cwd). The new directory must be inside the project root. Use this to navigate between subdirectories when working on files in different parts of the project.",
|
|
28375
28903
|
usageHint: "Change the working directory so relative paths in subsequent tool calls resolve from a different directory. Pass `path` to set a new directory, or omit to query the current one. The directory must exist and be inside the project root.",
|
|
28376
28904
|
permission: "confirm",
|
|
28377
28905
|
mutating: true,
|
|
@@ -28401,19 +28929,23 @@ var setWorkingDirTool = {
|
|
|
28401
28929
|
} catch (err) {
|
|
28402
28930
|
return {
|
|
28403
28931
|
current: ctx.workingDir,
|
|
28404
|
-
error:
|
|
28932
|
+
error: toErrorMessage10(err)
|
|
28405
28933
|
};
|
|
28406
28934
|
}
|
|
28935
|
+
let isDirectory = false;
|
|
28407
28936
|
try {
|
|
28408
|
-
await fs32.
|
|
28937
|
+
isDirectory = (await fs32.stat(resolved)).isDirectory();
|
|
28409
28938
|
} catch {
|
|
28939
|
+
isDirectory = false;
|
|
28940
|
+
}
|
|
28941
|
+
if (!isDirectory) {
|
|
28410
28942
|
try {
|
|
28411
28943
|
ctx.setWorkingDir(previous);
|
|
28412
28944
|
} catch {
|
|
28413
28945
|
}
|
|
28414
28946
|
return {
|
|
28415
28947
|
current: ctx.workingDir,
|
|
28416
|
-
error: `Directory does not exist: ${resolved}`
|
|
28948
|
+
error: `Directory does not exist (or is not a directory): ${resolved}`
|
|
28417
28949
|
};
|
|
28418
28950
|
}
|
|
28419
28951
|
return {
|
|
@@ -28868,6 +29400,7 @@ var taskTool = {
|
|
|
28868
29400
|
inProgress: 0
|
|
28869
29401
|
};
|
|
28870
29402
|
}
|
|
29403
|
+
ctx.meta["task.path.resolved"] = taskPath;
|
|
28871
29404
|
if (todosToReplace) {
|
|
28872
29405
|
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
28873
29406
|
signal: AbortSignal.timeout(3e4)
|
|
@@ -28892,6 +29425,7 @@ var taskTool = {
|
|
|
28892
29425
|
formatted = formatPlan2(updated);
|
|
28893
29426
|
return updated;
|
|
28894
29427
|
});
|
|
29428
|
+
ctx.meta["plan.path.resolved"] = planPath;
|
|
28895
29429
|
} catch (err) {
|
|
28896
29430
|
return {
|
|
28897
29431
|
ok: false,
|
|
@@ -28935,7 +29469,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
|
|
|
28935
29469
|
init_spawn_stream();
|
|
28936
29470
|
init_util();
|
|
28937
29471
|
init_legacy_bridge();
|
|
28938
|
-
import * as
|
|
29472
|
+
import * as path37 from "node:path";
|
|
28939
29473
|
var testTool = {
|
|
28940
29474
|
name: "test",
|
|
28941
29475
|
category: "Code Quality",
|
|
@@ -29038,11 +29572,11 @@ var testTool = {
|
|
|
29038
29572
|
}
|
|
29039
29573
|
};
|
|
29040
29574
|
async function detectRunner(cwd) {
|
|
29041
|
-
const { stat:
|
|
29575
|
+
const { stat: stat20 } = await import("node:fs/promises");
|
|
29042
29576
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
29043
29577
|
for (const f of candidates) {
|
|
29044
29578
|
try {
|
|
29045
|
-
await
|
|
29579
|
+
await stat20(path37.join(cwd, f));
|
|
29046
29580
|
if (f.includes("vitest")) return "vitest";
|
|
29047
29581
|
if (f.includes("jest")) return "jest";
|
|
29048
29582
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -29420,7 +29954,7 @@ var toolUseTool = {
|
|
|
29420
29954
|
// src/tree.ts
|
|
29421
29955
|
init_util();
|
|
29422
29956
|
import * as fs33 from "node:fs/promises";
|
|
29423
|
-
import * as
|
|
29957
|
+
import * as path38 from "node:path";
|
|
29424
29958
|
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
29425
29959
|
var DEFAULT_IGNORE5 = /* @__PURE__ */ new Set([
|
|
29426
29960
|
...DEFAULT_WALK_IGNORE_DIRS4,
|
|
@@ -29440,6 +29974,7 @@ var treeTool = {
|
|
|
29440
29974
|
mutating: false,
|
|
29441
29975
|
capabilities: ["fs.read"],
|
|
29442
29976
|
icon: "tree",
|
|
29977
|
+
maxOutputBytes: 262144,
|
|
29443
29978
|
timeoutMs: 15e3,
|
|
29444
29979
|
inputSchema: {
|
|
29445
29980
|
type: "object",
|
|
@@ -29588,17 +30123,15 @@ async function walkDir(dir, depth, opts) {
|
|
|
29588
30123
|
if (opts.exclude.has(e.name)) return false;
|
|
29589
30124
|
return true;
|
|
29590
30125
|
});
|
|
29591
|
-
|
|
29592
|
-
|
|
29593
|
-
|
|
29594
|
-
|
|
29595
|
-
|
|
29596
|
-
|
|
29597
|
-
|
|
29598
|
-
|
|
29599
|
-
|
|
29600
|
-
opts.onProgress?.();
|
|
29601
|
-
}
|
|
30126
|
+
let dirCount = 0;
|
|
30127
|
+
let fileCount = 0;
|
|
30128
|
+
for (const e of filtered) {
|
|
30129
|
+
if (e.isDirectory()) dirCount++;
|
|
30130
|
+
else if (e.isFile()) fileCount++;
|
|
30131
|
+
}
|
|
30132
|
+
opts.totalDirs.value += dirCount;
|
|
30133
|
+
opts.totalFiles.value += fileCount;
|
|
30134
|
+
opts.onProgress?.();
|
|
29602
30135
|
const items = filtered.sort((a, b) => {
|
|
29603
30136
|
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
29604
30137
|
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
@@ -29626,7 +30159,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
29626
30159
|
opts.retention.outputBytes += lineBytes;
|
|
29627
30160
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
29628
30161
|
const childPrefix = opts.prefix + connector;
|
|
29629
|
-
await walkDir(
|
|
30162
|
+
await walkDir(path38.join(dir, entry.name), depth + 1, {
|
|
29630
30163
|
...opts,
|
|
29631
30164
|
prefix: childPrefix,
|
|
29632
30165
|
isLast
|
|
@@ -29639,7 +30172,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
29639
30172
|
init_spawn_stream();
|
|
29640
30173
|
init_util();
|
|
29641
30174
|
init_legacy_bridge();
|
|
29642
|
-
import * as
|
|
30175
|
+
import * as path39 from "node:path";
|
|
29643
30176
|
var typecheckTool = {
|
|
29644
30177
|
name: "typecheck",
|
|
29645
30178
|
category: "Code Quality",
|
|
@@ -29661,11 +30194,7 @@ var typecheckTool = {
|
|
|
29661
30194
|
},
|
|
29662
30195
|
all: {
|
|
29663
30196
|
type: "boolean",
|
|
29664
|
-
description: "Type-check all
|
|
29665
|
-
},
|
|
29666
|
-
json: {
|
|
29667
|
-
type: "boolean",
|
|
29668
|
-
description: "Emit JSON output from tsc (default: false)"
|
|
30197
|
+
description: "Type-check all workspace packages (pnpm workspaces run `pnpm -r exec tsc --noEmit`; other setups run a single `tsc --noEmit` at cwd) (default: false)"
|
|
29669
30198
|
}
|
|
29670
30199
|
}
|
|
29671
30200
|
},
|
|
@@ -29701,29 +30230,42 @@ var typecheckTool = {
|
|
|
29701
30230
|
};
|
|
29702
30231
|
return;
|
|
29703
30232
|
}
|
|
29704
|
-
let
|
|
30233
|
+
let cmd;
|
|
30234
|
+
let cmdArgs;
|
|
29705
30235
|
let project;
|
|
29706
30236
|
if (input.all) {
|
|
29707
|
-
args = ["--noEmit"];
|
|
29708
30237
|
project = "workspace";
|
|
30238
|
+
const tscArgs = ["--noEmit"];
|
|
30239
|
+
if (input.strict) tscArgs.push("--strict");
|
|
30240
|
+
const manager = await detectPackageManager(cwd, ctx.projectRoot);
|
|
30241
|
+
if (manager === "pnpm") {
|
|
30242
|
+
cmd = "pnpm";
|
|
30243
|
+
cmdArgs = ["-r", "--no-bail", "exec", "tsc", ...tscArgs];
|
|
30244
|
+
} else {
|
|
30245
|
+
cmd = "npx";
|
|
30246
|
+
cmdArgs = ["tsc", ...tscArgs];
|
|
30247
|
+
}
|
|
29709
30248
|
} else {
|
|
29710
30249
|
const tsconfig = input.project ? safeResolve(input.project, ctx) : await findTsConfig(cwd);
|
|
29711
|
-
|
|
29712
|
-
if (input.strict)
|
|
29713
|
-
if (tsconfig)
|
|
30250
|
+
const tscArgs = ["--noEmit"];
|
|
30251
|
+
if (input.strict) tscArgs.push("--strict");
|
|
30252
|
+
if (tsconfig) tscArgs.push("--project", tsconfig);
|
|
29714
30253
|
project = tsconfig ?? "default";
|
|
30254
|
+
cmd = "npx";
|
|
30255
|
+
cmdArgs = ["tsc", ...tscArgs];
|
|
29715
30256
|
}
|
|
29716
|
-
|
|
29717
|
-
yield { type: "log", text: `tsc ${args.join(" ")}`, data: { project } };
|
|
30257
|
+
yield { type: "log", text: `${cmd} ${cmdArgs.join(" ")}`, data: { project } };
|
|
29718
30258
|
const result = yield* spawnStream({
|
|
29719
|
-
cmd
|
|
29720
|
-
args:
|
|
30259
|
+
cmd,
|
|
30260
|
+
args: cmdArgs,
|
|
29721
30261
|
cwd,
|
|
29722
30262
|
signal: opts.signal,
|
|
29723
30263
|
maxBytes: 2e5
|
|
29724
30264
|
});
|
|
29725
|
-
const
|
|
29726
|
-
|
|
30265
|
+
const combined = `${result.stdout}
|
|
30266
|
+
${result.stderr}`;
|
|
30267
|
+
const errors = [...combined.matchAll(/^.*\berror TS\d+:/gm)].length;
|
|
30268
|
+
const warnings = [...combined.matchAll(/^.*\bwarning TS\d+:/gm)].length;
|
|
29727
30269
|
yield {
|
|
29728
30270
|
type: "final",
|
|
29729
30271
|
output: {
|
|
@@ -29738,12 +30280,12 @@ var typecheckTool = {
|
|
|
29738
30280
|
}
|
|
29739
30281
|
};
|
|
29740
30282
|
async function findTsConfig(cwd) {
|
|
29741
|
-
const { stat:
|
|
30283
|
+
const { stat: stat20 } = await import("node:fs/promises");
|
|
29742
30284
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
29743
30285
|
for (const f of candidates) {
|
|
29744
30286
|
try {
|
|
29745
|
-
const s = await
|
|
29746
|
-
if (s.isFile()) return
|
|
30287
|
+
const s = await stat20(path39.join(cwd, f));
|
|
30288
|
+
if (s.isFile()) return path39.join(cwd, f);
|
|
29747
30289
|
} catch {
|
|
29748
30290
|
}
|
|
29749
30291
|
}
|
|
@@ -29752,21 +30294,32 @@ async function findTsConfig(cwd) {
|
|
|
29752
30294
|
|
|
29753
30295
|
// src/write.ts
|
|
29754
30296
|
import * as fs34 from "node:fs/promises";
|
|
29755
|
-
import { ToolValidationError as
|
|
29756
|
-
import {
|
|
30297
|
+
import { ToolValidationError as ToolValidationError11 } from "@wrongstack/core/types";
|
|
30298
|
+
import {
|
|
30299
|
+
atomicWrite as atomicWrite5,
|
|
30300
|
+
detectNewlineStyle as detectNewlineStyle3,
|
|
30301
|
+
normalizeToLf as normalizeToLf3,
|
|
30302
|
+
toStyle as toStyle3,
|
|
30303
|
+
unifiedDiff as unifiedDiff3
|
|
30304
|
+
} from "@wrongstack/core/utils";
|
|
29757
30305
|
init_util();
|
|
30306
|
+
var MAX_DIFF_BYTES3 = 262144;
|
|
29758
30307
|
var writeTool = {
|
|
29759
30308
|
name: "write",
|
|
29760
30309
|
category: "Filesystem",
|
|
29761
30310
|
description: "Write or completely overwrite a file on disk. This is a high-privilege operation. For modifying existing files, you should almost always prefer the `edit` tool instead, because `edit` is safer and works on the last-read version of the file.",
|
|
29762
|
-
usageHint: "RULES FOR CORRECT USAGE:\n- Use `write` primarily for **new files** or when you want to replace the entire content.\n- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n-
|
|
30311
|
+
usageHint: "RULES FOR CORRECT USAGE:\n- Use `write` primarily for **new files** or when you want to replace the entire content.\n- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n- When overwriting an existing file, the tool reads the current content itself to compute the diff \u2014 but still `read` the file first before large rewrites so you know what you are replacing.\n- When overwriting an existing file, the content is normalized to the dominant line-ending style (CRLF/LF) of the existing file; new files are written verbatim.\n- The path is resolved relative to the project root and protected against escaping the workspace.",
|
|
29763
30312
|
selection: {
|
|
29764
30313
|
doNotUseWhen: "making a precise change to part of an existing file.",
|
|
29765
30314
|
useInstead: ["edit"]
|
|
29766
30315
|
},
|
|
29767
30316
|
permission: "confirm",
|
|
30317
|
+
// WS-046: gives permission decisions something to key on — the file being
|
|
30318
|
+
// written, so trust rules can scope by path.
|
|
30319
|
+
subjectKey: "path",
|
|
29768
30320
|
mutating: true,
|
|
29769
30321
|
timeoutMs: 5e3,
|
|
30322
|
+
maxOutputBytes: 262144,
|
|
29770
30323
|
capabilities: ["fs.write"],
|
|
29771
30324
|
icon: "file",
|
|
29772
30325
|
inputSchema: {
|
|
@@ -29802,13 +30355,13 @@ async function writeFile6(input, ctx, signal) {
|
|
|
29802
30355
|
}
|
|
29803
30356
|
async function prepareWrite(input, ctx) {
|
|
29804
30357
|
if (!input?.path) {
|
|
29805
|
-
throw new
|
|
30358
|
+
throw new ToolValidationError11({
|
|
29806
30359
|
message: "write: path is required",
|
|
29807
30360
|
field: "path"
|
|
29808
30361
|
});
|
|
29809
30362
|
}
|
|
29810
30363
|
if (input.content === void 0) {
|
|
29811
|
-
throw new
|
|
30364
|
+
throw new ToolValidationError11({
|
|
29812
30365
|
message: "write: content is required",
|
|
29813
30366
|
field: "content"
|
|
29814
30367
|
});
|
|
@@ -29817,12 +30370,12 @@ async function prepareWrite(input, ctx) {
|
|
|
29817
30370
|
let existed = false;
|
|
29818
30371
|
let prev = "";
|
|
29819
30372
|
try {
|
|
29820
|
-
const
|
|
29821
|
-
existed =
|
|
30373
|
+
const stat20 = await fs34.stat(absPath);
|
|
30374
|
+
existed = stat20.isFile();
|
|
29822
30375
|
if (existed) {
|
|
29823
30376
|
if (!ctx.hasRead(absPath)) {
|
|
29824
30377
|
prev = await fs34.readFile(absPath, "utf8");
|
|
29825
|
-
ctx.recordRead(absPath,
|
|
30378
|
+
ctx.recordRead(absPath, stat20.mtimeMs, "write", sha256hex(prev));
|
|
29826
30379
|
} else {
|
|
29827
30380
|
prev = await fs34.readFile(absPath, "utf8");
|
|
29828
30381
|
}
|
|
@@ -29835,31 +30388,42 @@ async function prepareWrite(input, ctx) {
|
|
|
29835
30388
|
return { absPath, existed, prev };
|
|
29836
30389
|
}
|
|
29837
30390
|
async function finishWrite(input, ctx, prepared, signal) {
|
|
30391
|
+
const content = prepared.existed ? toStyle3(normalizeToLf3(input.content), detectNewlineStyle3(prepared.prev)) : input.content;
|
|
29838
30392
|
signal?.throwIfAborted();
|
|
29839
|
-
await atomicWrite5(prepared.absPath,
|
|
29840
|
-
const
|
|
29841
|
-
+ (new file, ${
|
|
29842
|
-
const
|
|
29843
|
-
|
|
30393
|
+
await atomicWrite5(prepared.absPath, content);
|
|
30394
|
+
const rawDiff = prepared.existed ? unifiedDiff3(prepared.prev, content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
30395
|
+
+ (new file, ${content.split("\n").length} lines)`;
|
|
30396
|
+
const { text: diff, truncated: diffTruncated } = truncateDiffPayload(rawDiff, MAX_DIFF_BYTES3);
|
|
30397
|
+
const stat20 = await fs34.stat(prepared.absPath);
|
|
30398
|
+
ctx.recordRead(prepared.absPath, stat20.mtimeMs, "write", sha256hex(content));
|
|
29844
30399
|
ctx.session.recordFileChange({
|
|
29845
30400
|
path: prepared.absPath,
|
|
29846
30401
|
action: prepared.existed ? "modified" : "created",
|
|
29847
30402
|
before: prepared.existed ? prepared.prev : null,
|
|
29848
|
-
after:
|
|
30403
|
+
after: content
|
|
29849
30404
|
});
|
|
29850
30405
|
const syntax = await checkSyntax(
|
|
29851
30406
|
prepared.absPath,
|
|
29852
|
-
|
|
30407
|
+
content,
|
|
29853
30408
|
prepared.existed ? prepared.prev : void 0
|
|
29854
30409
|
).catch(() => void 0);
|
|
29855
30410
|
const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
|
|
30411
|
+
const notes = [];
|
|
30412
|
+
if (diffTruncated) {
|
|
30413
|
+
notes.push("Diff truncated to the 256 KiB output budget \u2014 the full write is on disk.");
|
|
30414
|
+
}
|
|
30415
|
+
if (hasSyntaxErrors) {
|
|
30416
|
+
notes.push(
|
|
30417
|
+
syntax.preExisting ? "Syntax check: the file still has parse errors (they pre-date this write) \u2014 see syntax_errors." : `Syntax check: the written content has ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`
|
|
30418
|
+
);
|
|
30419
|
+
}
|
|
29856
30420
|
return {
|
|
29857
30421
|
path: prepared.absPath,
|
|
29858
|
-
bytes_written: Buffer.byteLength(
|
|
30422
|
+
bytes_written: Buffer.byteLength(content, "utf8"),
|
|
29859
30423
|
created: !prepared.existed,
|
|
29860
30424
|
diff,
|
|
29861
30425
|
syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
|
|
29862
|
-
note:
|
|
30426
|
+
note: notes.length > 0 ? notes.join("\n") : void 0
|
|
29863
30427
|
};
|
|
29864
30428
|
}
|
|
29865
30429
|
|
|
@@ -29984,7 +30548,7 @@ init_circuit_breaker();
|
|
|
29984
30548
|
init_languages();
|
|
29985
30549
|
|
|
29986
30550
|
// src/memory.ts
|
|
29987
|
-
import { ToolValidationError as
|
|
30551
|
+
import { ToolValidationError as ToolValidationError12 } from "@wrongstack/core/types";
|
|
29988
30552
|
function rememberTool(memory) {
|
|
29989
30553
|
return {
|
|
29990
30554
|
name: "remember",
|
|
@@ -30028,7 +30592,7 @@ function rememberTool(memory) {
|
|
|
30028
30592
|
},
|
|
30029
30593
|
async execute(input) {
|
|
30030
30594
|
if (!input?.text) {
|
|
30031
|
-
throw new
|
|
30595
|
+
throw new ToolValidationError12({
|
|
30032
30596
|
message: "remember: text is required",
|
|
30033
30597
|
field: "text"
|
|
30034
30598
|
});
|
|
@@ -30047,28 +30611,48 @@ function forgetTool(memory) {
|
|
|
30047
30611
|
return {
|
|
30048
30612
|
name: "forget",
|
|
30049
30613
|
category: "Session",
|
|
30050
|
-
description: "Remove memory entries that contain the given substring (case-insensitive). Use with caution.",
|
|
30051
|
-
usageHint: "This permanently deletes matching memories in the chosen scope.\n- Provide a reasonably specific `query` to avoid deleting unrelated memories.\n- Always double-check before calling with broad queries.\n- Use `remember` + `forget` together to maintain clean long-term memory.",
|
|
30614
|
+
description: "Remove memory entries that contain the given substring (case-insensitive). Use with caution. Pass `dry_run: true` to preview the matching entries (capped at 20) without deleting anything.",
|
|
30615
|
+
usageHint: "This permanently deletes matching memories in the chosen scope.\n- Provide a reasonably specific `query` to avoid deleting unrelated memories.\n- Always double-check before calling with broad queries \u2014 `dry_run: true` previews the matches without deleting.\n- Use `remember` + `forget` together to maintain clean long-term memory.",
|
|
30052
30616
|
permission: "confirm",
|
|
30617
|
+
// WS-046: gives permission decisions something to key on — the substring
|
|
30618
|
+
// being forgotten.
|
|
30619
|
+
subjectKey: "query",
|
|
30053
30620
|
mutating: true,
|
|
30054
30621
|
timeoutMs: 2e3,
|
|
30055
30622
|
capabilities: ["memory.delete"],
|
|
30623
|
+
icon: "settings",
|
|
30056
30624
|
inputSchema: {
|
|
30057
30625
|
type: "object",
|
|
30058
30626
|
properties: {
|
|
30059
30627
|
query: { type: "string" },
|
|
30060
|
-
scope: { type: "string", enum: ["project-agents", "project-memory", "user-memory"] }
|
|
30628
|
+
scope: { type: "string", enum: ["project-agents", "project-memory", "user-memory"] },
|
|
30629
|
+
dry_run: {
|
|
30630
|
+
type: "boolean",
|
|
30631
|
+
description: "When true, return the matched entries (capped at 20) WITHOUT deleting them. Default false."
|
|
30632
|
+
}
|
|
30061
30633
|
},
|
|
30062
30634
|
required: ["query"]
|
|
30063
30635
|
},
|
|
30064
30636
|
async execute(input) {
|
|
30065
30637
|
if (!input?.query) {
|
|
30066
|
-
throw new
|
|
30638
|
+
throw new ToolValidationError12({
|
|
30067
30639
|
message: "forget: query is required",
|
|
30068
30640
|
field: "query"
|
|
30069
30641
|
});
|
|
30070
30642
|
}
|
|
30071
30643
|
const scope = input.scope ?? "project-memory";
|
|
30644
|
+
if (input.dry_run) {
|
|
30645
|
+
const entries = await memory.list(scope);
|
|
30646
|
+
const needle = input.query.toLowerCase();
|
|
30647
|
+
const matching = entries.filter((entry) => entry.text.toLowerCase().includes(needle));
|
|
30648
|
+
return {
|
|
30649
|
+
removed: 0,
|
|
30650
|
+
scope,
|
|
30651
|
+
dryRun: true,
|
|
30652
|
+
matched: matching.length,
|
|
30653
|
+
matches: matching.slice(0, 20).map((entry) => entry.text)
|
|
30654
|
+
};
|
|
30655
|
+
}
|
|
30072
30656
|
const removed = await memory.forget(input.query, scope);
|
|
30073
30657
|
return { removed, scope };
|
|
30074
30658
|
}
|
|
@@ -30105,7 +30689,7 @@ function searchMemoryTool(memory) {
|
|
|
30105
30689
|
},
|
|
30106
30690
|
async execute(input) {
|
|
30107
30691
|
if (!input?.query) {
|
|
30108
|
-
throw new
|
|
30692
|
+
throw new ToolValidationError12({
|
|
30109
30693
|
message: "search_memory: query is required",
|
|
30110
30694
|
field: "query"
|
|
30111
30695
|
});
|
|
@@ -30157,7 +30741,7 @@ function relatedMemoryTool(memory) {
|
|
|
30157
30741
|
},
|
|
30158
30742
|
async execute(input) {
|
|
30159
30743
|
if (!input?.text) {
|
|
30160
|
-
throw new
|
|
30744
|
+
throw new ToolValidationError12({
|
|
30161
30745
|
message: "find_related_memories: text is required",
|
|
30162
30746
|
field: "text"
|
|
30163
30747
|
});
|
|
@@ -30193,6 +30777,9 @@ function createModeTool(modeStore) {
|
|
|
30193
30777
|
description: "Manage agent operating modes. Modes change the agent's behavior, personality, and system prompt for different workflows (e.g. coding, security review, planning).",
|
|
30194
30778
|
usageHint: "POWERFUL BEHAVIOR CONTROL TOOL:\n\n- Use `list` to see available modes.\n- Use `set <modeId>` to switch the agent into a specific role/mode.\n- Use `get` to check current mode.\n- Use `clear` to return to default behavior.\nSwitching modes is very effective for specialized tasks. The mode change affects how the agent reasons and which guidelines it follows.",
|
|
30195
30779
|
permission: "confirm",
|
|
30780
|
+
// WS-046: gives permission decisions something to key on — the mode being
|
|
30781
|
+
// activated. Permission semantics are unchanged.
|
|
30782
|
+
subjectKey: "mode",
|
|
30196
30783
|
mutating: true,
|
|
30197
30784
|
timeoutMs: 5e3,
|
|
30198
30785
|
capabilities: ["session.mode"],
|
|
@@ -30931,14 +31518,14 @@ function createGlobalPsSlashCommand() {
|
|
|
30931
31518
|
|
|
30932
31519
|
// src/skill.ts
|
|
30933
31520
|
import * as fs35 from "node:fs/promises";
|
|
30934
|
-
import * as
|
|
31521
|
+
import * as path40 from "node:path";
|
|
30935
31522
|
import {
|
|
30936
31523
|
missingRequiredRuntimeTools,
|
|
30937
31524
|
missingRuntimeCapabilities,
|
|
30938
31525
|
runtimeToolReferencesFromText
|
|
30939
31526
|
} from "@wrongstack/core/agent-catalog";
|
|
30940
31527
|
import { SKILL_LIMITS, stripFrontmatter } from "@wrongstack/core/skills";
|
|
30941
|
-
import { ToolValidationError as
|
|
31528
|
+
import { ToolValidationError as ToolValidationError13 } from "@wrongstack/core/types";
|
|
30942
31529
|
var MAX_BODY_CHARS = SKILL_LIMITS.MAX_SKILL_BODY_CHARS;
|
|
30943
31530
|
var MAX_RESOURCE_CHARS = SKILL_LIMITS.MAX_RESOURCE_CHARS;
|
|
30944
31531
|
var MAX_LISTED_RESOURCES = SKILL_LIMITS.MAX_LISTED_RESOURCES;
|
|
@@ -30971,11 +31558,11 @@ function makeSkillTool(skillLoader) {
|
|
|
30971
31558
|
async execute(input, ctx) {
|
|
30972
31559
|
const name = input?.name?.trim();
|
|
30973
31560
|
if (!name) {
|
|
30974
|
-
throw new
|
|
31561
|
+
throw new ToolValidationError13({ message: "skill: name is required", field: "name" });
|
|
30975
31562
|
}
|
|
30976
31563
|
const manifest = await skillLoader.find(name);
|
|
30977
31564
|
if (!manifest) {
|
|
30978
|
-
throw new
|
|
31565
|
+
throw new ToolValidationError13({
|
|
30979
31566
|
message: `skill "${name}" not found \u2014 use /skill to list available skills`,
|
|
30980
31567
|
field: "name"
|
|
30981
31568
|
});
|
|
@@ -30987,7 +31574,7 @@ function makeSkillTool(skillLoader) {
|
|
|
30987
31574
|
);
|
|
30988
31575
|
const missingTools = missingRequiredRuntimeTools(manifest.requiredTools, availableToolNames);
|
|
30989
31576
|
if (missingCapabilities.length > 0 || missingTools.length > 0) {
|
|
30990
|
-
throw new
|
|
31577
|
+
throw new ToolValidationError13({
|
|
30991
31578
|
message: `skill "${name}" is unavailable in this runtime; ` + [
|
|
30992
31579
|
missingCapabilities.length > 0 ? `missing capabilities: ${missingCapabilities.join(", ")}` : "",
|
|
30993
31580
|
missingTools.length > 0 ? `missing tools: ${missingTools.join(", ")}` : ""
|
|
@@ -30995,7 +31582,7 @@ function makeSkillTool(skillLoader) {
|
|
|
30995
31582
|
field: "name"
|
|
30996
31583
|
});
|
|
30997
31584
|
}
|
|
30998
|
-
const dir =
|
|
31585
|
+
const dir = path40.dirname(manifest.path);
|
|
30999
31586
|
let loadedResource;
|
|
31000
31587
|
if (input.resource?.trim()) {
|
|
31001
31588
|
loadedResource = await loadResource(dir, input.resource.trim());
|
|
@@ -31005,12 +31592,7 @@ function makeSkillTool(skillLoader) {
|
|
|
31005
31592
|
runtimeToolReferencesFromText(raw),
|
|
31006
31593
|
availableToolNames
|
|
31007
31594
|
);
|
|
31008
|
-
|
|
31009
|
-
throw new ToolValidationError10({
|
|
31010
|
-
message: `skill "${name}" references unregistered tools: ${missingBodyTools.join(", ")}`,
|
|
31011
|
-
field: "name"
|
|
31012
|
-
});
|
|
31013
|
-
}
|
|
31595
|
+
const warning = missingBodyTools.length > 0 ? `Warning: skill "${name}" references tools not registered in this runtime: ${missingBodyTools.join(", ")}. Steps that call them may be unavailable.` : void 0;
|
|
31014
31596
|
const body = stripFrontmatter(raw).trim().slice(0, MAX_BODY_CHARS);
|
|
31015
31597
|
const resources = loadedResource ? [] : await listResources(dir);
|
|
31016
31598
|
try {
|
|
@@ -31027,44 +31609,48 @@ function makeSkillTool(skillLoader) {
|
|
|
31027
31609
|
body,
|
|
31028
31610
|
resources,
|
|
31029
31611
|
dir,
|
|
31030
|
-
loadedResource
|
|
31612
|
+
loadedResource,
|
|
31613
|
+
warning
|
|
31031
31614
|
};
|
|
31032
31615
|
},
|
|
31033
31616
|
serialize(output) {
|
|
31617
|
+
const warningLine = output.warning ? `
|
|
31618
|
+
|
|
31619
|
+
${output.warning}` : "";
|
|
31034
31620
|
if (output.loadedResource) {
|
|
31035
31621
|
const lr = output.loadedResource;
|
|
31036
31622
|
const note = lr.truncated ? ` (truncated to ${lr.content.length} chars of ${lr.bytes} B)` : "";
|
|
31037
31623
|
return `# Resource: ${output.name}/${lr.rel}
|
|
31038
31624
|
(abs path: ${lr.absPath})${note}
|
|
31039
31625
|
|
|
31040
|
-
${lr.content}`;
|
|
31626
|
+
${lr.content}${warningLine}`;
|
|
31041
31627
|
}
|
|
31042
31628
|
const head = `# Skill: ${output.name}
|
|
31043
31629
|
${output.description}
|
|
31044
31630
|
|
|
31045
31631
|
${output.body}`;
|
|
31046
|
-
if (output.resources.length === 0) return head
|
|
31632
|
+
if (output.resources.length === 0) return `${head}${warningLine}`;
|
|
31047
31633
|
const listing = output.resources.map((r) => `- ${r.path} (${r.bytes} B)`).join("\n");
|
|
31048
31634
|
return `${head}
|
|
31049
31635
|
|
|
31050
31636
|
## Bundled resources (load on demand)
|
|
31051
31637
|
Load any with: \`skill({ name: "${output.name}", resource: "<path>" })\`. Run scripts via bash using their abs path under ${output.dir}.
|
|
31052
|
-
${listing}`;
|
|
31638
|
+
${listing}${warningLine}`;
|
|
31053
31639
|
}
|
|
31054
31640
|
};
|
|
31055
31641
|
}
|
|
31056
31642
|
async function loadResource(skillDir, rel) {
|
|
31057
31643
|
const norm = rel.replace(/\\/g, "/");
|
|
31058
|
-
if (
|
|
31059
|
-
throw new
|
|
31644
|
+
if (path40.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
|
|
31645
|
+
throw new ToolValidationError13({
|
|
31060
31646
|
message: `skill: invalid resource path "${rel}"`,
|
|
31061
31647
|
field: "resource"
|
|
31062
31648
|
});
|
|
31063
31649
|
}
|
|
31064
|
-
const absPath =
|
|
31065
|
-
const root =
|
|
31066
|
-
if (absPath !== root && !absPath.startsWith(root +
|
|
31067
|
-
throw new
|
|
31650
|
+
const absPath = path40.resolve(skillDir, rel);
|
|
31651
|
+
const root = path40.resolve(skillDir);
|
|
31652
|
+
if (absPath !== root && !absPath.startsWith(root + path40.sep)) {
|
|
31653
|
+
throw new ToolValidationError13({
|
|
31068
31654
|
message: `skill: resource "${rel}" escapes the skill directory`,
|
|
31069
31655
|
field: "resource"
|
|
31070
31656
|
});
|
|
@@ -31075,13 +31661,13 @@ async function loadResource(skillDir, rel) {
|
|
|
31075
31661
|
realRoot = await fs35.realpath(root);
|
|
31076
31662
|
realPath = await fs35.realpath(absPath);
|
|
31077
31663
|
} catch {
|
|
31078
|
-
throw new
|
|
31664
|
+
throw new ToolValidationError13({
|
|
31079
31665
|
message: `skill: resource "${rel}" not readable`,
|
|
31080
31666
|
field: "resource"
|
|
31081
31667
|
});
|
|
31082
31668
|
}
|
|
31083
|
-
if (realPath !== realRoot && !realPath.startsWith(realRoot +
|
|
31084
|
-
throw new
|
|
31669
|
+
if (realPath !== realRoot && !realPath.startsWith(realRoot + path40.sep)) {
|
|
31670
|
+
throw new ToolValidationError13({
|
|
31085
31671
|
message: `skill: resource "${rel}" resolves outside the skill directory`,
|
|
31086
31672
|
field: "resource"
|
|
31087
31673
|
});
|
|
@@ -31090,7 +31676,7 @@ async function loadResource(skillDir, rel) {
|
|
|
31090
31676
|
try {
|
|
31091
31677
|
buf = await fs35.readFile(realPath);
|
|
31092
31678
|
} catch {
|
|
31093
|
-
throw new
|
|
31679
|
+
throw new ToolValidationError13({
|
|
31094
31680
|
message: `skill: resource "${rel}" not readable`,
|
|
31095
31681
|
field: "resource"
|
|
31096
31682
|
});
|
|
@@ -31123,7 +31709,7 @@ async function walk(root, dir, out) {
|
|
|
31123
31709
|
}
|
|
31124
31710
|
for (const e of entries) {
|
|
31125
31711
|
if (out.length >= MAX_LISTED_RESOURCES) return;
|
|
31126
|
-
const fullPath =
|
|
31712
|
+
const fullPath = path40.join(dir, e.name);
|
|
31127
31713
|
let isDir = e.isDirectory();
|
|
31128
31714
|
if (e.isSymbolicLink()) {
|
|
31129
31715
|
try {
|
|
@@ -31138,9 +31724,9 @@ async function walk(root, dir, out) {
|
|
|
31138
31724
|
} else if (e.isFile()) {
|
|
31139
31725
|
if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
|
|
31140
31726
|
try {
|
|
31141
|
-
const
|
|
31142
|
-
const rel =
|
|
31143
|
-
out.push({ path: rel, bytes:
|
|
31727
|
+
const stat20 = await fs35.stat(fullPath);
|
|
31728
|
+
const rel = path40.relative(root, fullPath).split(path40.sep).join("/");
|
|
31729
|
+
out.push({ path: rel, bytes: stat20.size });
|
|
31144
31730
|
} catch {
|
|
31145
31731
|
}
|
|
31146
31732
|
}
|
|
@@ -31233,9 +31819,15 @@ var TOOL_ICON_MAP = {
|
|
|
31233
31819
|
"codebase-index": "index",
|
|
31234
31820
|
"codebase-search": "index",
|
|
31235
31821
|
"codebase-stats": "index",
|
|
31822
|
+
"codebase-incoming-calls": "index",
|
|
31823
|
+
"codebase-outgoing-calls": "index",
|
|
31824
|
+
"dead-code-scan": "index",
|
|
31236
31825
|
codebase_index: "index",
|
|
31237
31826
|
codebase_search: "index",
|
|
31238
31827
|
codebase_stats: "index",
|
|
31828
|
+
codebase_incoming_calls: "index",
|
|
31829
|
+
codebase_outgoing_calls: "index",
|
|
31830
|
+
dead_code_scan: "index",
|
|
31239
31831
|
// Data
|
|
31240
31832
|
json: "json",
|
|
31241
31833
|
parse: "json",
|