@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/builtin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
3
|
+
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
|
|
6
6
|
import * as fs14 from 'node:fs/promises';
|
|
@@ -4571,8 +4571,8 @@ function processFile(content, absPath, _style, _overwrite, target) {
|
|
|
4571
4571
|
var editTool = {
|
|
4572
4572
|
name: "edit",
|
|
4573
4573
|
category: "Filesystem",
|
|
4574
|
-
description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It
|
|
4575
|
-
usageHint: "
|
|
4574
|
+
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.",
|
|
4575
|
+
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.",
|
|
4576
4576
|
permission: "confirm",
|
|
4577
4577
|
mutating: true,
|
|
4578
4578
|
capabilities: ["fs.write"],
|
|
@@ -4601,9 +4601,7 @@ var editTool = {
|
|
|
4601
4601
|
throw err;
|
|
4602
4602
|
});
|
|
4603
4603
|
if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
|
|
4604
|
-
|
|
4605
|
-
throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
|
|
4606
|
-
}
|
|
4604
|
+
const autoRead = !ctx.hasRead(absPath);
|
|
4607
4605
|
const original = await fs14.readFile(absPath, "utf8");
|
|
4608
4606
|
const updated = await fs14.stat(absPath);
|
|
4609
4607
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
@@ -4611,15 +4609,21 @@ var editTool = {
|
|
|
4611
4609
|
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
4612
4610
|
throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
|
|
4613
4611
|
}
|
|
4612
|
+
if (autoRead && updated.mtimeMs > stat11.mtimeMs + mtimeTolerance) {
|
|
4613
|
+
throw new Error(`edit: file "${input.path}" changed while being auto-read. Retry the edit.`);
|
|
4614
|
+
}
|
|
4615
|
+
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;
|
|
4614
4616
|
const style = detectNewlineStyle(original);
|
|
4615
4617
|
const fileLf = normalizeToLf(original);
|
|
4616
4618
|
const oldLf = normalizeToLf(input.old_string);
|
|
4617
4619
|
const newLf = normalizeToLf(input.new_string);
|
|
4618
4620
|
if (oldLf === newLf) {
|
|
4621
|
+
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs);
|
|
4619
4622
|
return {
|
|
4620
4623
|
path: absPath,
|
|
4621
4624
|
replacements: 0,
|
|
4622
|
-
diff: "(no-op: old and new are identical)"
|
|
4625
|
+
diff: "(no-op: old and new are identical)",
|
|
4626
|
+
note: autoReadNote
|
|
4623
4627
|
};
|
|
4624
4628
|
}
|
|
4625
4629
|
let count = 0;
|
|
@@ -4660,7 +4664,8 @@ var editTool = {
|
|
|
4660
4664
|
return {
|
|
4661
4665
|
path: absPath,
|
|
4662
4666
|
replacements: input.replace_all ? count : 1,
|
|
4663
|
-
diff
|
|
4667
|
+
diff,
|
|
4668
|
+
note: autoReadNote
|
|
4664
4669
|
};
|
|
4665
4670
|
}
|
|
4666
4671
|
};
|
|
@@ -5336,7 +5341,7 @@ var gitTool = {
|
|
|
5336
5341
|
name: "git",
|
|
5337
5342
|
category: "Git",
|
|
5338
5343
|
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.",
|
|
5339
|
-
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.",
|
|
5344
|
+
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.",
|
|
5340
5345
|
permission: "confirm",
|
|
5341
5346
|
icon: "git",
|
|
5342
5347
|
// Conservative: any of these may mutate. The non-mutating commands
|
|
@@ -5425,6 +5430,22 @@ var gitTool = {
|
|
|
5425
5430
|
};
|
|
5426
5431
|
}
|
|
5427
5432
|
const args = buildArgs(input);
|
|
5433
|
+
let safetyWarning;
|
|
5434
|
+
if (input.command === "commit") {
|
|
5435
|
+
try {
|
|
5436
|
+
const report = await assessCommitSafety({
|
|
5437
|
+
cwd: ctx.cwd,
|
|
5438
|
+
projectRoot: ctx.projectRoot,
|
|
5439
|
+
sessionId: ctx.session?.id,
|
|
5440
|
+
signal: opts.signal
|
|
5441
|
+
});
|
|
5442
|
+
if (report.warning) {
|
|
5443
|
+
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.";
|
|
5444
|
+
safetyWarning = report.warning + scopeNote;
|
|
5445
|
+
}
|
|
5446
|
+
} catch {
|
|
5447
|
+
}
|
|
5448
|
+
}
|
|
5428
5449
|
let stagedDiff;
|
|
5429
5450
|
if (input.command === "commit" && !input.dry_run) {
|
|
5430
5451
|
try {
|
|
@@ -5438,6 +5459,7 @@ var gitTool = {
|
|
|
5438
5459
|
}
|
|
5439
5460
|
const result = await runGit2(args, gitDir, opts.signal);
|
|
5440
5461
|
if (stagedDiff !== void 0) result.diff = stagedDiff;
|
|
5462
|
+
if (safetyWarning !== void 0) result.warning = safetyWarning;
|
|
5441
5463
|
return result;
|
|
5442
5464
|
}
|
|
5443
5465
|
};
|
|
@@ -6589,12 +6611,15 @@ var outdatedTool = {
|
|
|
6589
6611
|
// fixed four sibling tools (mcp_control, shellcheck, shellcheck_scan,
|
|
6590
6612
|
// web_search) but missed this one; applying the same contract here.
|
|
6591
6613
|
mutating: true,
|
|
6592
|
-
// Capability is
|
|
6614
|
+
// Capability is outbound network — the tool only hits the package
|
|
6593
6615
|
// registry over HTTP, never touches the filesystem or runs shell.
|
|
6616
|
+
// Use the canonical `net.outbound` capability (not the non-existent
|
|
6617
|
+
// `network` string) so the subagent allowlist recognises it and
|
|
6618
|
+
// permits read-only registry lookups under a director.
|
|
6594
6619
|
// The H7 invariant test requires this array to be non-empty for
|
|
6595
6620
|
// any mutating:true tool (meta-tools whitelisted). See
|
|
6596
6621
|
// tests/permission-mutating-invariant.test.ts:92.
|
|
6597
|
-
capabilities: ["
|
|
6622
|
+
capabilities: ["net.outbound"],
|
|
6598
6623
|
timeoutMs: 6e4,
|
|
6599
6624
|
inputSchema: {
|
|
6600
6625
|
type: "object",
|
|
@@ -7087,6 +7112,11 @@ var readTool = {
|
|
|
7087
7112
|
limit: {
|
|
7088
7113
|
type: "integer",
|
|
7089
7114
|
description: "Maximum number of lines to return (default is 2000)."
|
|
7115
|
+
},
|
|
7116
|
+
mode: {
|
|
7117
|
+
type: "string",
|
|
7118
|
+
enum: ["content", "summary"],
|
|
7119
|
+
description: "Return full line-numbered content (default) or a compact file summary with imports/exports/symbols."
|
|
7090
7120
|
}
|
|
7091
7121
|
},
|
|
7092
7122
|
required: ["path"]
|
|
@@ -7100,14 +7130,27 @@ var readTool = {
|
|
|
7100
7130
|
} catch (err) {
|
|
7101
7131
|
const code = err.code;
|
|
7102
7132
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
7103
|
-
throw new Error(
|
|
7104
|
-
`read: failed to stat "${input.path}": ${toErrorMessage(err)}`
|
|
7105
|
-
);
|
|
7133
|
+
throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage(err)}`);
|
|
7106
7134
|
}
|
|
7107
7135
|
if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
7108
7136
|
if (stat11.size > MAX_BYTES2) {
|
|
7109
7137
|
throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES2})`);
|
|
7110
7138
|
}
|
|
7139
|
+
const offset = Math.max(1, input.offset ?? 1);
|
|
7140
|
+
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
7141
|
+
const prior = getReadRangeRecord(ctx, absPath);
|
|
7142
|
+
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
7143
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat11.mtimeMs, offset, requestedEnd)) {
|
|
7144
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
7145
|
+
return {
|
|
7146
|
+
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.]`,
|
|
7147
|
+
total_lines: prior.totalLines,
|
|
7148
|
+
encoding: "utf8",
|
|
7149
|
+
truncated: requestedEnd < prior.totalLines,
|
|
7150
|
+
cached: true,
|
|
7151
|
+
note: "Repeated read suppressed to save tokens."
|
|
7152
|
+
};
|
|
7153
|
+
}
|
|
7111
7154
|
const buf = await fs14.readFile(absPath);
|
|
7112
7155
|
if (isBinaryBuffer(buf)) {
|
|
7113
7156
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
@@ -7115,17 +7158,38 @@ var readTool = {
|
|
|
7115
7158
|
const text = buf.toString("utf8");
|
|
7116
7159
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
7117
7160
|
const total = allLines.length;
|
|
7118
|
-
|
|
7119
|
-
|
|
7161
|
+
if (input.mode === "summary") {
|
|
7162
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
7163
|
+
rememberReadRange(ctx, absPath, stat11.mtimeMs, total, 1, Math.min(total, 200));
|
|
7164
|
+
return {
|
|
7165
|
+
text: summarizeFile(input.path, stat11.size, allLines),
|
|
7166
|
+
total_lines: total,
|
|
7167
|
+
encoding: "utf8",
|
|
7168
|
+
truncated: total > 200,
|
|
7169
|
+
note: "Summary mode returned compact structure instead of full file content."
|
|
7170
|
+
};
|
|
7171
|
+
}
|
|
7120
7172
|
if (limit === 0) {
|
|
7121
7173
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
7174
|
+
rememberReadRange(ctx, absPath, stat11.mtimeMs, total, 1, 0);
|
|
7122
7175
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
7123
7176
|
}
|
|
7177
|
+
if (offset > total) {
|
|
7178
|
+
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
7179
|
+
rememberReadRange(ctx, absPath, stat11.mtimeMs, total, total + 1, total + 1);
|
|
7180
|
+
return {
|
|
7181
|
+
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
7182
|
+
total_lines: total,
|
|
7183
|
+
encoding: "utf8",
|
|
7184
|
+
truncated: false
|
|
7185
|
+
};
|
|
7186
|
+
}
|
|
7124
7187
|
const slice = allLines.slice(offset - 1, offset - 1 + limit);
|
|
7125
7188
|
const truncated = offset - 1 + slice.length < total;
|
|
7126
7189
|
const width = String(offset + slice.length - 1).length;
|
|
7127
7190
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
7128
7191
|
ctx.recordRead(absPath, stat11.mtimeMs);
|
|
7192
|
+
rememberReadRange(ctx, absPath, stat11.mtimeMs, total, offset, offset + slice.length - 1);
|
|
7129
7193
|
return {
|
|
7130
7194
|
text: numbered,
|
|
7131
7195
|
total_lines: total,
|
|
@@ -7134,6 +7198,62 @@ var readTool = {
|
|
|
7134
7198
|
};
|
|
7135
7199
|
}
|
|
7136
7200
|
};
|
|
7201
|
+
var READ_RANGES_META_KEY = "tools.read.ranges.v1";
|
|
7202
|
+
function getReadRanges(ctx) {
|
|
7203
|
+
const existing = ctx.meta[READ_RANGES_META_KEY];
|
|
7204
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
7205
|
+
return existing;
|
|
7206
|
+
}
|
|
7207
|
+
const next = {};
|
|
7208
|
+
ctx.meta[READ_RANGES_META_KEY] = next;
|
|
7209
|
+
return next;
|
|
7210
|
+
}
|
|
7211
|
+
function getReadRangeRecord(ctx, absPath) {
|
|
7212
|
+
return getReadRanges(ctx)[absPath];
|
|
7213
|
+
}
|
|
7214
|
+
function rememberReadRange(ctx, absPath, mtimeMs, totalLines, start, end) {
|
|
7215
|
+
if (end < start) return;
|
|
7216
|
+
const ranges = getReadRanges(ctx);
|
|
7217
|
+
const prior = ranges[absPath];
|
|
7218
|
+
const nextRanges = prior && Math.abs(prior.mtimeMs - mtimeMs) <= 1 ? prior.ranges.slice() : [];
|
|
7219
|
+
nextRanges.push({ start, end });
|
|
7220
|
+
ranges[absPath] = {
|
|
7221
|
+
mtimeMs,
|
|
7222
|
+
totalLines,
|
|
7223
|
+
ranges: mergeRanges(nextRanges)
|
|
7224
|
+
};
|
|
7225
|
+
}
|
|
7226
|
+
function coversRange(record, mtimeMs, start, end) {
|
|
7227
|
+
if (Math.abs(record.mtimeMs - mtimeMs) > 1) return false;
|
|
7228
|
+
return record.ranges.some((range) => range.start <= start && range.end >= end);
|
|
7229
|
+
}
|
|
7230
|
+
function mergeRanges(ranges) {
|
|
7231
|
+
const sorted = ranges.slice().sort((a, b) => a.start - b.start);
|
|
7232
|
+
const merged = [];
|
|
7233
|
+
for (const range of sorted) {
|
|
7234
|
+
const last = merged[merged.length - 1];
|
|
7235
|
+
if (!last || range.start > last.end + 1) {
|
|
7236
|
+
merged.push({ ...range });
|
|
7237
|
+
continue;
|
|
7238
|
+
}
|
|
7239
|
+
last.end = Math.max(last.end, range.end);
|
|
7240
|
+
}
|
|
7241
|
+
return merged;
|
|
7242
|
+
}
|
|
7243
|
+
function summarizeFile(filePath, bytes, lines) {
|
|
7244
|
+
const interesting = lines.map((line, index) => ({ line: line.trim(), number: index + 1 })).filter(
|
|
7245
|
+
({ 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(
|
|
7246
|
+
line
|
|
7247
|
+
)
|
|
7248
|
+
).slice(0, 80).map(({ line, number }) => `${number}: ${line}`);
|
|
7249
|
+
return [
|
|
7250
|
+
`summary: ${filePath}`,
|
|
7251
|
+
`bytes=${bytes}`,
|
|
7252
|
+
`total_lines=${lines.length}`,
|
|
7253
|
+
interesting.length > 0 ? `symbols/imports:
|
|
7254
|
+
${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
7255
|
+
].join("\n");
|
|
7256
|
+
}
|
|
7137
7257
|
var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
7138
7258
|
var replaceTool = {
|
|
7139
7259
|
name: "replace",
|