@wrongstack/tools 0.265.1 → 0.267.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/builtin.js +136 -16
- package/dist/builtin.js.map +1 -1
- package/dist/edit.d.ts +1 -0
- package/dist/edit.js +12 -7
- package/dist/edit.js.map +1 -1
- package/dist/git.d.ts +7 -0
- package/dist/git.js +19 -2
- package/dist/git.js.map +1 -1
- package/dist/index.js +141 -19
- package/dist/index.js.map +1 -1
- package/dist/outdated.js +5 -2
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +136 -16
- package/dist/pack.js.map +1 -1
- package/dist/read.d.ts +3 -0
- package/dist/read.js +103 -6
- package/dist/read.js.map +1 -1
- package/dist/tool-icons.js +2 -2
- package/dist/tool-icons.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs4 from 'node:fs/promises';
|
|
2
|
+
import { toErrorMessage } from '@wrongstack/core/utils';
|
|
2
3
|
import * as path from 'node:path';
|
|
3
4
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
4
5
|
import * as Core from '@wrongstack/core';
|
|
5
|
-
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
6
|
-
import { toErrorMessage } from '@wrongstack/core/utils';
|
|
6
|
+
import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, assessCommitSafety, recordPackageAction, detectPackageEcosystem, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
7
7
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
8
8
|
import * as os from 'node:os';
|
|
9
9
|
import * as fs7 from 'node:fs';
|
|
@@ -168,6 +168,8 @@ function normalizeCommandOutput(raw, opts = {}) {
|
|
|
168
168
|
text = text.replace(/\n{3,}/g, "\n\n");
|
|
169
169
|
return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);
|
|
170
170
|
}
|
|
171
|
+
|
|
172
|
+
// src/read.ts
|
|
171
173
|
var MAX_BYTES = 5 * 1024 * 1024;
|
|
172
174
|
var readTool = {
|
|
173
175
|
name: "read",
|
|
@@ -194,6 +196,11 @@ var readTool = {
|
|
|
194
196
|
limit: {
|
|
195
197
|
type: "integer",
|
|
196
198
|
description: "Maximum number of lines to return (default is 2000)."
|
|
199
|
+
},
|
|
200
|
+
mode: {
|
|
201
|
+
type: "string",
|
|
202
|
+
enum: ["content", "summary"],
|
|
203
|
+
description: "Return full line-numbered content (default) or a compact file summary with imports/exports/symbols."
|
|
197
204
|
}
|
|
198
205
|
},
|
|
199
206
|
required: ["path"]
|
|
@@ -207,14 +214,27 @@ var readTool = {
|
|
|
207
214
|
} catch (err) {
|
|
208
215
|
const code = err.code;
|
|
209
216
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
210
|
-
throw new Error(
|
|
211
|
-
`read: failed to stat "${input.path}": ${toErrorMessage(err)}`
|
|
212
|
-
);
|
|
217
|
+
throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage(err)}`);
|
|
213
218
|
}
|
|
214
219
|
if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
215
220
|
if (stat11.size > MAX_BYTES) {
|
|
216
221
|
throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES})`);
|
|
217
222
|
}
|
|
223
|
+
const offset = Math.max(1, input.offset ?? 1);
|
|
224
|
+
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
225
|
+
const prior = getReadRangeRecord(ctx, absPath);
|
|
226
|
+
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
227
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat11.mtimeMs, offset, requestedEnd)) {
|
|
228
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
229
|
+
return {
|
|
230
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat11.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
231
|
+
total_lines: prior.totalLines,
|
|
232
|
+
encoding: "utf8",
|
|
233
|
+
truncated: requestedEnd < prior.totalLines,
|
|
234
|
+
cached: true,
|
|
235
|
+
note: "Repeated read suppressed to save tokens."
|
|
236
|
+
};
|
|
237
|
+
}
|
|
218
238
|
const buf = await fs4.readFile(absPath);
|
|
219
239
|
if (isBinaryBuffer(buf)) {
|
|
220
240
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
@@ -222,17 +242,38 @@ var readTool = {
|
|
|
222
242
|
const text = buf.toString("utf8");
|
|
223
243
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
224
244
|
const total = allLines.length;
|
|
225
|
-
|
|
226
|
-
|
|
245
|
+
if (input.mode === "summary") {
|
|
246
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
247
|
+
rememberReadRange(ctx, absPath, stat11.mtimeMs, total, 1, Math.min(total, 200));
|
|
248
|
+
return {
|
|
249
|
+
text: summarizeFile(input.path, stat11.size, allLines),
|
|
250
|
+
total_lines: total,
|
|
251
|
+
encoding: "utf8",
|
|
252
|
+
truncated: total > 200,
|
|
253
|
+
note: "Summary mode returned compact structure instead of full file content."
|
|
254
|
+
};
|
|
255
|
+
}
|
|
227
256
|
if (limit === 0) {
|
|
228
257
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
258
|
+
rememberReadRange(ctx, absPath, stat11.mtimeMs, total, 1, 0);
|
|
229
259
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
230
260
|
}
|
|
261
|
+
if (offset > total) {
|
|
262
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
263
|
+
rememberReadRange(ctx, absPath, stat11.mtimeMs, total, total + 1, total + 1);
|
|
264
|
+
return {
|
|
265
|
+
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
266
|
+
total_lines: total,
|
|
267
|
+
encoding: "utf8",
|
|
268
|
+
truncated: false
|
|
269
|
+
};
|
|
270
|
+
}
|
|
231
271
|
const slice = allLines.slice(offset - 1, offset - 1 + limit);
|
|
232
272
|
const truncated = offset - 1 + slice.length < total;
|
|
233
273
|
const width = String(offset + slice.length - 1).length;
|
|
234
274
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
235
275
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
276
|
+
rememberReadRange(ctx, absPath, stat11.mtimeMs, total, offset, offset + slice.length - 1);
|
|
236
277
|
return {
|
|
237
278
|
text: numbered,
|
|
238
279
|
total_lines: total,
|
|
@@ -241,6 +282,62 @@ var readTool = {
|
|
|
241
282
|
};
|
|
242
283
|
}
|
|
243
284
|
};
|
|
285
|
+
var READ_RANGES_META_KEY = "tools.read.ranges.v1";
|
|
286
|
+
function getReadRanges(ctx) {
|
|
287
|
+
const existing = ctx.meta[READ_RANGES_META_KEY];
|
|
288
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
289
|
+
return existing;
|
|
290
|
+
}
|
|
291
|
+
const next = {};
|
|
292
|
+
ctx.meta[READ_RANGES_META_KEY] = next;
|
|
293
|
+
return next;
|
|
294
|
+
}
|
|
295
|
+
function getReadRangeRecord(ctx, absPath) {
|
|
296
|
+
return getReadRanges(ctx)[absPath];
|
|
297
|
+
}
|
|
298
|
+
function rememberReadRange(ctx, absPath, mtimeMs, totalLines, start, end) {
|
|
299
|
+
if (end < start) return;
|
|
300
|
+
const ranges = getReadRanges(ctx);
|
|
301
|
+
const prior = ranges[absPath];
|
|
302
|
+
const nextRanges = prior && Math.abs(prior.mtimeMs - mtimeMs) <= 1 ? prior.ranges.slice() : [];
|
|
303
|
+
nextRanges.push({ start, end });
|
|
304
|
+
ranges[absPath] = {
|
|
305
|
+
mtimeMs,
|
|
306
|
+
totalLines,
|
|
307
|
+
ranges: mergeRanges(nextRanges)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function coversRange(record, mtimeMs, start, end) {
|
|
311
|
+
if (Math.abs(record.mtimeMs - mtimeMs) > 1) return false;
|
|
312
|
+
return record.ranges.some((range) => range.start <= start && range.end >= end);
|
|
313
|
+
}
|
|
314
|
+
function mergeRanges(ranges) {
|
|
315
|
+
const sorted = ranges.slice().sort((a, b) => a.start - b.start);
|
|
316
|
+
const merged = [];
|
|
317
|
+
for (const range of sorted) {
|
|
318
|
+
const last = merged[merged.length - 1];
|
|
319
|
+
if (!last || range.start > last.end + 1) {
|
|
320
|
+
merged.push({ ...range });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
last.end = Math.max(last.end, range.end);
|
|
324
|
+
}
|
|
325
|
+
return merged;
|
|
326
|
+
}
|
|
327
|
+
function summarizeFile(filePath, bytes, lines) {
|
|
328
|
+
const interesting = lines.map((line, index) => ({ line: line.trim(), number: index + 1 })).filter(
|
|
329
|
+
({ line }) => /^(import\s|export\s|class\s|interface\s|type\s|function\s|const\s+\w+\s*=|let\s+\w+\s*=|var\s+\w+\s*=|def\s+|async\s+function\s)/.test(
|
|
330
|
+
line
|
|
331
|
+
)
|
|
332
|
+
).slice(0, 80).map(({ line, number }) => `${number}: ${line}`);
|
|
333
|
+
return [
|
|
334
|
+
`summary: ${filePath}`,
|
|
335
|
+
`bytes=${bytes}`,
|
|
336
|
+
`total_lines=${lines.length}`,
|
|
337
|
+
interesting.length > 0 ? `symbols/imports:
|
|
338
|
+
${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
339
|
+
].join("\n");
|
|
340
|
+
}
|
|
244
341
|
var writeTool = {
|
|
245
342
|
name: "write",
|
|
246
343
|
category: "Filesystem",
|
|
@@ -309,8 +406,8 @@ var writeTool = {
|
|
|
309
406
|
var editTool = {
|
|
310
407
|
name: "edit",
|
|
311
408
|
category: "Filesystem",
|
|
312
|
-
description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It
|
|
313
|
-
usageHint: "
|
|
409
|
+
description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. Fails safely if the `old_string` appears more than once unless `replace_all` is set.",
|
|
410
|
+
usageHint: "RECOMMENDED WORKFLOW:\n1. Prefer calling `read` on the target file first when planning an edit.\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nIf no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.",
|
|
314
411
|
permission: "confirm",
|
|
315
412
|
mutating: true,
|
|
316
413
|
capabilities: ["fs.write"],
|
|
@@ -339,9 +436,7 @@ var editTool = {
|
|
|
339
436
|
throw err;
|
|
340
437
|
});
|
|
341
438
|
if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
|
|
342
|
-
|
|
343
|
-
throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
|
|
344
|
-
}
|
|
439
|
+
const autoRead = !ctx.hasRead(absPath);
|
|
345
440
|
const original = await fs4.readFile(absPath, "utf8");
|
|
346
441
|
const updated = await fs4.stat(absPath);
|
|
347
442
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
@@ -349,15 +444,21 @@ var editTool = {
|
|
|
349
444
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
350
445
|
throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
|
|
351
446
|
}
|
|
447
|
+
if (autoRead && updated.mtimeMs > stat11.mtimeMs + mtimeTolerance) {
|
|
448
|
+
throw new Error(`edit: file "${input.path}" changed while being auto-read. Retry the edit.`);
|
|
449
|
+
}
|
|
450
|
+
const autoReadNote = autoRead ? `No prior read was recorded for "${input.path}"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.` : void 0;
|
|
352
451
|
const style = detectNewlineStyle(original);
|
|
353
452
|
const fileLf = normalizeToLf(original);
|
|
354
453
|
const oldLf = normalizeToLf(input.old_string);
|
|
355
454
|
const newLf = normalizeToLf(input.new_string);
|
|
356
455
|
if (oldLf === newLf) {
|
|
456
|
+
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs);
|
|
357
457
|
return {
|
|
358
458
|
path: absPath,
|
|
359
459
|
replacements: 0,
|
|
360
|
-
diff: "(no-op: old and new are identical)"
|
|
460
|
+
diff: "(no-op: old and new are identical)",
|
|
461
|
+
note: autoReadNote
|
|
361
462
|
};
|
|
362
463
|
}
|
|
363
464
|
let count = 0;
|
|
@@ -398,7 +499,8 @@ var editTool = {
|
|
|
398
499
|
return {
|
|
399
500
|
path: absPath,
|
|
400
501
|
replacements: input.replace_all ? count : 1,
|
|
401
|
-
diff
|
|
502
|
+
diff,
|
|
503
|
+
note: autoReadNote
|
|
402
504
|
};
|
|
403
505
|
}
|
|
404
506
|
};
|
|
@@ -3167,7 +3269,7 @@ var gitTool = {
|
|
|
3167
3269
|
name: "git",
|
|
3168
3270
|
category: "Git",
|
|
3169
3271
|
description: "Safe wrapper around common git operations. Supports status, log, diff, commit, branch, checkout, stash, push, pull, fetch, reset, worktree, etc. This is the preferred way to interact with git instead of using the raw `bash` or `exec` tools.",
|
|
3170
|
-
usageHint: "ALWAYS prefer this tool over raw shell git commands.\n\nKey fields:\n- `command`: one of the supported subcommands (status, log, diff, commit, etc.)\n- Use `message` only for commit operations.\n- Use `files` array for operations that take paths (status, diff, add, etc.).\n- Non-mutating commands (status, log, diff, branch, fetch) are still permission:confirm for safety.\nNever pass raw git flags through `args` for dangerous operations \u2014 use the structured fields.",
|
|
3272
|
+
usageHint: "ALWAYS prefer this tool over raw shell git commands.\n\nKey fields:\n- `command`: one of the supported subcommands (status, log, diff, commit, etc.)\n- Use `message` only for commit operations.\n- Use `files` array for operations that take paths (status, diff, add, etc.).\n- Non-mutating commands (status, log, diff, branch, fetch) are still permission:confirm for safety.\n- For `commit` in a possibly-shared working tree, pass an explicit `files` list scoped to what YOU changed. A bare commit (no `files`) includes ALL staged changes and may capture another agent's half-done work. Heed the `warning` field on the result.\nNever pass raw git flags through `args` for dangerous operations \u2014 use the structured fields.",
|
|
3171
3273
|
permission: "confirm",
|
|
3172
3274
|
icon: "git",
|
|
3173
3275
|
// Conservative: any of these may mutate. The non-mutating commands
|
|
@@ -3256,6 +3358,22 @@ var gitTool = {
|
|
|
3256
3358
|
};
|
|
3257
3359
|
}
|
|
3258
3360
|
const args = buildArgs(input);
|
|
3361
|
+
let safetyWarning;
|
|
3362
|
+
if (input.command === "commit") {
|
|
3363
|
+
try {
|
|
3364
|
+
const report = await assessCommitSafety({
|
|
3365
|
+
cwd: ctx.cwd,
|
|
3366
|
+
projectRoot: ctx.projectRoot,
|
|
3367
|
+
sessionId: ctx.session?.id,
|
|
3368
|
+
signal: opts.signal
|
|
3369
|
+
});
|
|
3370
|
+
if (report.warning) {
|
|
3371
|
+
const scopeNote = input.files ? "" : "\nNote: this commit has no explicit `files` list, so it will include ALL staged changes. Pass `files` to scope the commit to only what you changed.";
|
|
3372
|
+
safetyWarning = report.warning + scopeNote;
|
|
3373
|
+
}
|
|
3374
|
+
} catch {
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3259
3377
|
let stagedDiff;
|
|
3260
3378
|
if (input.command === "commit" && !input.dry_run) {
|
|
3261
3379
|
try {
|
|
@@ -3269,6 +3387,7 @@ var gitTool = {
|
|
|
3269
3387
|
}
|
|
3270
3388
|
const result = await runGit(args, gitDir, opts.signal);
|
|
3271
3389
|
if (stagedDiff !== void 0) result.diff = stagedDiff;
|
|
3390
|
+
if (safetyWarning !== void 0) result.warning = safetyWarning;
|
|
3272
3391
|
return result;
|
|
3273
3392
|
}
|
|
3274
3393
|
};
|
|
@@ -4839,12 +4958,15 @@ var outdatedTool = {
|
|
|
4839
4958
|
// fixed four sibling tools (mcp_control, shellcheck, shellcheck_scan,
|
|
4840
4959
|
// web_search) but missed this one; applying the same contract here.
|
|
4841
4960
|
mutating: true,
|
|
4842
|
-
// Capability is
|
|
4961
|
+
// Capability is outbound network — the tool only hits the package
|
|
4843
4962
|
// registry over HTTP, never touches the filesystem or runs shell.
|
|
4963
|
+
// Use the canonical `net.outbound` capability (not the non-existent
|
|
4964
|
+
// `network` string) so the subagent allowlist recognises it and
|
|
4965
|
+
// permits read-only registry lookups under a director.
|
|
4844
4966
|
// The H7 invariant test requires this array to be non-empty for
|
|
4845
4967
|
// any mutating:true tool (meta-tools whitelisted). See
|
|
4846
4968
|
// tests/permission-mutating-invariant.test.ts:92.
|
|
4847
|
-
capabilities: ["
|
|
4969
|
+
capabilities: ["net.outbound"],
|
|
4848
4970
|
timeoutMs: 6e4,
|
|
4849
4971
|
inputSchema: {
|
|
4850
4972
|
type: "object",
|
|
@@ -9566,8 +9688,8 @@ var TOOL_ICON_CONFIG = {
|
|
|
9566
9688
|
// emerald
|
|
9567
9689
|
folder: { icon: "folder", color: "#8b5cf6" },
|
|
9568
9690
|
// violet
|
|
9569
|
-
terminal: { icon: "terminal", color: "#
|
|
9570
|
-
//
|
|
9691
|
+
terminal: { icon: "terminal", color: "#fb923c" },
|
|
9692
|
+
// orange
|
|
9571
9693
|
web: { icon: "web", color: "#06b6d4" },
|
|
9572
9694
|
// cyan
|
|
9573
9695
|
git: { icon: "git", color: "#f97316" },
|