@wrongstack/tools 0.305.1 → 0.306.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_shell-pick.d.ts +4 -5
- package/dist/_util.d.ts +22 -5
- package/dist/audit.d.ts +0 -1
- package/dist/audit.js +135 -46
- package/dist/bash.js +61 -37
- package/dist/browser/index.js +29 -9
- package/dist/browser/types.d.ts +7 -1
- package/dist/builtin.js +1294 -726
- package/dist/codebase-index/codebase-search-tool.d.ts +5 -0
- package/dist/codebase-index/index.js +223 -152
- package/dist/codebase-index/project-server.js +10 -11
- package/dist/diff.d.ts +5 -0
- package/dist/diff.js +78 -12
- package/dist/document.js +18 -6
- package/dist/edit.js +69 -16
- package/dist/exec.js +44 -22
- package/dist/fetch.js +13 -1
- package/dist/format.d.ts +4 -2
- package/dist/format.js +81 -31
- package/dist/glob.js +12 -4
- package/dist/grep.d.ts +2 -0
- package/dist/grep.js +15 -4
- package/dist/index.js +1359 -762
- package/dist/install.js +96 -37
- package/dist/kanban-tool-types.d.ts +6 -1
- package/dist/kanban.js +60 -0
- package/dist/languages/index.js +28 -13
- package/dist/lint.js +28 -13
- package/dist/logs.d.ts +0 -1
- package/dist/logs.js +44 -13
- package/dist/memory.d.ts +8 -0
- package/dist/memory.js +23 -3
- package/dist/mode.d.ts +1 -1
- package/dist/mode.js +3 -0
- package/dist/next-steps.d.ts +2 -3
- package/dist/next-steps.js +3 -3
- package/dist/outdated.d.ts +0 -3
- package/dist/outdated.js +89 -48
- package/dist/pack.js +1294 -726
- package/dist/plan.js +91 -3
- package/dist/process-registry.d.ts +8 -2
- package/dist/process-registry.js +28 -13
- package/dist/ps-slash.js +22 -12
- package/dist/read.js +10 -3
- package/dist/replace.d.ts +4 -0
- package/dist/replace.js +104 -7
- package/dist/search.d.ts +6 -0
- package/dist/search.js +47 -26
- package/dist/session-kanban.js +3 -1
- package/dist/skill.d.ts +6 -0
- package/dist/skill.js +9 -10
- package/dist/task.js +81 -2
- package/dist/test.js +28 -13
- package/dist/todo.js +79 -2
- package/dist/tool-icons.js +4 -2
- package/dist/tool-summary.d.ts +1 -1
- package/dist/tool-summary.js +76 -1
- package/dist/tool-tier.js +1294 -726
- package/dist/tree.js +9 -10
- package/dist/typecheck.d.ts +0 -2
- package/dist/typecheck.js +98 -31
- package/dist/write.js +58 -10
- package/package.json +4 -4
package/dist/diff.js
CHANGED
|
@@ -4,8 +4,10 @@ import { statSync } from "node:fs";
|
|
|
4
4
|
import * as fs from "node:fs/promises";
|
|
5
5
|
import * as path2 from "node:path";
|
|
6
6
|
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
7
|
+
import { ToolValidationError } from "@wrongstack/core/types";
|
|
7
8
|
|
|
8
9
|
// src/_util.ts
|
|
10
|
+
import * as fsp from "node:fs/promises";
|
|
9
11
|
import * as path from "node:path";
|
|
10
12
|
import * as Core from "@wrongstack/core/utils";
|
|
11
13
|
function resolvePath(input, ctx) {
|
|
@@ -29,14 +31,48 @@ function ensureInsideRoot(absPath, ctx) {
|
|
|
29
31
|
function safeResolve(input, ctx) {
|
|
30
32
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
31
33
|
}
|
|
34
|
+
async function resolveRealInsideRoot(absPath, ctx) {
|
|
35
|
+
if (ctx.allowOutsideProjectRoot) return absPath;
|
|
36
|
+
const realRoots = await Promise.all(
|
|
37
|
+
allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r)))
|
|
38
|
+
);
|
|
39
|
+
let probe = absPath;
|
|
40
|
+
const pendingTail = [];
|
|
41
|
+
for (; ; ) {
|
|
42
|
+
let real;
|
|
43
|
+
try {
|
|
44
|
+
real = await fsp.realpath(probe);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.code === "ENOENT") {
|
|
47
|
+
const parent = path.dirname(probe);
|
|
48
|
+
if (parent === probe) return absPath;
|
|
49
|
+
pendingTail.unshift(path.basename(probe));
|
|
50
|
+
probe = parent;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
if (isInsideAny(real, realRoots)) {
|
|
56
|
+
return pendingTail.length > 0 ? path.join(real, ...pendingTail) : real;
|
|
57
|
+
}
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function safeResolveReal(input, ctx) {
|
|
64
|
+
const abs = safeResolve(input, ctx);
|
|
65
|
+
return await resolveRealInsideRoot(abs, ctx);
|
|
66
|
+
}
|
|
32
67
|
|
|
33
68
|
// src/diff.ts
|
|
34
69
|
var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
|
|
70
|
+
var MAX_GIT_DIFF_CHARS = 1e5;
|
|
35
71
|
var diffTool = {
|
|
36
72
|
name: "diff",
|
|
37
73
|
category: "Filesystem",
|
|
38
74
|
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.",
|
|
39
|
-
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`
|
|
75
|
+
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).',
|
|
40
76
|
permission: "auto",
|
|
41
77
|
mutating: false,
|
|
42
78
|
maxOutputBytes: 262144,
|
|
@@ -69,11 +105,12 @@ var diffTool = {
|
|
|
69
105
|
mode: {
|
|
70
106
|
type: "string",
|
|
71
107
|
enum: ["unified", "side-by-side", "stat"],
|
|
72
|
-
description: 'Output format. "unified" is default
|
|
108
|
+
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.'
|
|
73
109
|
},
|
|
74
110
|
context: {
|
|
75
111
|
type: "integer",
|
|
76
|
-
|
|
112
|
+
minimum: 0,
|
|
113
|
+
description: "Number of context lines for git unified diffs (default: 3, passed as -U<n>). Ignored by the `files`-only dump path."
|
|
77
114
|
}
|
|
78
115
|
}
|
|
79
116
|
},
|
|
@@ -86,16 +123,31 @@ var diffTool = {
|
|
|
86
123
|
};
|
|
87
124
|
async function gitDiff(input, ctx, signal) {
|
|
88
125
|
if (input.a?.startsWith("-")) {
|
|
89
|
-
throw new
|
|
126
|
+
throw new ToolValidationError({
|
|
127
|
+
message: `diff: unsafe ref "${input.a}" \u2014 refs may not begin with '-' (flag injection)`,
|
|
128
|
+
field: "a"
|
|
129
|
+
});
|
|
90
130
|
}
|
|
91
131
|
if (input.b?.startsWith("-")) {
|
|
92
|
-
throw new
|
|
132
|
+
throw new ToolValidationError({
|
|
133
|
+
message: `diff: unsafe ref "${input.b}" \u2014 refs may not begin with '-' (flag injection)`,
|
|
134
|
+
field: "b"
|
|
135
|
+
});
|
|
93
136
|
}
|
|
137
|
+
const requestedMode = input.mode ?? "unified";
|
|
138
|
+
const statMode = requestedMode === "stat";
|
|
139
|
+
const effectiveMode = statMode ? "stat" : "unified";
|
|
140
|
+
const sideBySideNote = requestedMode === "side-by-side" ? "side-by-side output is not supported; a unified diff was produced instead." : void 0;
|
|
94
141
|
const gitDir = findGitDir(ctx.cwd);
|
|
95
142
|
if (!gitDir) {
|
|
96
|
-
return { diff: "", files: [], truncated: false, mode:
|
|
143
|
+
return { diff: "", files: [], truncated: false, mode: effectiveMode };
|
|
97
144
|
}
|
|
98
145
|
const args = ["diff", "--no-color"];
|
|
146
|
+
if (statMode) args.push("--stat");
|
|
147
|
+
if (!statMode && input.context !== void 0) {
|
|
148
|
+
const contextLines = Math.max(0, Math.floor(input.context));
|
|
149
|
+
if (Number.isFinite(contextLines)) args.push(`-U${contextLines}`);
|
|
150
|
+
}
|
|
99
151
|
if (input.staged) args.push("--staged");
|
|
100
152
|
if (input.a) args.push(input.a);
|
|
101
153
|
if (input.b) args.push(input.b);
|
|
@@ -104,11 +156,22 @@ async function gitDiff(input, ctx, signal) {
|
|
|
104
156
|
args.push("--", ...files.map((f) => f.trim()));
|
|
105
157
|
}
|
|
106
158
|
const result = await runGit(args, gitDir, signal);
|
|
159
|
+
let diff = result.stdout;
|
|
160
|
+
let truncated = false;
|
|
161
|
+
if (diff.length > MAX_GIT_DIFF_CHARS) {
|
|
162
|
+
let clipped = diff.slice(0, MAX_GIT_DIFF_CHARS);
|
|
163
|
+
const nl = clipped.lastIndexOf("\n");
|
|
164
|
+
if (nl > 0) clipped = clipped.slice(0, nl);
|
|
165
|
+
diff = `${clipped}
|
|
166
|
+
\u2026[git diff truncated: ${result.stdout.length - clipped.length} of ${result.stdout.length} characters omitted]`;
|
|
167
|
+
truncated = true;
|
|
168
|
+
}
|
|
107
169
|
return {
|
|
108
|
-
diff
|
|
170
|
+
diff,
|
|
109
171
|
files: [],
|
|
110
|
-
truncated
|
|
111
|
-
mode:
|
|
172
|
+
truncated,
|
|
173
|
+
mode: effectiveMode,
|
|
174
|
+
note: sideBySideNote
|
|
112
175
|
};
|
|
113
176
|
}
|
|
114
177
|
function findGitDir(cwd) {
|
|
@@ -154,13 +217,13 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
154
217
|
diff: "No files specified",
|
|
155
218
|
files: [],
|
|
156
219
|
truncated: false,
|
|
157
|
-
mode:
|
|
220
|
+
mode: "dump"
|
|
158
221
|
};
|
|
159
222
|
}
|
|
160
223
|
const results = [];
|
|
161
224
|
let truncated = false;
|
|
162
225
|
for (const file of files) {
|
|
163
|
-
const absPath =
|
|
226
|
+
const absPath = await safeResolveReal(file, ctx);
|
|
164
227
|
const stat2 = await fs.stat(absPath).catch(() => null);
|
|
165
228
|
if (!stat2?.isFile()) continue;
|
|
166
229
|
if (stat2.size > MAX_FILE_DUMP_BYTES) {
|
|
@@ -178,7 +241,10 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
178
241
|
diff: results.join("\n\n"),
|
|
179
242
|
files,
|
|
180
243
|
truncated,
|
|
181
|
-
mode:
|
|
244
|
+
// Honest mode: this path always produces a line-numbered dump — it never
|
|
245
|
+
// honors `mode`, so it must not echo the requested value back.
|
|
246
|
+
mode: "dump",
|
|
247
|
+
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
|
|
182
248
|
};
|
|
183
249
|
}
|
|
184
250
|
function formatWithLineNumbers(file, lines) {
|
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
|
}
|