@wrongstack/tools 0.305.1 → 0.306.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_shell-pick.d.ts +4 -5
- package/dist/_util.d.ts +22 -5
- package/dist/audit.d.ts +0 -1
- package/dist/audit.js +135 -46
- package/dist/bash.js +61 -37
- package/dist/browser/index.js +29 -9
- package/dist/browser/types.d.ts +7 -1
- package/dist/builtin.js +1279 -726
- package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
- package/dist/codebase-index/index.js +223 -152
- package/dist/diff.d.ts +5 -0
- package/dist/diff.js +78 -12
- package/dist/document.js +18 -6
- package/dist/edit.js +69 -16
- package/dist/exec.js +44 -22
- package/dist/fetch.js +13 -1
- package/dist/format.d.ts +4 -2
- package/dist/format.js +81 -31
- package/dist/glob.js +12 -4
- package/dist/grep.d.ts +2 -0
- package/dist/grep.js +15 -4
- package/dist/index.js +1342 -761
- package/dist/install.js +96 -37
- package/dist/kanban-tool-types.d.ts +6 -1
- package/dist/kanban.js +60 -0
- package/dist/languages/index.js +28 -13
- package/dist/lint.js +28 -13
- package/dist/logs.d.ts +0 -1
- package/dist/logs.js +44 -13
- package/dist/memory.d.ts +8 -0
- package/dist/memory.js +23 -3
- package/dist/mode.d.ts +1 -1
- package/dist/mode.js +3 -0
- package/dist/next-steps.d.ts +2 -3
- package/dist/next-steps.js +3 -3
- package/dist/outdated.d.ts +0 -3
- package/dist/outdated.js +89 -48
- package/dist/pack.js +1279 -726
- package/dist/plan.js +76 -3
- package/dist/process-registry.d.ts +8 -2
- package/dist/process-registry.js +28 -13
- package/dist/ps-slash.js +22 -12
- package/dist/read.js +10 -3
- package/dist/replace.d.ts +4 -0
- package/dist/replace.js +104 -7
- package/dist/search.d.ts +6 -0
- package/dist/search.js +47 -26
- package/dist/skill.d.ts +6 -0
- package/dist/skill.js +9 -10
- package/dist/task.js +66 -2
- package/dist/test.js +28 -13
- package/dist/todo.js +64 -2
- package/dist/tool-icons.js +4 -2
- package/dist/tool-summary.d.ts +1 -1
- package/dist/tool-summary.js +76 -1
- package/dist/tool-tier.js +1279 -726
- package/dist/tree.js +9 -10
- package/dist/typecheck.d.ts +0 -2
- package/dist/typecheck.js +98 -31
- package/dist/write.js +58 -10
- package/package.json +4 -4
package/dist/document.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/document.ts
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path2 from "node:path";
|
|
3
4
|
|
|
4
5
|
// src/_util.ts
|
|
5
6
|
import * as path from "node:path";
|
|
@@ -30,8 +31,8 @@ function safeResolve(input, ctx) {
|
|
|
30
31
|
var documentTool = {
|
|
31
32
|
name: "document",
|
|
32
33
|
category: "Project",
|
|
33
|
-
description: "DEPRECATED \u2014
|
|
34
|
-
usageHint: "Deprecated:
|
|
34
|
+
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.",
|
|
35
|
+
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).",
|
|
35
36
|
permission: "auto",
|
|
36
37
|
mutating: false,
|
|
37
38
|
timeoutMs: 3e4,
|
|
@@ -67,7 +68,11 @@ var documentTool = {
|
|
|
67
68
|
const results = [];
|
|
68
69
|
let filesProcessed = 0;
|
|
69
70
|
let itemsDocumented = 0;
|
|
70
|
-
const fileList = input.files ? await resolveFiles(
|
|
71
|
+
const fileList = input.files ? await resolveFiles(
|
|
72
|
+
Array.isArray(input.files) ? input.files.join(",") : input.files,
|
|
73
|
+
cwd,
|
|
74
|
+
ctx
|
|
75
|
+
) : input.path ? [safeResolve(input.path, ctx)] : [];
|
|
71
76
|
for (const absPath of fileList) {
|
|
72
77
|
try {
|
|
73
78
|
const content = await fs.readFile(absPath, "utf8");
|
|
@@ -100,11 +105,18 @@ var documentTool = {
|
|
|
100
105
|
};
|
|
101
106
|
}
|
|
102
107
|
};
|
|
103
|
-
async function resolveFiles(filesInput, cwd) {
|
|
104
|
-
const files =
|
|
108
|
+
async function resolveFiles(filesInput, cwd, ctx) {
|
|
109
|
+
const files = filesInput.split(",");
|
|
105
110
|
const resolved = [];
|
|
106
111
|
for (const f of files) {
|
|
107
|
-
const
|
|
112
|
+
const entry = f.trim();
|
|
113
|
+
if (!entry) continue;
|
|
114
|
+
let absPath;
|
|
115
|
+
try {
|
|
116
|
+
absPath = ensureInsideRoot(path2.resolve(cwd, entry), ctx);
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
108
120
|
try {
|
|
109
121
|
const stat2 = await fs.stat(absPath);
|
|
110
122
|
if (stat2.isFile()) resolved.push(absPath);
|
package/dist/edit.js
CHANGED
|
@@ -381,8 +381,35 @@ async function safeResolveReal(input, ctx) {
|
|
|
381
381
|
const abs = safeResolve(input, ctx);
|
|
382
382
|
return await resolveRealInsideRoot(abs, ctx);
|
|
383
383
|
}
|
|
384
|
+
function truncateDiffPayload(diff, maxBytes) {
|
|
385
|
+
const total = Buffer.byteLength(diff, "utf8");
|
|
386
|
+
if (total <= maxBytes) return { text: diff, truncated: false };
|
|
387
|
+
const MARKER_RESERVE = 96;
|
|
388
|
+
let head = takeHeadBytes(diff, Math.max(0, maxBytes - MARKER_RESERVE));
|
|
389
|
+
const nl = head.lastIndexOf("\n");
|
|
390
|
+
if (nl > 0) head = head.slice(0, nl);
|
|
391
|
+
const kept = Buffer.byteLength(head, "utf8");
|
|
392
|
+
return {
|
|
393
|
+
text: `${head}
|
|
394
|
+
\u2026[diff truncated: ${total - kept} of ${total} bytes omitted]`,
|
|
395
|
+
truncated: true
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function takeHeadBytes(s, maxBytes) {
|
|
399
|
+
if (maxBytes <= 0) return "";
|
|
400
|
+
if (Buffer.byteLength(s, "utf8") <= maxBytes) return s;
|
|
401
|
+
let lo = 0;
|
|
402
|
+
let hi = s.length;
|
|
403
|
+
while (lo < hi) {
|
|
404
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
405
|
+
if (Buffer.byteLength(s.slice(0, mid), "utf8") <= maxBytes) lo = mid;
|
|
406
|
+
else hi = mid - 1;
|
|
407
|
+
}
|
|
408
|
+
return s.slice(0, lo);
|
|
409
|
+
}
|
|
384
410
|
|
|
385
411
|
// src/edit.ts
|
|
412
|
+
var MAX_DIFF_BYTES = 262144;
|
|
386
413
|
var editTool = {
|
|
387
414
|
name: "edit",
|
|
388
415
|
category: "Filesystem",
|
|
@@ -393,17 +420,33 @@ var editTool = {
|
|
|
393
420
|
useInstead: ["write", "patch"]
|
|
394
421
|
},
|
|
395
422
|
permission: "confirm",
|
|
423
|
+
// WS-046: gives permission decisions something to key on — the file being
|
|
424
|
+
// edited, so trust rules can scope by path.
|
|
425
|
+
subjectKey: "path",
|
|
396
426
|
mutating: true,
|
|
397
427
|
capabilities: ["fs.write"],
|
|
398
428
|
icon: "edit",
|
|
399
429
|
timeoutMs: 5e3,
|
|
430
|
+
maxOutputBytes: 262144,
|
|
400
431
|
inputSchema: {
|
|
401
432
|
type: "object",
|
|
402
433
|
properties: {
|
|
403
|
-
path: {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
434
|
+
path: {
|
|
435
|
+
type: "string",
|
|
436
|
+
description: "Path to the file to edit \u2014 relative to the project root, or absolute inside it."
|
|
437
|
+
},
|
|
438
|
+
old_string: {
|
|
439
|
+
type: "string",
|
|
440
|
+
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."
|
|
441
|
+
},
|
|
442
|
+
new_string: {
|
|
443
|
+
type: "string",
|
|
444
|
+
description: "The exact replacement text (may be empty to delete `old_string`)."
|
|
445
|
+
},
|
|
446
|
+
replace_all: {
|
|
447
|
+
type: "boolean",
|
|
448
|
+
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."
|
|
449
|
+
}
|
|
407
450
|
},
|
|
408
451
|
required: ["path", "old_string", "new_string"]
|
|
409
452
|
},
|
|
@@ -483,6 +526,9 @@ var editTool = {
|
|
|
483
526
|
const oldLf = normalizeToLf(input.old_string);
|
|
484
527
|
const newLf = normalizeToLf(input.new_string);
|
|
485
528
|
if (oldLf === newLf) {
|
|
529
|
+
if (!fileLf.includes(oldLf)) {
|
|
530
|
+
throw noMatchError(input.path, fileLf, oldLf);
|
|
531
|
+
}
|
|
486
532
|
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
|
|
487
533
|
return {
|
|
488
534
|
path: absPath,
|
|
@@ -496,13 +542,7 @@ var editTool = {
|
|
|
496
542
|
const ladder = findLadderMatches(fileLf, oldLf);
|
|
497
543
|
if (!ladder) {
|
|
498
544
|
opts?.signal?.throwIfAborted();
|
|
499
|
-
|
|
500
|
-
throw new ToolValidationError({
|
|
501
|
-
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
|
|
502
|
-
${hint.snippet}
|
|
503
|
-
Compare this against your old_string and retry with the file's actual text.` : ""}`,
|
|
504
|
-
field: "old_string"
|
|
505
|
-
});
|
|
545
|
+
throw noMatchError(input.path, fileLf, oldLf);
|
|
506
546
|
}
|
|
507
547
|
const { tier, matches } = ladder;
|
|
508
548
|
const count = matches.length;
|
|
@@ -564,16 +604,20 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
564
604
|
after: newFile
|
|
565
605
|
});
|
|
566
606
|
opts?.signal?.throwIfAborted();
|
|
567
|
-
const diff =
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
607
|
+
const { text: diff, truncated: diffTruncated } = truncateDiffPayload(
|
|
608
|
+
unifiedDiff(original, newFile, {
|
|
609
|
+
fromFile: input.path,
|
|
610
|
+
toFile: input.path
|
|
611
|
+
}),
|
|
612
|
+
MAX_DIFF_BYTES
|
|
613
|
+
);
|
|
614
|
+
const diffNote = diffTruncated ? "Diff truncated to the 256 KiB output budget \u2014 the full edit is on disk." : void 0;
|
|
571
615
|
const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
|
|
572
616
|
let syntaxNote;
|
|
573
617
|
if (syntax && syntax.errors.length > 0) {
|
|
574
618
|
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.`;
|
|
575
619
|
}
|
|
576
|
-
const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
|
|
620
|
+
const notes = [autoReadNote, tierNote, diffNote, syntaxNote].filter(Boolean);
|
|
577
621
|
return {
|
|
578
622
|
path: absPath,
|
|
579
623
|
replacements: input.replace_all ? count : 1,
|
|
@@ -584,6 +628,15 @@ Compare this against your old_string and retry with the file's actual text.` : "
|
|
|
584
628
|
};
|
|
585
629
|
}
|
|
586
630
|
};
|
|
631
|
+
function noMatchError(inputPath, fileLf, oldLf) {
|
|
632
|
+
const hint = nearestMatchHint(fileLf, oldLf);
|
|
633
|
+
return new ToolValidationError({
|
|
634
|
+
message: `edit: no match for old_string in "${inputPath}".${hint ? ` Nearest match near line ${hint.line}:
|
|
635
|
+
${hint.snippet}
|
|
636
|
+
Compare this against your old_string and retry with the file's actual text.` : ""}`,
|
|
637
|
+
field: "old_string"
|
|
638
|
+
});
|
|
639
|
+
}
|
|
587
640
|
export {
|
|
588
641
|
editTool
|
|
589
642
|
};
|
package/dist/exec.js
CHANGED
|
@@ -829,8 +829,11 @@ var SENSITIVE_FLAG_PATTERNS = [
|
|
|
829
829
|
/--(?: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,
|
|
830
830
|
// -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
|
|
831
831
|
// (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
|
|
832
|
+
// The value must be token-like (>= 8 chars) so ordinary combined flags such
|
|
833
|
+
// as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
|
|
834
|
+
// redacted, not just the first.
|
|
832
835
|
// NOTE: synced with @wrongstack/core observability/redact-command.ts.
|
|
833
|
-
/(?<![-\w])-t(?:[=\s]+)?[^\s,-]
|
|
836
|
+
/(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
|
|
834
837
|
// -p|-password|-a (redis auth) short flags: attached + separated + =value.
|
|
835
838
|
// Same token-start anchor; over-redaction is an accepted tradeoff for a
|
|
836
839
|
// redaction function. Synced with core copy.
|
|
@@ -838,8 +841,9 @@ var SENSITIVE_FLAG_PATTERNS = [
|
|
|
838
841
|
// env var–style secrets: TOKEN=x, API_KEY=y, etc.
|
|
839
842
|
/(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
|
|
840
843
|
// Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
|
|
841
|
-
// when preceded by a flag name (e.g. --github-token=EyJ...).
|
|
842
|
-
|
|
844
|
+
// when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
|
|
845
|
+
// every such flag in the command line is redacted, not just the first.
|
|
846
|
+
/--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
|
|
843
847
|
];
|
|
844
848
|
function redactCommand(cmd) {
|
|
845
849
|
let result = cmd;
|
|
@@ -928,11 +932,15 @@ var ProcessRegistryImpl = class {
|
|
|
928
932
|
return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
|
|
929
933
|
}
|
|
930
934
|
_canSignalProcessGroup(p) {
|
|
931
|
-
return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
|
|
935
|
+
return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
|
|
932
936
|
}
|
|
933
937
|
_killChildDirect(p, signal) {
|
|
934
938
|
try {
|
|
935
|
-
p.child
|
|
939
|
+
if (p.child) {
|
|
940
|
+
p.child.kill(signal);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
|
|
936
944
|
} catch {
|
|
937
945
|
}
|
|
938
946
|
}
|
|
@@ -1130,15 +1138,15 @@ var ProcessRegistryImpl = class {
|
|
|
1130
1138
|
this._pruneStale(pid);
|
|
1131
1139
|
const p = this.processes.get(pid);
|
|
1132
1140
|
if (!p) return false;
|
|
1133
|
-
if (p.killed) return true;
|
|
1141
|
+
if (p.killed && opts.force !== true) return true;
|
|
1134
1142
|
if (p.protected && opts.includeProtected !== true) return false;
|
|
1135
1143
|
if (opts.preserveBackground && p.background) return false;
|
|
1136
1144
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
1137
1145
|
const isWin3 = os.platform() === "win32";
|
|
1138
1146
|
if (isWin3) {
|
|
1139
|
-
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
1147
|
+
const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
|
|
1140
1148
|
const directFallback = () => {
|
|
1141
|
-
if (p.child.exitCode === null) {
|
|
1149
|
+
if (p.child && p.child.exitCode === null) {
|
|
1142
1150
|
try {
|
|
1143
1151
|
p.child.kill("SIGKILL");
|
|
1144
1152
|
} catch {
|
|
@@ -1150,10 +1158,7 @@ var ProcessRegistryImpl = class {
|
|
|
1150
1158
|
onSettled: directFallback
|
|
1151
1159
|
})) {
|
|
1152
1160
|
} else {
|
|
1153
|
-
|
|
1154
|
-
p.child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
1155
|
-
} catch {
|
|
1156
|
-
}
|
|
1161
|
+
this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
|
|
1157
1162
|
}
|
|
1158
1163
|
p.killed = true;
|
|
1159
1164
|
return true;
|
|
@@ -1164,7 +1169,7 @@ var ProcessRegistryImpl = class {
|
|
|
1164
1169
|
} else {
|
|
1165
1170
|
this._killPosix(p, "SIGTERM");
|
|
1166
1171
|
const timer = setTimeout(() => {
|
|
1167
|
-
if (this.processes.has(pid) && !p.child
|
|
1172
|
+
if (this.processes.has(pid) && !p.child?.killed) {
|
|
1168
1173
|
this._killPosix(p, "SIGKILL");
|
|
1169
1174
|
}
|
|
1170
1175
|
}, graceMs);
|
|
@@ -1219,6 +1224,16 @@ var ProcessRegistryImpl = class {
|
|
|
1219
1224
|
* before reusing a PID, but we want to clean up before that becomes a risk.
|
|
1220
1225
|
*/
|
|
1221
1226
|
_isStaleEntry(entry) {
|
|
1227
|
+
if (entry.child === null) {
|
|
1228
|
+
if (Date.now() - entry.startedAt <= 6e4) return false;
|
|
1229
|
+
if (os.platform() === "win32") return false;
|
|
1230
|
+
try {
|
|
1231
|
+
process.kill(entry.pid, 0);
|
|
1232
|
+
return false;
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
return err.code !== "EPERM";
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1222
1237
|
return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
|
|
1223
1238
|
}
|
|
1224
1239
|
/**
|
|
@@ -1512,7 +1527,6 @@ var PersistentProcessRegistry = class {
|
|
|
1512
1527
|
try {
|
|
1513
1528
|
const data = await readRegistryFile(this.registryPath);
|
|
1514
1529
|
data.instances.set(String(entry.pid), entry);
|
|
1515
|
-
const child = null;
|
|
1516
1530
|
this.baseRegistry.register({
|
|
1517
1531
|
pid: entry.pid,
|
|
1518
1532
|
name: entry.name,
|
|
@@ -1520,7 +1534,7 @@ var PersistentProcessRegistry = class {
|
|
|
1520
1534
|
startedAt: entry.startedAt,
|
|
1521
1535
|
sessionId: entry.sessionId,
|
|
1522
1536
|
protected: entry.protected,
|
|
1523
|
-
child
|
|
1537
|
+
child: null
|
|
1524
1538
|
});
|
|
1525
1539
|
await writeRegistryFile(this.registryPath, data);
|
|
1526
1540
|
} finally {
|
|
@@ -2602,6 +2616,7 @@ function getExecAllowlist() {
|
|
|
2602
2616
|
var MAX_ARGS = 20;
|
|
2603
2617
|
var MAX_OUTPUT = 2e5;
|
|
2604
2618
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
2619
|
+
var MAX_TIMEOUT_MS = 6e5;
|
|
2605
2620
|
var BLOCKED_ARG_PATTERNS = {
|
|
2606
2621
|
python: [],
|
|
2607
2622
|
// git --exec=<cmd> runs arbitrary commands via upload-pack/receive-pack;
|
|
@@ -2727,8 +2742,8 @@ var SAFE_DANGER = { level: "safe", reasons: [] };
|
|
|
2727
2742
|
var execTool = {
|
|
2728
2743
|
name: "exec",
|
|
2729
2744
|
category: "Shell",
|
|
2730
|
-
description: "Execute a
|
|
2731
|
-
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).\
|
|
2745
|
+
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.",
|
|
2746
|
+
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.",
|
|
2732
2747
|
selection: {
|
|
2733
2748
|
doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
|
|
2734
2749
|
useInstead: ["bash"]
|
|
@@ -2744,7 +2759,13 @@ var execTool = {
|
|
|
2744
2759
|
subjectKey: "command",
|
|
2745
2760
|
mutating: true,
|
|
2746
2761
|
riskTier: "standard",
|
|
2747
|
-
|
|
2762
|
+
// Executor-level abort ceiling. Must sit ABOVE the per-call timeout ceiling
|
|
2763
|
+
// (MAX_TIMEOUT_MS): the tool's own timer resolves with exit 124 + registry
|
|
2764
|
+
// tree-kill; the executor's AbortSignal.timeout is a blunt abort that would
|
|
2765
|
+
// otherwise fire first and discard the structured timeout result. The 10s
|
|
2766
|
+
// margin covers the kill/teardown window. (The executor additionally clamps
|
|
2767
|
+
// to config `tools.maxToolTimeoutMs`.)
|
|
2768
|
+
timeoutMs: MAX_TIMEOUT_MS + 1e4,
|
|
2748
2769
|
capabilities: ["shell.restricted"],
|
|
2749
2770
|
icon: "terminal",
|
|
2750
2771
|
inputSchema: {
|
|
@@ -2765,7 +2786,7 @@ var execTool = {
|
|
|
2765
2786
|
},
|
|
2766
2787
|
timeout: {
|
|
2767
2788
|
type: "integer",
|
|
2768
|
-
description: "Per-command timeout in milliseconds."
|
|
2789
|
+
description: "Per-command timeout in milliseconds (default 30000, max 600000)."
|
|
2769
2790
|
}
|
|
2770
2791
|
},
|
|
2771
2792
|
required: ["command"]
|
|
@@ -2809,7 +2830,7 @@ var execTool = {
|
|
|
2809
2830
|
};
|
|
2810
2831
|
}
|
|
2811
2832
|
const args = (input.args ?? []).slice(0, MAX_ARGS);
|
|
2812
|
-
const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
2833
|
+
const timeout = Math.max(1, Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS));
|
|
2813
2834
|
const danger = detectDanger(cmd, args, dangerBypass);
|
|
2814
2835
|
const killCheck = await checkExecKillCommand(cmd, args);
|
|
2815
2836
|
if (killCheck.blocked) {
|
|
@@ -2837,15 +2858,16 @@ var execTool = {
|
|
|
2837
2858
|
danger
|
|
2838
2859
|
};
|
|
2839
2860
|
}
|
|
2861
|
+
const defaultCwd = ctx.workingDir ?? ctx.cwd;
|
|
2840
2862
|
let cwd;
|
|
2841
2863
|
try {
|
|
2842
|
-
cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(
|
|
2864
|
+
cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : await safeResolveReal(defaultCwd, ctx);
|
|
2843
2865
|
} catch {
|
|
2844
2866
|
return {
|
|
2845
2867
|
command: cmd,
|
|
2846
2868
|
args,
|
|
2847
2869
|
stdout: "",
|
|
2848
|
-
stderr: `cwd "${input.cwd ??
|
|
2870
|
+
stderr: `cwd "${input.cwd ?? defaultCwd}" resolves outside project root`,
|
|
2849
2871
|
exitCode: 1,
|
|
2850
2872
|
truncated: false,
|
|
2851
2873
|
allowed: false,
|
package/dist/fetch.js
CHANGED
|
@@ -102,6 +102,10 @@ async function guardedFetch(url, maxRedirects, signal, headers = {
|
|
|
102
102
|
if (res.status < 300 || res.status > 399) {
|
|
103
103
|
return res;
|
|
104
104
|
}
|
|
105
|
+
try {
|
|
106
|
+
await res.body?.cancel();
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
105
109
|
redirectCount++;
|
|
106
110
|
if (redirectCount > maxRedirects) {
|
|
107
111
|
throw new FetchError({
|
|
@@ -184,6 +188,8 @@ TD.addRule("stripDangerousElements", {
|
|
|
184
188
|
filter: ["script", "style", "noscript"],
|
|
185
189
|
replacement: () => ""
|
|
186
190
|
});
|
|
191
|
+
var PRUNED_BOILERPLATE_TAGS = /* @__PURE__ */ new Set(["nav", "header", "footer", "aside", "svg", "iframe"]);
|
|
192
|
+
TD.remove((node) => PRUNED_BOILERPLATE_TAGS.has(node.nodeName.toLowerCase()));
|
|
187
193
|
var MAX_BYTES = 131072;
|
|
188
194
|
var TIMEOUT_MS = 2e4;
|
|
189
195
|
var combineSignals = (signals) => AbortSignal.any(signals);
|
|
@@ -213,7 +219,7 @@ var fetchTool = {
|
|
|
213
219
|
format: {
|
|
214
220
|
type: "string",
|
|
215
221
|
enum: ["markdown", "text", "raw"],
|
|
216
|
-
description: 'Output format. "markdown" is recommended for HTML pages.'
|
|
222
|
+
description: 'Output format. "markdown" is recommended for HTML pages; for non-HTML content types it falls back to plain text (JSON is pretty-printed).'
|
|
217
223
|
}
|
|
218
224
|
},
|
|
219
225
|
required: ["url"]
|
|
@@ -248,6 +254,12 @@ var fetchTool = {
|
|
|
248
254
|
});
|
|
249
255
|
}
|
|
250
256
|
const u = new URL(input.url);
|
|
257
|
+
if (u.username || u.password) {
|
|
258
|
+
throw new ToolValidationError2({
|
|
259
|
+
message: "fetch: URLs with embedded credentials (user:pass@host) are not allowed",
|
|
260
|
+
field: "url"
|
|
261
|
+
});
|
|
262
|
+
}
|
|
251
263
|
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
252
264
|
throw new ToolValidationError2({
|
|
253
265
|
message: `fetch: unsupported protocol "${u.protocol}"`,
|
package/dist/format.d.ts
CHANGED
|
@@ -7,8 +7,10 @@ interface FormatInput {
|
|
|
7
7
|
}
|
|
8
8
|
interface FormatOutput {
|
|
9
9
|
fixer: string;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/** Parsed from formatter output when confidently available; undefined otherwise. */
|
|
11
|
+
files_checked: number | undefined;
|
|
12
|
+
/** Parsed from formatter output when confidently available; undefined otherwise. */
|
|
13
|
+
files_changed: number | undefined;
|
|
12
14
|
output: string;
|
|
13
15
|
truncated: boolean;
|
|
14
16
|
}
|
package/dist/format.js
CHANGED
|
@@ -364,8 +364,11 @@ var SENSITIVE_FLAG_PATTERNS = [
|
|
|
364
364
|
/--(?: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,
|
|
365
365
|
// -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
|
|
366
366
|
// (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
|
|
367
|
+
// The value must be token-like (>= 8 chars) so ordinary combined flags such
|
|
368
|
+
// as `tar -tf` / `ssh -tt` are not eaten. Global flag: EVERY occurrence is
|
|
369
|
+
// redacted, not just the first.
|
|
367
370
|
// NOTE: synced with @wrongstack/core observability/redact-command.ts.
|
|
368
|
-
/(?<![-\w])-t(?:[=\s]+)?[^\s,-]
|
|
371
|
+
/(?<![-\w])-t(?:[=\s]+)?[^\s,-]{8,}/g,
|
|
369
372
|
// -p|-password|-a (redis auth) short flags: attached + separated + =value.
|
|
370
373
|
// Same token-start anchor; over-redaction is an accepted tradeoff for a
|
|
371
374
|
// redaction function. Synced with core copy.
|
|
@@ -373,8 +376,9 @@ var SENSITIVE_FLAG_PATTERNS = [
|
|
|
373
376
|
// env var–style secrets: TOKEN=x, API_KEY=y, etc.
|
|
374
377
|
/(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
|
|
375
378
|
// Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
|
|
376
|
-
// when preceded by a flag name (e.g. --github-token=EyJ...).
|
|
377
|
-
|
|
379
|
+
// when preceded by a flag name (e.g. --github-token=EyJ...). Global flag so
|
|
380
|
+
// every such flag in the command line is redacted, not just the first.
|
|
381
|
+
/--\w*(?:token|key|secret|password|passwd|auth|credential)\w*[=\s,][A-Za-z0-9+/=]{32,}/g
|
|
378
382
|
];
|
|
379
383
|
function redactCommand(cmd) {
|
|
380
384
|
let result = cmd;
|
|
@@ -463,11 +467,15 @@ var ProcessRegistryImpl = class {
|
|
|
463
467
|
return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
|
|
464
468
|
}
|
|
465
469
|
_canSignalProcessGroup(p) {
|
|
466
|
-
return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && typeof p.child.pid === "number" && p.child.pid === p.pid;
|
|
470
|
+
return os.platform() !== "win32" && p.processGroupLeader === true && this._isSafeSignalPid(p.pid) && p.child !== null && typeof p.child.pid === "number" && p.child.pid === p.pid;
|
|
467
471
|
}
|
|
468
472
|
_killChildDirect(p, signal) {
|
|
469
473
|
try {
|
|
470
|
-
p.child
|
|
474
|
+
if (p.child) {
|
|
475
|
+
p.child.kill(signal);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (this._isSafeSignalPid(p.pid)) process.kill(p.pid, signal);
|
|
471
479
|
} catch {
|
|
472
480
|
}
|
|
473
481
|
}
|
|
@@ -665,15 +673,15 @@ var ProcessRegistryImpl = class {
|
|
|
665
673
|
this._pruneStale(pid);
|
|
666
674
|
const p = this.processes.get(pid);
|
|
667
675
|
if (!p) return false;
|
|
668
|
-
if (p.killed) return true;
|
|
676
|
+
if (p.killed && opts.force !== true) return true;
|
|
669
677
|
if (p.protected && opts.includeProtected !== true) return false;
|
|
670
678
|
if (opts.preserveBackground && p.background) return false;
|
|
671
679
|
const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
|
|
672
680
|
const isWin2 = os.platform() === "win32";
|
|
673
681
|
if (isWin2) {
|
|
674
|
-
const liveRealChild = p.child.exitCode === null && typeof p.child.pid === "number";
|
|
682
|
+
const liveRealChild = p.child === null || p.child.exitCode === null && typeof p.child.pid === "number";
|
|
675
683
|
const directFallback = () => {
|
|
676
|
-
if (p.child.exitCode === null) {
|
|
684
|
+
if (p.child && p.child.exitCode === null) {
|
|
677
685
|
try {
|
|
678
686
|
p.child.kill("SIGKILL");
|
|
679
687
|
} catch {
|
|
@@ -685,10 +693,7 @@ var ProcessRegistryImpl = class {
|
|
|
685
693
|
onSettled: directFallback
|
|
686
694
|
})) {
|
|
687
695
|
} else {
|
|
688
|
-
|
|
689
|
-
p.child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
690
|
-
} catch {
|
|
691
|
-
}
|
|
696
|
+
this._killChildDirect(p, force ? "SIGKILL" : "SIGTERM");
|
|
692
697
|
}
|
|
693
698
|
p.killed = true;
|
|
694
699
|
return true;
|
|
@@ -699,7 +704,7 @@ var ProcessRegistryImpl = class {
|
|
|
699
704
|
} else {
|
|
700
705
|
this._killPosix(p, "SIGTERM");
|
|
701
706
|
const timer = setTimeout(() => {
|
|
702
|
-
if (this.processes.has(pid) && !p.child
|
|
707
|
+
if (this.processes.has(pid) && !p.child?.killed) {
|
|
703
708
|
this._killPosix(p, "SIGKILL");
|
|
704
709
|
}
|
|
705
710
|
}, graceMs);
|
|
@@ -754,6 +759,16 @@ var ProcessRegistryImpl = class {
|
|
|
754
759
|
* before reusing a PID, but we want to clean up before that becomes a risk.
|
|
755
760
|
*/
|
|
756
761
|
_isStaleEntry(entry) {
|
|
762
|
+
if (entry.child === null) {
|
|
763
|
+
if (Date.now() - entry.startedAt <= 6e4) return false;
|
|
764
|
+
if (os.platform() === "win32") return false;
|
|
765
|
+
try {
|
|
766
|
+
process.kill(entry.pid, 0);
|
|
767
|
+
return false;
|
|
768
|
+
} catch (err) {
|
|
769
|
+
return err.code !== "EPERM";
|
|
770
|
+
}
|
|
771
|
+
}
|
|
757
772
|
return entry.child.exitCode !== null && Date.now() - entry.startedAt > 6e4;
|
|
758
773
|
}
|
|
759
774
|
/**
|
|
@@ -3590,8 +3605,9 @@ var formatTool = {
|
|
|
3590
3605
|
type: "final",
|
|
3591
3606
|
output: {
|
|
3592
3607
|
fixer: bridge.language,
|
|
3593
|
-
|
|
3594
|
-
|
|
3608
|
+
// Language-bridge runs don't report per-file counts.
|
|
3609
|
+
files_checked: void 0,
|
|
3610
|
+
files_changed: void 0,
|
|
3595
3611
|
output: normalizeCommandOutput(run.output || run.error || ""),
|
|
3596
3612
|
truncated: run.truncated
|
|
3597
3613
|
}
|
|
@@ -3618,11 +3634,14 @@ var formatTool = {
|
|
|
3618
3634
|
text: `Running ${detected}\u2026`,
|
|
3619
3635
|
data: { fixer: detected, check: !!input.check }
|
|
3620
3636
|
};
|
|
3621
|
-
const
|
|
3622
|
-
|
|
3623
|
-
if (
|
|
3624
|
-
|
|
3625
|
-
args.push(
|
|
3637
|
+
const fileList = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((f) => f.trim()) : [];
|
|
3638
|
+
let args;
|
|
3639
|
+
if (detected === "prettier") {
|
|
3640
|
+
args = [input.check ? "--check" : "--write"];
|
|
3641
|
+
args.push(...fileList.length > 0 ? fileList : ["."]);
|
|
3642
|
+
} else {
|
|
3643
|
+
args = ["format", input.check ? "--check" : "--write"];
|
|
3644
|
+
if (fileList.length > 0) args.push("--", ...fileList);
|
|
3626
3645
|
}
|
|
3627
3646
|
const result = yield* spawnStream({
|
|
3628
3647
|
cmd: detected,
|
|
@@ -3631,32 +3650,63 @@ var formatTool = {
|
|
|
3631
3650
|
signal: opts.signal,
|
|
3632
3651
|
maxBytes: 1e5
|
|
3633
3652
|
});
|
|
3634
|
-
const
|
|
3653
|
+
const combinedOut = `${result.stdout}
|
|
3654
|
+
${result.stderr}`;
|
|
3655
|
+
const counts = parseFormatterCounts(detected, combinedOut);
|
|
3635
3656
|
yield {
|
|
3636
3657
|
type: "final",
|
|
3637
3658
|
output: {
|
|
3638
3659
|
fixer: detected,
|
|
3639
|
-
files_checked:
|
|
3640
|
-
files_changed: changed,
|
|
3660
|
+
files_checked: counts.checked,
|
|
3661
|
+
files_changed: counts.changed,
|
|
3641
3662
|
output: normalizeCommandOutput(result.stdout || result.stderr || result.error || ""),
|
|
3642
3663
|
truncated: result.truncated
|
|
3643
3664
|
}
|
|
3644
3665
|
};
|
|
3645
3666
|
}
|
|
3646
3667
|
};
|
|
3668
|
+
function parseFormatterCounts(fixer, output) {
|
|
3669
|
+
if (fixer !== "biome") return { checked: void 0, changed: void 0 };
|
|
3670
|
+
const checkedMatch = /\b(?:Checked|Formatted)\s+(\d+)\s+files?\b/i.exec(output);
|
|
3671
|
+
const changedMatch = /\bFixed\s+(\d+)\s+files?\b/i.exec(output);
|
|
3672
|
+
return {
|
|
3673
|
+
checked: checkedMatch?.[1] !== void 0 ? Number(checkedMatch[1]) : void 0,
|
|
3674
|
+
changed: changedMatch?.[1] !== void 0 ? Number(changedMatch[1]) : void 0
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3647
3677
|
async function detectFixer(cwd) {
|
|
3648
|
-
const
|
|
3649
|
-
|
|
3650
|
-
await stat5(`${cwd}/biome.json`);
|
|
3651
|
-
return "biome";
|
|
3652
|
-
} catch {
|
|
3678
|
+
const fs6 = await import("node:fs/promises");
|
|
3679
|
+
const exists = async (file) => {
|
|
3653
3680
|
try {
|
|
3654
|
-
await
|
|
3655
|
-
return
|
|
3681
|
+
await fs6.stat(`${cwd}/${file}`);
|
|
3682
|
+
return true;
|
|
3656
3683
|
} catch {
|
|
3657
|
-
return
|
|
3684
|
+
return false;
|
|
3658
3685
|
}
|
|
3686
|
+
};
|
|
3687
|
+
if (await exists("biome.json") || await exists("biome.jsonc")) return "biome";
|
|
3688
|
+
const PRETTIER_CONFIGS = [
|
|
3689
|
+
".prettierrc",
|
|
3690
|
+
".prettierrc.json",
|
|
3691
|
+
".prettierrc.yml",
|
|
3692
|
+
".prettierrc.yaml",
|
|
3693
|
+
".prettierrc.js",
|
|
3694
|
+
".prettierrc.cjs",
|
|
3695
|
+
".prettierrc.mjs",
|
|
3696
|
+
"prettier.config.js",
|
|
3697
|
+
"prettier.config.cjs",
|
|
3698
|
+
"prettier.config.mjs"
|
|
3699
|
+
];
|
|
3700
|
+
for (const cfg of PRETTIER_CONFIGS) {
|
|
3701
|
+
if (await exists(cfg)) return "prettier";
|
|
3702
|
+
}
|
|
3703
|
+
try {
|
|
3704
|
+
const raw = await fs6.readFile(`${cwd}/package.json`, "utf8");
|
|
3705
|
+
const pkg = JSON.parse(raw);
|
|
3706
|
+
if (pkg["prettier"] !== void 0) return "prettier";
|
|
3707
|
+
} catch {
|
|
3659
3708
|
}
|
|
3709
|
+
return "biome";
|
|
3660
3710
|
}
|
|
3661
3711
|
export {
|
|
3662
3712
|
formatTool
|