@wrongstack/tools 0.283.1 → 0.284.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/audit.js.map +1 -1
- package/dist/bash.js +4 -0
- package/dist/bash.js.map +1 -1
- package/dist/builtin.js +685 -253
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.js +2 -2
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/diff.js.map +1 -1
- package/dist/document.js.map +1 -1
- package/dist/edit.d.ts +43 -0
- package/dist/edit.js +386 -60
- package/dist/edit.js.map +1 -1
- package/dist/{exec-Ca3fnpUh.d.ts → exec-2OKoT6tk.d.ts} +1 -1
- package/dist/exec.d.ts +1 -1
- package/dist/exec.js +13 -14
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js.map +1 -1
- package/dist/format.js.map +1 -1
- package/dist/git.js.map +1 -1
- package/dist/glob.js +10 -1
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +4 -0
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.js +730 -268
- package/dist/index.js.map +1 -1
- package/dist/install.js.map +1 -1
- package/dist/json.js.map +1 -1
- package/dist/lint.js.map +1 -1
- package/dist/logs.js.map +1 -1
- package/dist/next-steps.d.ts +66 -0
- package/dist/next-steps.js +116 -0
- package/dist/next-steps.js.map +1 -0
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +685 -253
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +43 -0
- package/dist/patch.js.map +1 -1
- package/dist/read.js +13 -4
- package/dist/read.js.map +1 -1
- package/dist/replace.js +14 -0
- package/dist/replace.js.map +1 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/test.js.map +1 -1
- package/dist/tool-diff.d.ts +88 -0
- package/dist/tool-diff.js +229 -0
- package/dist/tool-diff.js.map +1 -0
- package/dist/tool-summary.d.ts +24 -0
- package/dist/tool-summary.js +208 -0
- package/dist/tool-summary.js.map +1 -0
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js.map +1 -1
- package/dist/write.d.ts +6 -0
- package/dist/write.js +118 -19
- package/dist/write.js.map +1 -1
- package/package.json +15 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import { statSync, mkdirSync, createWriteStream } from 'node:fs';
|
|
3
|
-
import * as
|
|
3
|
+
import * as path12 from 'node:path';
|
|
4
4
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
5
5
|
import { spawn, execFileSync } from 'node:child_process';
|
|
6
6
|
import * as Core from '@wrongstack/core';
|
|
7
|
-
import { SKILL_LIMITS, buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, ToolValidationError, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, ToolError, FetchError, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, deepMerge,
|
|
7
|
+
import { SKILL_LIMITS, buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, ToolValidationError, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, ToolError, FetchError, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, deepMerge, deserializeTaskGraph, serializeTaskGraph, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, FsError, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, stripFrontmatter, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
8
8
|
import * as fs2 from 'node:fs/promises';
|
|
9
9
|
import * as os2 from 'node:os';
|
|
10
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
10
11
|
import { toErrorMessage as toErrorMessage$3 } from '@wrongstack/core/utils';
|
|
11
12
|
import { createRequire } from 'node:module';
|
|
12
13
|
import { fileURLToPath } from 'node:url';
|
|
@@ -17,7 +18,7 @@ import * as dns from 'node:dns/promises';
|
|
|
17
18
|
import * as net from 'node:net';
|
|
18
19
|
import { Agent, fetch } from 'undici';
|
|
19
20
|
import TurndownService from 'turndown';
|
|
20
|
-
import {
|
|
21
|
+
import { addLinkToTask, addNoteToTask, updateCheckOnTask, addCheckToTask, updateGoalMetricOnTask, addGoalMetricToTask, addDependency, getKanbanQueueHealth, listKanbanEvents, recoverStaleTaskAssignments, heartbeatTaskAssignment, updateTaskAssignment, assignTask, releaseTaskClaim, claimReadyTask, getTaskChain, setTaskChain, removeTask, moveTask, updateTask, getTask, transferTaskToBoard, copyTaskToBoard, mergeTasks, splitTask, addTask, removeColumn, updateColumn, addColumn, getKanbanOrchestrationSnapshot, listReadyTasks, searchKanban, syncBoardFromTaskGraph, exportBoardToTaskGraph, exportBoardAsMarkdown, generateBoardFromDescription, createBoard, parseLinesIntoTasks, getBoard, removeBoard, duplicateBoard, updateBoard, listBoards } from '@wrongstack/kanban';
|
|
21
22
|
|
|
22
23
|
// src/_danger-detect.ts
|
|
23
24
|
var argHas = (args, value) => args.includes(value);
|
|
@@ -230,23 +231,18 @@ var RULES = [
|
|
|
230
231
|
},
|
|
231
232
|
reason: "inline script evaluation (-c / -e / --eval)"
|
|
232
233
|
},
|
|
233
|
-
// ----- pipe-to-shell (
|
|
234
|
+
// ----- pipe-to-shell (destructive — download-and-run pattern) -----
|
|
234
235
|
// The classic `curl https://... | sh` download-and-run vector. Detected by
|
|
235
|
-
// looking for a known fetcher
|
|
236
|
-
//
|
|
237
|
-
//
|
|
236
|
+
// looking for a known fetcher piped into a shell or expression evaluator.
|
|
237
|
+
// A shell command passed through `bash -c` / `pwsh -Command` arrives as one
|
|
238
|
+
// argv string, so scan the reconstructed argv text rather than individual
|
|
239
|
+
// tokens only.
|
|
238
240
|
{
|
|
239
241
|
id: "pipe-to-shell",
|
|
240
|
-
level: "
|
|
241
|
-
test: (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
);
|
|
245
|
-
const hasShellSink = args.some(
|
|
246
|
-
(a) => a === "sh" || a === "bash" || a === "zsh" || a === "fish" || a === "pwsh" || a === "powershell" || a.endsWith("/sh") || a.endsWith("/bash") || a.endsWith("/zsh") || a.endsWith("/pwsh")
|
|
247
|
-
);
|
|
248
|
-
return hasFetcher && hasShellSink;
|
|
249
|
-
},
|
|
242
|
+
level: "destructive",
|
|
243
|
+
test: (cmd, args) => /\b(?:curl|wget|fetch|httpie|http|irm|iwr|Invoke-WebRequest|Invoke-RestMethod)\b[\s\S]{0,300}\|\s*(?:sudo\s+)?(?:sh|bash|zsh|fish|pwsh|powershell|iex|Invoke-Expression)\b/i.test(
|
|
244
|
+
[cmd, ...args].join(" ")
|
|
245
|
+
),
|
|
250
246
|
reason: "network fetch piped to a shell (download-and-run pattern)"
|
|
251
247
|
},
|
|
252
248
|
// ----- privilege escalation (caution) -----
|
|
@@ -306,13 +302,13 @@ function levelRank(level) {
|
|
|
306
302
|
}
|
|
307
303
|
function resolveWin32Command(cmd) {
|
|
308
304
|
if (process.platform !== "win32") return cmd;
|
|
309
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
305
|
+
if (cmd.includes("/") || cmd.includes("\\") || path12.extname(cmd.replace(/\//g, "\\"))) {
|
|
310
306
|
return cmd;
|
|
311
307
|
}
|
|
312
308
|
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
313
|
-
const pathDirs = (process.env["PATH"] ?? "").split(
|
|
309
|
+
const pathDirs = (process.env["PATH"] ?? "").split(path12.delimiter);
|
|
314
310
|
for (const dir of pathDirs) {
|
|
315
|
-
const base =
|
|
311
|
+
const base = path12.join(dir, cmd);
|
|
316
312
|
for (const ext of pathext) {
|
|
317
313
|
const full = `${base}${ext}`;
|
|
318
314
|
try {
|
|
@@ -393,7 +389,7 @@ var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
|
393
389
|
var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
|
|
394
390
|
var sweepStarted = false;
|
|
395
391
|
function toolOutputDir() {
|
|
396
|
-
return
|
|
392
|
+
return path12.join(wstackGlobalRoot(), "tool-output");
|
|
397
393
|
}
|
|
398
394
|
function sweepOldSpoolFiles(dir) {
|
|
399
395
|
if (sweepStarted) return;
|
|
@@ -403,7 +399,7 @@ function sweepOldSpoolFiles(dir) {
|
|
|
403
399
|
const now2 = Date.now();
|
|
404
400
|
for (const name of await fs2.readdir(dir)) {
|
|
405
401
|
if (!name.endsWith(".log")) continue;
|
|
406
|
-
const p =
|
|
402
|
+
const p = path12.join(dir, name);
|
|
407
403
|
try {
|
|
408
404
|
const st = await fs2.stat(p);
|
|
409
405
|
if (now2 - st.mtimeMs > SPOOL_RETENTION_MS) await fs2.unlink(p);
|
|
@@ -438,7 +434,7 @@ function createOutputSpool(opts) {
|
|
|
438
434
|
sweepOldSpoolFiles(dir);
|
|
439
435
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
440
436
|
const rand = Math.random().toString(36).slice(2, 6);
|
|
441
|
-
filePath =
|
|
437
|
+
filePath = path12.join(dir, `${stamp}-${safeTool}-${rand}.log`);
|
|
442
438
|
stream = createWriteStream(filePath, { flags: "w", encoding: "utf8" });
|
|
443
439
|
stream.on("error", () => {
|
|
444
440
|
failed = true;
|
|
@@ -1237,37 +1233,40 @@ async function* spawnStream(opts) {
|
|
|
1237
1233
|
}
|
|
1238
1234
|
}
|
|
1239
1235
|
}
|
|
1236
|
+
function sha256hex(content) {
|
|
1237
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1238
|
+
}
|
|
1240
1239
|
async function detectPackageManager(cwd) {
|
|
1241
|
-
const { stat:
|
|
1240
|
+
const { stat: stat13 } = await import('node:fs/promises');
|
|
1242
1241
|
try {
|
|
1243
|
-
await
|
|
1242
|
+
await stat13(`${cwd}/pnpm-lock.yaml`);
|
|
1244
1243
|
return "pnpm";
|
|
1245
1244
|
} catch {
|
|
1246
1245
|
}
|
|
1247
1246
|
try {
|
|
1248
|
-
await
|
|
1247
|
+
await stat13(`${cwd}/yarn.lock`);
|
|
1249
1248
|
return "yarn";
|
|
1250
1249
|
} catch {
|
|
1251
1250
|
}
|
|
1252
1251
|
return "npm";
|
|
1253
1252
|
}
|
|
1254
1253
|
function resolvePath(input, ctx) {
|
|
1255
|
-
return
|
|
1254
|
+
return path12.isAbsolute(input) ? path12.normalize(input) : path12.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
1256
1255
|
}
|
|
1257
1256
|
function allowedRoots(ctx) {
|
|
1258
|
-
return [
|
|
1257
|
+
return [path12.resolve(ctx.projectRoot), path12.resolve(Core.wstackGlobalRoot())];
|
|
1259
1258
|
}
|
|
1260
1259
|
function isInsideAny(target, roots) {
|
|
1261
1260
|
return roots.some((root) => {
|
|
1262
|
-
const rel =
|
|
1263
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
1261
|
+
const rel = path12.relative(root, target);
|
|
1262
|
+
return rel === "" || !rel.startsWith("..") && !path12.isAbsolute(rel);
|
|
1264
1263
|
});
|
|
1265
1264
|
}
|
|
1266
1265
|
function ensureInsideRoot(absPath, ctx) {
|
|
1267
|
-
const target =
|
|
1266
|
+
const target = path12.resolve(absPath);
|
|
1268
1267
|
if (ctx.allowOutsideProjectRoot) return target;
|
|
1269
1268
|
if (isInsideAny(target, allowedRoots(ctx))) return target;
|
|
1270
|
-
throw new Error(`Path "${absPath}" is outside project root "${
|
|
1269
|
+
throw new Error(`Path "${absPath}" is outside project root "${path12.resolve(ctx.projectRoot)}"`);
|
|
1271
1270
|
}
|
|
1272
1271
|
function safeResolve(input, ctx) {
|
|
1273
1272
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
@@ -1275,7 +1274,7 @@ function safeResolve(input, ctx) {
|
|
|
1275
1274
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
1276
1275
|
if (ctx.allowOutsideProjectRoot) return;
|
|
1277
1276
|
const realRoots = await Promise.all(
|
|
1278
|
-
allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() =>
|
|
1277
|
+
allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() => path12.resolve(r)))
|
|
1279
1278
|
);
|
|
1280
1279
|
let probe = absPath;
|
|
1281
1280
|
for (; ; ) {
|
|
@@ -1284,7 +1283,7 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
1284
1283
|
real = await fs2.realpath(probe);
|
|
1285
1284
|
} catch (err) {
|
|
1286
1285
|
if (err.code === "ENOENT") {
|
|
1287
|
-
const parent =
|
|
1286
|
+
const parent = path12.dirname(probe);
|
|
1288
1287
|
if (parent === probe) return;
|
|
1289
1288
|
probe = parent;
|
|
1290
1289
|
continue;
|
|
@@ -1597,15 +1596,15 @@ var PersistentProcessRegistry = class {
|
|
|
1597
1596
|
constructor(baseRegistry) {
|
|
1598
1597
|
this.instanceId = generateInstanceId();
|
|
1599
1598
|
const homeDir = os2.homedir();
|
|
1600
|
-
this.registryPath =
|
|
1601
|
-
this.lockPath =
|
|
1599
|
+
this.registryPath = path12.join(homeDir, REGISTRY_FILE);
|
|
1600
|
+
this.lockPath = path12.join(homeDir, LOCKFILE);
|
|
1602
1601
|
this.baseRegistry = baseRegistry ?? getProcessRegistry();
|
|
1603
1602
|
this.ensureDirectory().catch((err) => {
|
|
1604
1603
|
emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
|
|
1605
1604
|
});
|
|
1606
1605
|
}
|
|
1607
1606
|
async ensureDirectory() {
|
|
1608
|
-
const dir =
|
|
1607
|
+
const dir = path12.dirname(this.registryPath);
|
|
1609
1608
|
try {
|
|
1610
1609
|
await fs2.mkdir(dir, { recursive: true });
|
|
1611
1610
|
} catch (err) {
|
|
@@ -2191,6 +2190,10 @@ var bashTool = {
|
|
|
2191
2190
|
category: "Shell",
|
|
2192
2191
|
description: "Execute an arbitrary command in the user's default shell (bash/zsh/pwsh/cmd). stdout and stderr are merged into one stream. This is the most powerful and dangerous tool \u2014 it gives the model full access to the developer's machine. Prefer specialized tools whenever possible.",
|
|
2193
2192
|
usageHint: "SECURITY WARNING: This tool runs with the full privileges of the current user.\n\nBest practices for the model:\n- Strongly prefer `exec` for known safe commands (node, npm, pnpm, tsc, git, etc.).\n- Use bash only when you genuinely need shell features (pipes, redirection, complex one-liners).\n- Prefer single focused commands over huge `&&` chains.\n- Use `background: true` only for long-running processes (dev servers, watchers).\n- The working directory is the project root.\n- Output may be truncated in the middle for very large results.",
|
|
2193
|
+
selection: {
|
|
2194
|
+
doNotUseWhen: "the command is allowlisted and does not require pipes, redirection, or shell expansion.",
|
|
2195
|
+
useInstead: ["exec"]
|
|
2196
|
+
},
|
|
2194
2197
|
permission: "confirm",
|
|
2195
2198
|
mutating: true,
|
|
2196
2199
|
riskTier: "destructive",
|
|
@@ -3055,7 +3058,7 @@ var IndexStore = class {
|
|
|
3055
3058
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
3056
3059
|
fs.mkdirSync(this.indexDir, { recursive: true });
|
|
3057
3060
|
const Database = loadDatabaseSync();
|
|
3058
|
-
this.db = new Database(
|
|
3061
|
+
this.db = new Database(path12.join(this.indexDir, DB_FILE));
|
|
3059
3062
|
try {
|
|
3060
3063
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
3061
3064
|
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -3634,7 +3637,7 @@ var IndexStore = class {
|
|
|
3634
3637
|
}));
|
|
3635
3638
|
}
|
|
3636
3639
|
sizeBytes() {
|
|
3637
|
-
const dbPath =
|
|
3640
|
+
const dbPath = path12.join(this.indexDir, DB_FILE);
|
|
3638
3641
|
try {
|
|
3639
3642
|
return fs.statSync(dbPath).size;
|
|
3640
3643
|
} catch {
|
|
@@ -4133,9 +4136,9 @@ func formatType(t ast.Expr) string {
|
|
|
4133
4136
|
}
|
|
4134
4137
|
`;
|
|
4135
4138
|
async function syncGoParse(filePath, content, lang) {
|
|
4136
|
-
const tmpDir = await fs2.mkdtemp(
|
|
4139
|
+
const tmpDir = await fs2.mkdtemp(path12.join(os2.tmpdir(), "ws-go-parse-"));
|
|
4137
4140
|
try {
|
|
4138
|
-
const scriptPath =
|
|
4141
|
+
const scriptPath = path12.join(tmpDir, "parse.go");
|
|
4139
4142
|
await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
4140
4143
|
const proc = spawn("go", ["run", scriptPath], {
|
|
4141
4144
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -4397,9 +4400,9 @@ var _cachedScriptPath = null;
|
|
|
4397
4400
|
async function syncPyParse(filePath, content, lang) {
|
|
4398
4401
|
try {
|
|
4399
4402
|
if (!_cachedScriptPath) {
|
|
4400
|
-
const tmpDir =
|
|
4403
|
+
const tmpDir = path12.join(os2.tmpdir(), "ws-py-parse");
|
|
4401
4404
|
await fs2.mkdir(tmpDir, { recursive: true });
|
|
4402
|
-
_cachedScriptPath =
|
|
4405
|
+
_cachedScriptPath = path12.join(tmpDir, "parse.py");
|
|
4403
4406
|
await fs2.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
4404
4407
|
}
|
|
4405
4408
|
const proc = spawn("python", [_cachedScriptPath, filePath], {
|
|
@@ -4457,7 +4460,7 @@ async function parseSymbols4(opts) {
|
|
|
4457
4460
|
function checkNativeParser() {
|
|
4458
4461
|
try {
|
|
4459
4462
|
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
4460
|
-
const toolsDir =
|
|
4463
|
+
const toolsDir = path12.join(process.cwd(), "tools");
|
|
4461
4464
|
try {
|
|
4462
4465
|
execFileSync(
|
|
4463
4466
|
"cargo",
|
|
@@ -4467,7 +4470,7 @@ function checkNativeParser() {
|
|
|
4467
4470
|
"--format-version",
|
|
4468
4471
|
"1",
|
|
4469
4472
|
"--manifest-path",
|
|
4470
|
-
|
|
4473
|
+
path12.join(toolsDir, "Cargo.toml")
|
|
4471
4474
|
],
|
|
4472
4475
|
{ stdio: "pipe", windowsHide: true }
|
|
4473
4476
|
);
|
|
@@ -4481,13 +4484,13 @@ function checkNativeParser() {
|
|
|
4481
4484
|
}
|
|
4482
4485
|
async function tryNativeParse(file, content) {
|
|
4483
4486
|
try {
|
|
4484
|
-
const toolsDir =
|
|
4485
|
-
const crateDir =
|
|
4486
|
-
const tmpFile =
|
|
4487
|
+
const toolsDir = path12.join(process.cwd(), "tools");
|
|
4488
|
+
const crateDir = path12.join(toolsDir, "syn-parser");
|
|
4489
|
+
const tmpFile = path12.join(crateDir, "src", "input.rs");
|
|
4487
4490
|
await fs2.writeFile(tmpFile, content, "utf8");
|
|
4488
4491
|
const proc = spawn(
|
|
4489
4492
|
"cargo",
|
|
4490
|
-
["run", "--manifest-path",
|
|
4493
|
+
["run", "--manifest-path", path12.join(toolsDir, "Cargo.toml")],
|
|
4491
4494
|
{
|
|
4492
4495
|
cwd: process.cwd(),
|
|
4493
4496
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -4537,16 +4540,16 @@ function regexParse(opts) {
|
|
|
4537
4540
|
const { file, content, lang } = opts;
|
|
4538
4541
|
const symbols = [];
|
|
4539
4542
|
const lines = content.split("\n");
|
|
4540
|
-
const
|
|
4543
|
+
const lineOffsets2 = [0];
|
|
4541
4544
|
for (let i = 0; i < lines.length; i++) {
|
|
4542
|
-
|
|
4545
|
+
lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
|
|
4543
4546
|
}
|
|
4544
4547
|
function lineFromOffset(offset) {
|
|
4545
4548
|
let lo = 0;
|
|
4546
|
-
let hi =
|
|
4549
|
+
let hi = lineOffsets2.length - 1;
|
|
4547
4550
|
while (lo < hi) {
|
|
4548
4551
|
const mid = lo + hi + 1 >>> 1;
|
|
4549
|
-
if (expectDefined(
|
|
4552
|
+
if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
|
|
4550
4553
|
else hi = mid - 1;
|
|
4551
4554
|
}
|
|
4552
4555
|
return lo + 1;
|
|
@@ -4561,7 +4564,7 @@ function regexParse(opts) {
|
|
|
4561
4564
|
const name = expectDefined(match[1]);
|
|
4562
4565
|
const offset = match.index ?? 0;
|
|
4563
4566
|
const line = lineFromOffset(offset);
|
|
4564
|
-
const col = offset - (
|
|
4567
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4565
4568
|
const lineIdx = line - 1;
|
|
4566
4569
|
const signature = extractDeclaration(lineIdx);
|
|
4567
4570
|
symbols.push({
|
|
@@ -4599,22 +4602,22 @@ function parseSymbols5(opts) {
|
|
|
4599
4602
|
function regexParse2(opts) {
|
|
4600
4603
|
const { file, content, lang } = opts;
|
|
4601
4604
|
const symbols = [];
|
|
4602
|
-
const
|
|
4603
|
-
const isPackageJson =
|
|
4604
|
-
const isTsconfig =
|
|
4605
|
+
const basename4 = path12.basename(file).toLowerCase();
|
|
4606
|
+
const isPackageJson = basename4 === "package.json";
|
|
4607
|
+
const isTsconfig = basename4 === "tsconfig.json" || basename4 === "tsconfig.build.json";
|
|
4605
4608
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
4606
4609
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
4607
4610
|
const lines = content.split("\n");
|
|
4608
|
-
const
|
|
4611
|
+
const lineOffsets2 = [0];
|
|
4609
4612
|
for (let i = 0; i < lines.length; i++) {
|
|
4610
|
-
|
|
4613
|
+
lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
|
|
4611
4614
|
}
|
|
4612
4615
|
function lineFromOffset(offset) {
|
|
4613
4616
|
let lo = 0;
|
|
4614
|
-
let hi =
|
|
4617
|
+
let hi = lineOffsets2.length - 1;
|
|
4615
4618
|
while (lo < hi) {
|
|
4616
4619
|
const mid = lo + hi + 1 >>> 1;
|
|
4617
|
-
if (expectDefined(
|
|
4620
|
+
if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
|
|
4618
4621
|
else hi = mid - 1;
|
|
4619
4622
|
}
|
|
4620
4623
|
return lo + 1;
|
|
@@ -4625,11 +4628,11 @@ function regexParse2(opts) {
|
|
|
4625
4628
|
const line = lineFromOffset(offset);
|
|
4626
4629
|
symbols.push(
|
|
4627
4630
|
makeSymbol({
|
|
4628
|
-
name:
|
|
4631
|
+
name: path12.basename(file),
|
|
4629
4632
|
kind: "object",
|
|
4630
4633
|
line,
|
|
4631
4634
|
col: 0,
|
|
4632
|
-
signature: `"${
|
|
4635
|
+
signature: `"${path12.basename(file)}" = { ... }`,
|
|
4633
4636
|
file,
|
|
4634
4637
|
lang
|
|
4635
4638
|
})
|
|
@@ -4640,7 +4643,7 @@ function regexParse2(opts) {
|
|
|
4640
4643
|
const key = expectDefined(match[1]);
|
|
4641
4644
|
const offset = match.index ?? 0;
|
|
4642
4645
|
const line = lineFromOffset(offset);
|
|
4643
|
-
const col = offset - (
|
|
4646
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4644
4647
|
let kind = "property";
|
|
4645
4648
|
let signature = `"${key}": ..."`;
|
|
4646
4649
|
if (isPackageJson) {
|
|
@@ -4675,10 +4678,10 @@ function regexParse2(opts) {
|
|
|
4675
4678
|
})
|
|
4676
4679
|
);
|
|
4677
4680
|
if (isPackageJson && key === "scripts") {
|
|
4678
|
-
extractPackageScripts(content, symbols, file, lang,
|
|
4681
|
+
extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset);
|
|
4679
4682
|
}
|
|
4680
4683
|
if (isTsconfig && key === "compilerOptions") {
|
|
4681
|
-
extractCompilerOptions(content, symbols, file, lang,
|
|
4684
|
+
extractCompilerOptions(content, symbols, file, lang, lineOffsets2, line, lineFromOffset);
|
|
4682
4685
|
}
|
|
4683
4686
|
}
|
|
4684
4687
|
const defsRegex = /"\$defs"\s*:|"\$defs"\s*:/g;
|
|
@@ -4691,7 +4694,7 @@ function regexParse2(opts) {
|
|
|
4691
4694
|
name: "$defs",
|
|
4692
4695
|
kind: "property",
|
|
4693
4696
|
line,
|
|
4694
|
-
col: offset - (
|
|
4697
|
+
col: offset - (lineOffsets2[line - 1] ?? 0),
|
|
4695
4698
|
signature: '"$defs": { ... }',
|
|
4696
4699
|
file,
|
|
4697
4700
|
lang
|
|
@@ -4715,7 +4718,7 @@ function regexParse2(opts) {
|
|
|
4715
4718
|
name: key,
|
|
4716
4719
|
kind: "property",
|
|
4717
4720
|
line,
|
|
4718
|
-
col: offset - (
|
|
4721
|
+
col: offset - (lineOffsets2[line - 1] ?? 0),
|
|
4719
4722
|
signature: `"${key}": { ... }`,
|
|
4720
4723
|
file,
|
|
4721
4724
|
lang
|
|
@@ -4725,7 +4728,7 @@ function regexParse2(opts) {
|
|
|
4725
4728
|
}
|
|
4726
4729
|
return { file, lang, symbols, mtimeMs: Date.now() };
|
|
4727
4730
|
}
|
|
4728
|
-
function extractPackageScripts(content, symbols, file, lang,
|
|
4731
|
+
function extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset) {
|
|
4729
4732
|
const scriptsBlockRegex = /"scripts"\s*:\s*\{([^}]+)\}/g;
|
|
4730
4733
|
for (let match = scriptsBlockRegex.exec(content); match !== null; match = scriptsBlockRegex.exec(content)) {
|
|
4731
4734
|
const blockContent = expectDefined(match[0]);
|
|
@@ -4740,7 +4743,7 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFr
|
|
|
4740
4743
|
name: key,
|
|
4741
4744
|
kind: "function",
|
|
4742
4745
|
line,
|
|
4743
|
-
col: keyOffset - (
|
|
4746
|
+
col: keyOffset - (lineOffsets2[line - 1] ?? 0),
|
|
4744
4747
|
signature: `"${key}": "..."`,
|
|
4745
4748
|
file,
|
|
4746
4749
|
lang
|
|
@@ -4749,7 +4752,7 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFr
|
|
|
4749
4752
|
}
|
|
4750
4753
|
}
|
|
4751
4754
|
}
|
|
4752
|
-
function extractCompilerOptions(content, symbols, file, lang,
|
|
4755
|
+
function extractCompilerOptions(content, symbols, file, lang, lineOffsets2, parentLine, lineFromOffset) {
|
|
4753
4756
|
const optsBlockRegex = /"compilerOptions"\s*:\s*\{([^}]+)\}/g;
|
|
4754
4757
|
for (let match = optsBlockRegex.exec(content); match !== null; match = optsBlockRegex.exec(content)) {
|
|
4755
4758
|
const blockContent = expectDefined(match[0]);
|
|
@@ -4765,7 +4768,7 @@ function extractCompilerOptions(content, symbols, file, lang, lineOffsets, paren
|
|
|
4765
4768
|
name: key,
|
|
4766
4769
|
kind: "property",
|
|
4767
4770
|
line,
|
|
4768
|
-
col: keyOffset - (
|
|
4771
|
+
col: keyOffset - (lineOffsets2[line - 1] ?? 0),
|
|
4769
4772
|
signature: `"${key}": ...`,
|
|
4770
4773
|
file,
|
|
4771
4774
|
lang
|
|
@@ -4801,16 +4804,16 @@ function regexParse3(opts) {
|
|
|
4801
4804
|
const { file, content, lang } = opts;
|
|
4802
4805
|
const symbols = [];
|
|
4803
4806
|
const lines = content.split("\n");
|
|
4804
|
-
const
|
|
4807
|
+
const lineOffsets2 = [0];
|
|
4805
4808
|
for (let i = 0; i < lines.length; i++) {
|
|
4806
|
-
|
|
4809
|
+
lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
|
|
4807
4810
|
}
|
|
4808
4811
|
function lineFromOffset(offset) {
|
|
4809
4812
|
let lo = 0;
|
|
4810
|
-
let hi =
|
|
4813
|
+
let hi = lineOffsets2.length - 1;
|
|
4811
4814
|
while (lo < hi) {
|
|
4812
4815
|
const mid = lo + hi + 1 >>> 1;
|
|
4813
|
-
if (expectDefined(
|
|
4816
|
+
if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
|
|
4814
4817
|
else hi = mid - 1;
|
|
4815
4818
|
}
|
|
4816
4819
|
return lo + 1;
|
|
@@ -4820,7 +4823,7 @@ function regexParse3(opts) {
|
|
|
4820
4823
|
const name = expectDefined(match[1]);
|
|
4821
4824
|
const offset = match.index ?? 0;
|
|
4822
4825
|
const line = lineFromOffset(offset);
|
|
4823
|
-
const col = offset - (
|
|
4826
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4824
4827
|
symbols.push(
|
|
4825
4828
|
makeSymbol2({
|
|
4826
4829
|
name,
|
|
@@ -4838,7 +4841,7 @@ function regexParse3(opts) {
|
|
|
4838
4841
|
const name = expectDefined(match[1]);
|
|
4839
4842
|
const offset = match.index ?? 0;
|
|
4840
4843
|
const line = lineFromOffset(offset);
|
|
4841
|
-
const col = offset - (
|
|
4844
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4842
4845
|
symbols.push(
|
|
4843
4846
|
makeSymbol2({
|
|
4844
4847
|
name,
|
|
@@ -4858,7 +4861,7 @@ function regexParse3(opts) {
|
|
|
4858
4861
|
if (!key) continue;
|
|
4859
4862
|
const offset = match.index ?? 0;
|
|
4860
4863
|
const line = lineFromOffset(offset);
|
|
4861
|
-
const col = offset - (
|
|
4864
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4862
4865
|
const lineContent = lines[line - 1] ?? "";
|
|
4863
4866
|
if (/^[|&>]/.test(lineContent.trim())) continue;
|
|
4864
4867
|
if (key === "---" || key === "...") continue;
|
|
@@ -4873,7 +4876,7 @@ function regexParse3(opts) {
|
|
|
4873
4876
|
const key = expectDefined(match[2]);
|
|
4874
4877
|
const offset = match.index ?? 0;
|
|
4875
4878
|
const line = lineFromOffset(offset);
|
|
4876
|
-
const col = offset - (
|
|
4879
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4877
4880
|
const value = extractValue(content, offset + match[0]?.length);
|
|
4878
4881
|
const kind = isScalar(value) ? "literal" : "property";
|
|
4879
4882
|
symbols.push(
|
|
@@ -4893,7 +4896,7 @@ function regexParse3(opts) {
|
|
|
4893
4896
|
const key = expectDefined(match[2]);
|
|
4894
4897
|
const offset = match.index ?? 0;
|
|
4895
4898
|
const line = lineFromOffset(offset);
|
|
4896
|
-
const col = offset - (
|
|
4899
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4897
4900
|
symbols.push(
|
|
4898
4901
|
makeSymbol2({
|
|
4899
4902
|
name: key,
|
|
@@ -4979,7 +4982,7 @@ function compileGitignore(lines) {
|
|
|
4979
4982
|
async function loadGitignoreMatcher(projectRoot) {
|
|
4980
4983
|
let lines = [];
|
|
4981
4984
|
try {
|
|
4982
|
-
const raw = await fs2.readFile(
|
|
4985
|
+
const raw = await fs2.readFile(path12.join(projectRoot, ".gitignore"), "utf8");
|
|
4983
4986
|
lines = raw.split("\n");
|
|
4984
4987
|
} catch {
|
|
4985
4988
|
}
|
|
@@ -5044,14 +5047,14 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
5044
5047
|
dirCount++;
|
|
5045
5048
|
for (const e of entries) {
|
|
5046
5049
|
if (ignoreSet.has(e.name)) continue;
|
|
5047
|
-
const full =
|
|
5048
|
-
const rel =
|
|
5050
|
+
const full = path12.join(dir, e.name);
|
|
5051
|
+
const rel = path12.relative(projectRoot, full).replace(/\\/g, "/");
|
|
5049
5052
|
if (e.isDirectory()) {
|
|
5050
5053
|
if (isGitIgnored(rel, true)) continue;
|
|
5051
5054
|
await walk2(full);
|
|
5052
5055
|
} else if (e.isFile()) {
|
|
5053
5056
|
if (isGitIgnored(rel, false)) continue;
|
|
5054
|
-
const ext =
|
|
5057
|
+
const ext = path12.extname(e.name);
|
|
5055
5058
|
for (const { ext: extName, pat } of globs) {
|
|
5056
5059
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
5057
5060
|
results.push(full);
|
|
@@ -5107,7 +5110,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5107
5110
|
let files;
|
|
5108
5111
|
let discoveredFiles = null;
|
|
5109
5112
|
if (opts.files && opts.files.length > 0) {
|
|
5110
|
-
files = opts.files.map((f) =>
|
|
5113
|
+
files = opts.files.map((f) => path12.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path12.relative(projectRoot, f).replace(/\\/g, "/"), false));
|
|
5111
5114
|
} else {
|
|
5112
5115
|
files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
5113
5116
|
discoveredFiles = new Set(files);
|
|
@@ -5135,34 +5138,34 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5135
5138
|
const statOpts = signal ? { signal } : {};
|
|
5136
5139
|
const statReadParse = await Promise.allSettled(
|
|
5137
5140
|
batchFiles.map(async (file) => {
|
|
5138
|
-
let
|
|
5141
|
+
let stat13;
|
|
5139
5142
|
try {
|
|
5140
|
-
|
|
5143
|
+
stat13 = await fs2.stat(file, statOpts);
|
|
5141
5144
|
} catch (e) {
|
|
5142
5145
|
if (isAbortError(e)) throw e;
|
|
5143
5146
|
return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
|
|
5144
5147
|
}
|
|
5145
|
-
if (!
|
|
5148
|
+
if (!stat13.isFile()) return { file, stat: stat13, lang: "", parsed: null };
|
|
5146
5149
|
const lang = detectLang(file);
|
|
5147
|
-
if (!lang) return { file, stat:
|
|
5150
|
+
if (!lang) return { file, stat: stat13, lang: "", parsed: null };
|
|
5148
5151
|
const meta = existingMeta.get(file);
|
|
5149
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
5150
|
-
return { file, stat:
|
|
5152
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat13.mtimeMs)) {
|
|
5153
|
+
return { file, stat: stat13, lang, parsed: null, skippedMeta: meta };
|
|
5151
5154
|
}
|
|
5152
5155
|
let content;
|
|
5153
5156
|
try {
|
|
5154
5157
|
content = await fs2.readFile(file, { encoding: "utf8", signal });
|
|
5155
5158
|
} catch (e) {
|
|
5156
5159
|
if (isAbortError(e)) throw e;
|
|
5157
|
-
return { file, stat:
|
|
5160
|
+
return { file, stat: stat13, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
|
|
5158
5161
|
}
|
|
5159
5162
|
let parsed;
|
|
5160
5163
|
try {
|
|
5161
5164
|
parsed = await parseFile(file, content, lang);
|
|
5162
5165
|
} catch (e) {
|
|
5163
|
-
return { file, stat:
|
|
5166
|
+
return { file, stat: stat13, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
|
|
5164
5167
|
}
|
|
5165
|
-
return { file, stat:
|
|
5168
|
+
return { file, stat: stat13, lang, parsed, content };
|
|
5166
5169
|
})
|
|
5167
5170
|
);
|
|
5168
5171
|
const batchEntries = [];
|
|
@@ -5182,7 +5185,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5182
5185
|
if (result.error.includes("error:")) errors.push(result.error);
|
|
5183
5186
|
continue;
|
|
5184
5187
|
}
|
|
5185
|
-
const { stat:
|
|
5188
|
+
const { stat: stat13, lang, parsed } = result;
|
|
5186
5189
|
if (result.skippedMeta) {
|
|
5187
5190
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
5188
5191
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -5194,7 +5197,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5194
5197
|
store.upsertFile({
|
|
5195
5198
|
file,
|
|
5196
5199
|
lang,
|
|
5197
|
-
mtimeMs: Math.floor(
|
|
5200
|
+
mtimeMs: Math.floor(stat13.mtimeMs),
|
|
5198
5201
|
symbolCount: 0,
|
|
5199
5202
|
lastIndexed: Date.now()
|
|
5200
5203
|
});
|
|
@@ -5206,7 +5209,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5206
5209
|
store.upsertFile({
|
|
5207
5210
|
file,
|
|
5208
5211
|
lang,
|
|
5209
|
-
mtimeMs: Math.floor(
|
|
5212
|
+
mtimeMs: Math.floor(stat13.mtimeMs),
|
|
5210
5213
|
symbolCount: 0,
|
|
5211
5214
|
lastIndexed: Date.now()
|
|
5212
5215
|
});
|
|
@@ -5222,7 +5225,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5222
5225
|
lang,
|
|
5223
5226
|
symbols: parsed.symbols,
|
|
5224
5227
|
refs,
|
|
5225
|
-
mtimeMs: Math.floor(
|
|
5228
|
+
mtimeMs: Math.floor(stat13.mtimeMs),
|
|
5226
5229
|
symbolCount: parsed.symbols.length
|
|
5227
5230
|
});
|
|
5228
5231
|
deleteForFiles.push(file);
|
|
@@ -5371,8 +5374,8 @@ function statsService(args) {
|
|
|
5371
5374
|
}
|
|
5372
5375
|
|
|
5373
5376
|
// src/codebase-index/background-indexer.ts
|
|
5374
|
-
var DEFAULT_FULL_INDEX_TIMEOUT_MS =
|
|
5375
|
-
var DEFAULT_INCREMENTAL_TIMEOUT_MS =
|
|
5377
|
+
var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
|
|
5378
|
+
var DEFAULT_INCREMENTAL_TIMEOUT_MS = 6e4;
|
|
5376
5379
|
var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
|
|
5377
5380
|
var _ready = false;
|
|
5378
5381
|
var _indexing = false;
|
|
@@ -5887,6 +5890,25 @@ var codebaseStatsTool = {
|
|
|
5887
5890
|
};
|
|
5888
5891
|
}
|
|
5889
5892
|
};
|
|
5893
|
+
async function resolveReal(p) {
|
|
5894
|
+
const resolved = path12.resolve(p);
|
|
5895
|
+
let probe = resolved;
|
|
5896
|
+
const missing = [];
|
|
5897
|
+
for (; ; ) {
|
|
5898
|
+
try {
|
|
5899
|
+
return path12.resolve(await fs2.realpath(probe), ...missing);
|
|
5900
|
+
} catch (err) {
|
|
5901
|
+
if (err.code === "ENOENT") {
|
|
5902
|
+
const parent = path12.dirname(probe);
|
|
5903
|
+
if (parent === probe) return resolved;
|
|
5904
|
+
missing.unshift(path12.basename(probe));
|
|
5905
|
+
probe = parent;
|
|
5906
|
+
continue;
|
|
5907
|
+
}
|
|
5908
|
+
return resolved;
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5890
5912
|
function normalizeOverrides(set) {
|
|
5891
5913
|
const out = {};
|
|
5892
5914
|
if (set && typeof set === "object") {
|
|
@@ -6044,10 +6066,12 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
|
|
|
6044
6066
|
kitId: active.kit,
|
|
6045
6067
|
outPath: input.out
|
|
6046
6068
|
});
|
|
6047
|
-
const root =
|
|
6048
|
-
const
|
|
6049
|
-
const
|
|
6050
|
-
|
|
6069
|
+
const root = await resolveReal(ctx.projectRoot);
|
|
6070
|
+
const absResolved = path12.isAbsolute(result.path) ? path12.resolve(result.path) : path12.resolve(path12.join(ctx.projectRoot, result.path));
|
|
6071
|
+
const absParent = await resolveReal(path12.dirname(absResolved));
|
|
6072
|
+
const abs = path12.join(absParent, path12.basename(absResolved));
|
|
6073
|
+
const rel = path12.relative(root, abs);
|
|
6074
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) {
|
|
6051
6075
|
throw new Error(
|
|
6052
6076
|
`design: materialize path "${result.path}" would escape the project root`
|
|
6053
6077
|
);
|
|
@@ -6071,7 +6095,7 @@ ${result.content}
|
|
|
6071
6095
|
\`\`\``
|
|
6072
6096
|
};
|
|
6073
6097
|
}
|
|
6074
|
-
await fs2.mkdir(
|
|
6098
|
+
await fs2.mkdir(path12.dirname(abs), { recursive: true });
|
|
6075
6099
|
await fs2.writeFile(abs, result.content);
|
|
6076
6100
|
return {
|
|
6077
6101
|
action,
|
|
@@ -6199,11 +6223,11 @@ function findGitDir(cwd) {
|
|
|
6199
6223
|
let dir = cwd;
|
|
6200
6224
|
for (let i = 0; i < 20; i++) {
|
|
6201
6225
|
try {
|
|
6202
|
-
const
|
|
6203
|
-
if (
|
|
6226
|
+
const stat13 = statSync(path12.join(dir, ".git"));
|
|
6227
|
+
if (stat13.isDirectory()) return dir;
|
|
6204
6228
|
} catch {
|
|
6205
6229
|
}
|
|
6206
|
-
const parent =
|
|
6230
|
+
const parent = path12.dirname(dir);
|
|
6207
6231
|
if (parent === dir) break;
|
|
6208
6232
|
dir = parent;
|
|
6209
6233
|
}
|
|
@@ -6244,8 +6268,8 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
6244
6268
|
const results = [];
|
|
6245
6269
|
for (const file of files) {
|
|
6246
6270
|
const absPath = safeResolve(file, ctx);
|
|
6247
|
-
const
|
|
6248
|
-
if (!
|
|
6271
|
+
const stat13 = await fs2.stat(absPath).catch(() => null);
|
|
6272
|
+
if (!stat13?.isFile()) continue;
|
|
6249
6273
|
const content = await fs2.readFile(absPath, "utf8");
|
|
6250
6274
|
const lines = content.split(/\r?\n/);
|
|
6251
6275
|
results.push(formatWithLineNumbers(file, lines));
|
|
@@ -6342,8 +6366,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
6342
6366
|
for (const f of files) {
|
|
6343
6367
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
6344
6368
|
try {
|
|
6345
|
-
const
|
|
6346
|
-
if (
|
|
6369
|
+
const stat13 = await fs2.stat(absPath);
|
|
6370
|
+
if (stat13.isFile()) resolved.push(absPath);
|
|
6347
6371
|
} catch {
|
|
6348
6372
|
}
|
|
6349
6373
|
}
|
|
@@ -6409,11 +6433,303 @@ function processFile(content, absPath, _style, _overwrite, target) {
|
|
|
6409
6433
|
}
|
|
6410
6434
|
return results;
|
|
6411
6435
|
}
|
|
6436
|
+
|
|
6437
|
+
// src/_edit-match.ts
|
|
6438
|
+
var TIER_LABEL = {
|
|
6439
|
+
exact: "exact match",
|
|
6440
|
+
"trailing-whitespace": "whitespace-normalized match (trailing whitespace ignored)",
|
|
6441
|
+
"whitespace-normalized": "whitespace-normalized match (indentation ignored, replacement re-indented)",
|
|
6442
|
+
fuzzy: "fuzzy match (block anchors exact, interior \u226590% similar)"
|
|
6443
|
+
};
|
|
6444
|
+
var TIER_CONFIDENCE = {
|
|
6445
|
+
exact: "exact",
|
|
6446
|
+
"trailing-whitespace": "high",
|
|
6447
|
+
"whitespace-normalized": "medium",
|
|
6448
|
+
fuzzy: "low"
|
|
6449
|
+
};
|
|
6450
|
+
var MIN_NORMALIZED_NEEDLE_CHARS = 16;
|
|
6451
|
+
var FUZZY_MIN_SIMILARITY = 0.9;
|
|
6452
|
+
var FUZZY_AMBIGUITY_MARGIN = 0.05;
|
|
6453
|
+
var FUZZY_MAX_INTERIOR_CHARS = 2e3;
|
|
6454
|
+
function findLadderMatches(fileLf, oldLf) {
|
|
6455
|
+
const exact = [];
|
|
6456
|
+
let idx = fileLf.indexOf(oldLf);
|
|
6457
|
+
while (idx !== -1) {
|
|
6458
|
+
exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });
|
|
6459
|
+
idx = fileLf.indexOf(oldLf, idx + 1);
|
|
6460
|
+
}
|
|
6461
|
+
if (exact.length > 0) return { tier: "exact", matches: exact };
|
|
6462
|
+
const fileLines = fileLf.split("\n");
|
|
6463
|
+
const needleLines = oldLf.split("\n");
|
|
6464
|
+
if (needleLines.length > fileLines.length) return void 0;
|
|
6465
|
+
const offsets = lineOffsets(fileLines);
|
|
6466
|
+
const trailing = windowScan(
|
|
6467
|
+
fileLines,
|
|
6468
|
+
needleLines,
|
|
6469
|
+
offsets,
|
|
6470
|
+
(a, b) => a.trimEnd() === b.trimEnd()
|
|
6471
|
+
);
|
|
6472
|
+
if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
|
|
6473
|
+
const normalizedLen = needleLines.reduce((n, l) => n + l.trim().length, 0);
|
|
6474
|
+
if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
|
|
6475
|
+
const normalized = windowScan(fileLines, needleLines, offsets, (a, b) => a.trim() === b.trim());
|
|
6476
|
+
if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
|
|
6477
|
+
return fuzzyScan(fileLines, needleLines, offsets);
|
|
6478
|
+
}
|
|
6479
|
+
function lineAt(text, pos) {
|
|
6480
|
+
let line = 1;
|
|
6481
|
+
for (let i = 0; i < pos; i++) {
|
|
6482
|
+
if (text.charCodeAt(i) === 10) line++;
|
|
6483
|
+
}
|
|
6484
|
+
return line;
|
|
6485
|
+
}
|
|
6486
|
+
function lineOffsets(lines) {
|
|
6487
|
+
const out = new Array(lines.length);
|
|
6488
|
+
let pos = 0;
|
|
6489
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6490
|
+
out[i] = pos;
|
|
6491
|
+
pos += lines[i].length + 1;
|
|
6492
|
+
}
|
|
6493
|
+
return out;
|
|
6494
|
+
}
|
|
6495
|
+
function windowToMatch(fileLines, offsets, start, windowLen) {
|
|
6496
|
+
const lastLine = start + windowLen - 1;
|
|
6497
|
+
return {
|
|
6498
|
+
start: offsets[start],
|
|
6499
|
+
end: offsets[lastLine] + fileLines[lastLine].length,
|
|
6500
|
+
startLine: start + 1
|
|
6501
|
+
};
|
|
6502
|
+
}
|
|
6503
|
+
function windowScan(fileLines, needleLines, offsets, eq) {
|
|
6504
|
+
const n = needleLines.length;
|
|
6505
|
+
const out = [];
|
|
6506
|
+
for (let i = 0; i + n <= fileLines.length; i++) {
|
|
6507
|
+
let all = true;
|
|
6508
|
+
for (let j = 0; j < n; j++) {
|
|
6509
|
+
if (!eq(fileLines[i + j], needleLines[j])) {
|
|
6510
|
+
all = false;
|
|
6511
|
+
break;
|
|
6512
|
+
}
|
|
6513
|
+
}
|
|
6514
|
+
if (all) {
|
|
6515
|
+
out.push(windowToMatch(fileLines, offsets, i, n));
|
|
6516
|
+
i += n - 1;
|
|
6517
|
+
}
|
|
6518
|
+
}
|
|
6519
|
+
return out;
|
|
6520
|
+
}
|
|
6521
|
+
function fuzzyScan(fileLines, needleLines, offsets) {
|
|
6522
|
+
const n = needleLines.length;
|
|
6523
|
+
if (n < 3) return void 0;
|
|
6524
|
+
const firstNeedle = needleLines[0].trim();
|
|
6525
|
+
const lastNeedle = needleLines[n - 1].trim();
|
|
6526
|
+
const needleInterior = needleLines.slice(1, -1).map((l) => l.trim()).join("\n");
|
|
6527
|
+
if (needleInterior.length > FUZZY_MAX_INTERIOR_CHARS) return void 0;
|
|
6528
|
+
const candidates = [];
|
|
6529
|
+
for (let i = 0; i + n <= fileLines.length; i++) {
|
|
6530
|
+
if (fileLines[i].trim() !== firstNeedle) continue;
|
|
6531
|
+
if (fileLines[i + n - 1].trim() !== lastNeedle) continue;
|
|
6532
|
+
const windowInterior = fileLines.slice(i + 1, i + n - 1).map((l) => l.trim()).join("\n");
|
|
6533
|
+
if (windowInterior.length > FUZZY_MAX_INTERIOR_CHARS) continue;
|
|
6534
|
+
const score = similarity(needleInterior, windowInterior);
|
|
6535
|
+
if (score >= FUZZY_MIN_SIMILARITY) {
|
|
6536
|
+
candidates.push({ match: windowToMatch(fileLines, offsets, i, n), score });
|
|
6537
|
+
}
|
|
6538
|
+
}
|
|
6539
|
+
if (candidates.length === 0) return void 0;
|
|
6540
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
6541
|
+
const best = candidates[0];
|
|
6542
|
+
const runnerUp = candidates[1];
|
|
6543
|
+
if (runnerUp && best.score - runnerUp.score < FUZZY_AMBIGUITY_MARGIN) {
|
|
6544
|
+
return {
|
|
6545
|
+
tier: "fuzzy",
|
|
6546
|
+
matches: candidates.map((c) => c.match),
|
|
6547
|
+
score: best.score,
|
|
6548
|
+
ambiguous: true
|
|
6549
|
+
};
|
|
6550
|
+
}
|
|
6551
|
+
return { tier: "fuzzy", matches: [best.match], score: best.score };
|
|
6552
|
+
}
|
|
6553
|
+
function similarity(a, b) {
|
|
6554
|
+
if (a === b) return 1;
|
|
6555
|
+
const max = Math.max(a.length, b.length);
|
|
6556
|
+
if (max === 0) return 1;
|
|
6557
|
+
if (Math.abs(a.length - b.length) / max > 1 - FUZZY_MIN_SIMILARITY + 0.05) {
|
|
6558
|
+
return 1 - Math.abs(a.length - b.length) / max;
|
|
6559
|
+
}
|
|
6560
|
+
return 1 - levenshtein(a, b) / max;
|
|
6561
|
+
}
|
|
6562
|
+
function levenshtein(a, b) {
|
|
6563
|
+
if (a.length === 0) return b.length;
|
|
6564
|
+
if (b.length === 0) return a.length;
|
|
6565
|
+
let prev = new Array(b.length + 1);
|
|
6566
|
+
let cur = new Array(b.length + 1);
|
|
6567
|
+
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
6568
|
+
for (let i = 1; i <= a.length; i++) {
|
|
6569
|
+
cur[0] = i;
|
|
6570
|
+
const ca = a.charCodeAt(i - 1);
|
|
6571
|
+
for (let j = 1; j <= b.length; j++) {
|
|
6572
|
+
const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
|
|
6573
|
+
cur[j] = Math.min(
|
|
6574
|
+
prev[j] + 1,
|
|
6575
|
+
cur[j - 1] + 1,
|
|
6576
|
+
prev[j - 1] + cost
|
|
6577
|
+
);
|
|
6578
|
+
}
|
|
6579
|
+
[prev, cur] = [cur, prev];
|
|
6580
|
+
}
|
|
6581
|
+
return prev[b.length];
|
|
6582
|
+
}
|
|
6583
|
+
function firstLineIndent(text) {
|
|
6584
|
+
for (const line of text.split("\n")) {
|
|
6585
|
+
if (line.trim() === "") continue;
|
|
6586
|
+
return /^[ \t]*/.exec(line)[0];
|
|
6587
|
+
}
|
|
6588
|
+
return "";
|
|
6589
|
+
}
|
|
6590
|
+
function adjustIndent(newLf, fromIndent, toIndent) {
|
|
6591
|
+
if (fromIndent === toIndent) return { text: newLf, adjusted: false };
|
|
6592
|
+
const lines = newLf.split("\n");
|
|
6593
|
+
if (toIndent.startsWith(fromIndent)) {
|
|
6594
|
+
const extra = toIndent.slice(fromIndent.length);
|
|
6595
|
+
return {
|
|
6596
|
+
text: lines.map((l) => l.trim() === "" ? l : extra + l).join("\n"),
|
|
6597
|
+
adjusted: true
|
|
6598
|
+
};
|
|
6599
|
+
}
|
|
6600
|
+
if (fromIndent.startsWith(toIndent)) {
|
|
6601
|
+
const remove = fromIndent.slice(toIndent.length);
|
|
6602
|
+
return {
|
|
6603
|
+
text: lines.map((l) => l.startsWith(remove) ? l.slice(remove.length) : l).join("\n"),
|
|
6604
|
+
adjusted: true
|
|
6605
|
+
};
|
|
6606
|
+
}
|
|
6607
|
+
return { text: newLf, adjusted: false };
|
|
6608
|
+
}
|
|
6609
|
+
function nearestMatchHint(fileLf, oldLf) {
|
|
6610
|
+
const fileLines = fileLf.split("\n");
|
|
6611
|
+
const needleLines = oldLf.split("\n");
|
|
6612
|
+
if (needleLines.length > fileLines.length) return void 0;
|
|
6613
|
+
const needleTrimmed = needleLines.map((l) => l.trim());
|
|
6614
|
+
let bestScore = 0;
|
|
6615
|
+
let bestStart = -1;
|
|
6616
|
+
for (let i = 0; i + needleLines.length <= fileLines.length; i++) {
|
|
6617
|
+
let sum = 0;
|
|
6618
|
+
for (let j = 0; j < needleLines.length; j++) {
|
|
6619
|
+
sum += prefixSimilarity(needleTrimmed[j], fileLines[i + j].trim());
|
|
6620
|
+
}
|
|
6621
|
+
const score = sum / needleLines.length;
|
|
6622
|
+
if (score > bestScore) {
|
|
6623
|
+
bestScore = score;
|
|
6624
|
+
bestStart = i;
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
if (bestStart === -1 || bestScore < 0.5) return void 0;
|
|
6628
|
+
const snippet = fileLines.slice(bestStart, bestStart + Math.min(3, needleLines.length)).map((l) => l.length > 120 ? `${l.slice(0, 120)}\u2026` : l).join("\n");
|
|
6629
|
+
return { line: bestStart + 1, snippet };
|
|
6630
|
+
}
|
|
6631
|
+
function prefixSimilarity(a, b) {
|
|
6632
|
+
if (a === b) return 1;
|
|
6633
|
+
const max = Math.max(a.length, b.length);
|
|
6634
|
+
if (max === 0) return 1;
|
|
6635
|
+
let p = 0;
|
|
6636
|
+
const lim = Math.min(a.length, b.length);
|
|
6637
|
+
while (p < lim && a.charCodeAt(p) === b.charCodeAt(p)) p++;
|
|
6638
|
+
return p / max;
|
|
6639
|
+
}
|
|
6640
|
+
var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
6641
|
+
var MAX_CHECK_CHARS = 15e5;
|
|
6642
|
+
var MAX_ERRORS = 5;
|
|
6643
|
+
var tsLoad = null;
|
|
6644
|
+
function loadTypescript() {
|
|
6645
|
+
tsLoad ??= import('typescript').then(
|
|
6646
|
+
(m) => m.default ?? m,
|
|
6647
|
+
() => null
|
|
6648
|
+
);
|
|
6649
|
+
return tsLoad;
|
|
6650
|
+
}
|
|
6651
|
+
async function checkSyntax(filePath, content, previousContent) {
|
|
6652
|
+
const errors = await parseErrors(filePath, content);
|
|
6653
|
+
if (errors === void 0) return void 0;
|
|
6654
|
+
if (errors.length === 0) return { errors, preExisting: false };
|
|
6655
|
+
let preExisting = false;
|
|
6656
|
+
if (previousContent !== void 0) {
|
|
6657
|
+
const prevErrors = await parseErrors(filePath, previousContent);
|
|
6658
|
+
preExisting = prevErrors !== void 0 && prevErrors.length > 0;
|
|
6659
|
+
}
|
|
6660
|
+
return { errors, preExisting };
|
|
6661
|
+
}
|
|
6662
|
+
function isJsoncFile(filePath) {
|
|
6663
|
+
const base = path12.basename(filePath).toLowerCase();
|
|
6664
|
+
if (base.endsWith(".jsonc")) return true;
|
|
6665
|
+
if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
|
|
6666
|
+
const dir = path12.basename(path12.dirname(filePath)).toLowerCase();
|
|
6667
|
+
return dir === ".vscode";
|
|
6668
|
+
}
|
|
6669
|
+
async function parseErrors(filePath, content) {
|
|
6670
|
+
if (content.length > MAX_CHECK_CHARS) return void 0;
|
|
6671
|
+
const ext = path12.extname(filePath).toLowerCase();
|
|
6672
|
+
if (ext === ".json" || ext === ".jsonc") {
|
|
6673
|
+
try {
|
|
6674
|
+
JSON.parse(content);
|
|
6675
|
+
return [];
|
|
6676
|
+
} catch (err) {
|
|
6677
|
+
if (isJsoncFile(filePath)) {
|
|
6678
|
+
const ts3 = await loadTypescript();
|
|
6679
|
+
if (ts3) {
|
|
6680
|
+
const jsonc = ts3.parseConfigFileTextToJson(filePath, content);
|
|
6681
|
+
if (!jsonc.error) return [];
|
|
6682
|
+
return [formatDiag(ts3, jsonc.error, content)];
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
return [`JSON parse error: ${err.message}`];
|
|
6686
|
+
}
|
|
6687
|
+
}
|
|
6688
|
+
if (!TS_LIKE.has(ext)) return void 0;
|
|
6689
|
+
const ts2 = await loadTypescript();
|
|
6690
|
+
if (!ts2) return void 0;
|
|
6691
|
+
const scriptKind = ext === ".tsx" ? ts2.ScriptKind.TSX : ext === ".ts" || ext === ".mts" || ext === ".cts" ? ts2.ScriptKind.TS : (
|
|
6692
|
+
// Plain JS may legitimately contain JSX; the JSX grammar is a
|
|
6693
|
+
// superset for untyped code, so parsing .js/.jsx as JSX avoids
|
|
6694
|
+
// false positives on React files.
|
|
6695
|
+
ts2.ScriptKind.JSX
|
|
6696
|
+
);
|
|
6697
|
+
const sourceFile = ts2.createSourceFile(
|
|
6698
|
+
path12.basename(filePath),
|
|
6699
|
+
content,
|
|
6700
|
+
ts2.ScriptTarget.Latest,
|
|
6701
|
+
/* setParentNodes */
|
|
6702
|
+
false,
|
|
6703
|
+
scriptKind
|
|
6704
|
+
);
|
|
6705
|
+
const diags = sourceFile.parseDiagnostics ?? [];
|
|
6706
|
+
return diags.slice(0, MAX_ERRORS).map((d) => formatDiag(ts2, d, content, sourceFile));
|
|
6707
|
+
}
|
|
6708
|
+
function formatDiag(ts2, diag, content, sourceFile) {
|
|
6709
|
+
const message = ts2.flattenDiagnosticMessageText(diag.messageText, " ");
|
|
6710
|
+
if (diag.start === void 0) return message;
|
|
6711
|
+
let line;
|
|
6712
|
+
if (sourceFile) {
|
|
6713
|
+
line = sourceFile.getLineAndCharacterOfPosition(diag.start).line + 1;
|
|
6714
|
+
} else {
|
|
6715
|
+
line = 1;
|
|
6716
|
+
for (let i = 0; i < diag.start && i < content.length; i++) {
|
|
6717
|
+
if (content.charCodeAt(i) === 10) line++;
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
return `line ${line}: ${message}`;
|
|
6721
|
+
}
|
|
6722
|
+
|
|
6723
|
+
// src/edit.ts
|
|
6412
6724
|
var editTool = {
|
|
6413
6725
|
name: "edit",
|
|
6414
6726
|
category: "Filesystem",
|
|
6415
6727
|
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.",
|
|
6416
|
-
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.",
|
|
6728
|
+
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.\nIf `old_string` differs from the file only in whitespace (trailing spaces, indentation), a lower-confidence fallback match is applied and reported in `matched_by`/`note` \u2014 always verify the diff when this happens.\nAfter the edit, TS/JS/JSON files are syntax-checked; if `syntax_errors` is present in the output, fix those errors immediately with a follow-up edit.",
|
|
6729
|
+
selection: {
|
|
6730
|
+
doNotUseWhen: "creating a new file, replacing the whole file, or applying an existing unified diff.",
|
|
6731
|
+
useInstead: ["write", "patch"]
|
|
6732
|
+
},
|
|
6417
6733
|
permission: "confirm",
|
|
6418
6734
|
mutating: true,
|
|
6419
6735
|
capabilities: ["fs.write"],
|
|
@@ -6429,7 +6745,7 @@ var editTool = {
|
|
|
6429
6745
|
},
|
|
6430
6746
|
required: ["path", "old_string", "new_string"]
|
|
6431
6747
|
},
|
|
6432
|
-
async execute(input, ctx) {
|
|
6748
|
+
async execute(input, ctx, opts) {
|
|
6433
6749
|
if (!input?.path) {
|
|
6434
6750
|
throw new ToolValidationError({ message: "edit: path is required", field: "path" });
|
|
6435
6751
|
}
|
|
@@ -6452,7 +6768,7 @@ var editTool = {
|
|
|
6452
6768
|
});
|
|
6453
6769
|
}
|
|
6454
6770
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
6455
|
-
const
|
|
6771
|
+
const stat13 = await fs2.stat(absPath).catch((err) => {
|
|
6456
6772
|
if (err.code === "ENOENT") {
|
|
6457
6773
|
throw new ToolValidationError({
|
|
6458
6774
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
@@ -6462,7 +6778,7 @@ var editTool = {
|
|
|
6462
6778
|
}
|
|
6463
6779
|
throw err;
|
|
6464
6780
|
});
|
|
6465
|
-
if (!
|
|
6781
|
+
if (!stat13.isFile()) {
|
|
6466
6782
|
throw new ToolValidationError({
|
|
6467
6783
|
message: `edit: "${input.path}" is not a regular file`,
|
|
6468
6784
|
field: "path"
|
|
@@ -6472,15 +6788,27 @@ var editTool = {
|
|
|
6472
6788
|
const original = await fs2.readFile(absPath, "utf8");
|
|
6473
6789
|
const updated = await fs2.stat(absPath);
|
|
6474
6790
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
6475
|
-
const
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6791
|
+
const originalHash = sha256hex(original);
|
|
6792
|
+
const lastReadHash = ctx.lastReadHash?.(absPath);
|
|
6793
|
+
if (lastReadHash !== void 0) {
|
|
6794
|
+
if (lastReadHash !== originalHash) {
|
|
6795
|
+
throw new ToolValidationError({
|
|
6796
|
+
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
6797
|
+
field: "path",
|
|
6798
|
+
context: { reason: "external_modification" }
|
|
6799
|
+
});
|
|
6800
|
+
}
|
|
6801
|
+
} else {
|
|
6802
|
+
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
6803
|
+
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
6804
|
+
throw new ToolValidationError({
|
|
6805
|
+
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
6806
|
+
field: "path",
|
|
6807
|
+
context: { reason: "external_modification" }
|
|
6808
|
+
});
|
|
6809
|
+
}
|
|
6482
6810
|
}
|
|
6483
|
-
if (autoRead && updated.mtimeMs >
|
|
6811
|
+
if (autoRead && updated.mtimeMs > stat13.mtimeMs + mtimeTolerance) {
|
|
6484
6812
|
throw new ToolValidationError({
|
|
6485
6813
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
6486
6814
|
field: "path",
|
|
@@ -6493,42 +6821,78 @@ var editTool = {
|
|
|
6493
6821
|
const oldLf = normalizeToLf(input.old_string);
|
|
6494
6822
|
const newLf = normalizeToLf(input.new_string);
|
|
6495
6823
|
if (oldLf === newLf) {
|
|
6496
|
-
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs);
|
|
6824
|
+
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
|
|
6497
6825
|
return {
|
|
6498
6826
|
path: absPath,
|
|
6499
6827
|
replacements: 0,
|
|
6500
6828
|
diff: "(no-op: old and new are identical)",
|
|
6829
|
+
matched_by: "exact",
|
|
6501
6830
|
note: autoReadNote
|
|
6502
6831
|
};
|
|
6503
6832
|
}
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
while (idx !== -1) {
|
|
6508
|
-
matches.push(idx);
|
|
6509
|
-
count++;
|
|
6510
|
-
idx = fileLf.indexOf(oldLf, idx + 1);
|
|
6511
|
-
}
|
|
6512
|
-
if (count === 0) {
|
|
6513
|
-
const hint = findSimilarity(fileLf, oldLf);
|
|
6833
|
+
const ladder = findLadderMatches(fileLf, oldLf);
|
|
6834
|
+
if (!ladder) {
|
|
6835
|
+
const hint = nearestMatchHint(fileLf, oldLf);
|
|
6514
6836
|
throw new ToolValidationError({
|
|
6515
|
-
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}
|
|
6837
|
+
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
|
|
6838
|
+
${hint.snippet}
|
|
6839
|
+
Compare this against your old_string and retry with the file's actual text.` : ""}`,
|
|
6516
6840
|
field: "old_string"
|
|
6517
6841
|
});
|
|
6518
6842
|
}
|
|
6843
|
+
const { tier, matches } = ladder;
|
|
6844
|
+
const count = matches.length;
|
|
6845
|
+
if (ladder.ambiguous) {
|
|
6846
|
+
const lines = matches.map((m) => m.startLine);
|
|
6847
|
+
throw new ToolValidationError({
|
|
6848
|
+
message: `edit: old_string only matched fuzzily and ${count} candidate blocks scored too close to distinguish (lines: ${lines.join(", ")}) in "${input.path}". Re-read the file and use the exact text of the intended block.`,
|
|
6849
|
+
field: "old_string",
|
|
6850
|
+
context: { occurrences: count, matchTier: tier }
|
|
6851
|
+
});
|
|
6852
|
+
}
|
|
6853
|
+
if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
|
|
6854
|
+
throw new ToolValidationError({
|
|
6855
|
+
message: `edit: old_string only matched via ${TIER_LABEL[tier]} in "${input.path}", but replace_all requires an exact (or trailing-whitespace) match. Re-read the file and use its exact text.`,
|
|
6856
|
+
field: "old_string",
|
|
6857
|
+
context: { matchTier: tier }
|
|
6858
|
+
});
|
|
6859
|
+
}
|
|
6519
6860
|
if (count > 1 && !input.replace_all) {
|
|
6520
|
-
const lines =
|
|
6861
|
+
const lines = matches.map((m) => m.startLine);
|
|
6521
6862
|
throw new ToolValidationError({
|
|
6522
|
-
message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true.`,
|
|
6863
|
+
message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")})${tier === "exact" ? "" : ` via ${TIER_LABEL[tier]}`}. Add more context to make it unique, or set replace_all: true.`,
|
|
6523
6864
|
field: "old_string",
|
|
6524
|
-
context: { occurrences: count }
|
|
6865
|
+
context: { occurrences: count, matchTier: tier }
|
|
6525
6866
|
});
|
|
6526
6867
|
}
|
|
6527
|
-
|
|
6868
|
+
let newFileLf;
|
|
6869
|
+
let tierNote;
|
|
6870
|
+
if (tier === "exact") {
|
|
6871
|
+
newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
6872
|
+
} else {
|
|
6873
|
+
let replacement = newLf;
|
|
6874
|
+
let indentSuffix = "";
|
|
6875
|
+
if (tier === "whitespace-normalized" || tier === "fuzzy") {
|
|
6876
|
+
const first = matches[0];
|
|
6877
|
+
const matchedText = fileLf.slice(first.start, first.end);
|
|
6878
|
+
const adjusted = adjustIndent(newLf, firstLineIndent(oldLf), firstLineIndent(matchedText));
|
|
6879
|
+
replacement = adjusted.text;
|
|
6880
|
+
if (adjusted.adjusted) indentSuffix = "; replacement re-indented to match the file";
|
|
6881
|
+
}
|
|
6882
|
+
newFileLf = fileLf;
|
|
6883
|
+
const applied = input.replace_all ? matches : matches.slice(0, 1);
|
|
6884
|
+
for (let i = applied.length - 1; i >= 0; i--) {
|
|
6885
|
+
const m = applied[i];
|
|
6886
|
+
newFileLf = newFileLf.slice(0, m.start) + replacement + newFileLf.slice(m.end);
|
|
6887
|
+
}
|
|
6888
|
+
const scoreSuffix = ladder.score !== void 0 ? `, similarity ${(ladder.score * 100).toFixed(1)}%` : "";
|
|
6889
|
+
tierNote = `old_string did not match exactly; applied via ${TIER_LABEL[tier]} at line ${matches[0].startLine} (confidence: ${TIER_CONFIDENCE[tier]}${scoreSuffix}${indentSuffix}). Review the diff to confirm the intended target was edited.`;
|
|
6890
|
+
}
|
|
6528
6891
|
const newFile = toStyle(newFileLf, style);
|
|
6892
|
+
opts?.signal?.throwIfAborted();
|
|
6529
6893
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
6530
6894
|
const written = await fs2.stat(absPath);
|
|
6531
|
-
ctx.recordRead(absPath, written.mtimeMs, "write");
|
|
6895
|
+
ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
|
|
6532
6896
|
ctx.session.recordFileChange({
|
|
6533
6897
|
path: absPath,
|
|
6534
6898
|
action: "modified",
|
|
@@ -6539,38 +6903,22 @@ var editTool = {
|
|
|
6539
6903
|
fromFile: input.path,
|
|
6540
6904
|
toFile: input.path
|
|
6541
6905
|
});
|
|
6906
|
+
const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
|
|
6907
|
+
let syntaxNote;
|
|
6908
|
+
if (syntax && syntax.errors.length > 0) {
|
|
6909
|
+
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.`;
|
|
6910
|
+
}
|
|
6911
|
+
const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
|
|
6542
6912
|
return {
|
|
6543
6913
|
path: absPath,
|
|
6544
6914
|
replacements: input.replace_all ? count : 1,
|
|
6545
6915
|
diff,
|
|
6546
|
-
|
|
6916
|
+
matched_by: tier,
|
|
6917
|
+
syntax_errors: syntax && syntax.errors.length > 0 ? syntax.errors : void 0,
|
|
6918
|
+
note: notes.length > 0 ? notes.join("\n") : void 0
|
|
6547
6919
|
};
|
|
6548
6920
|
}
|
|
6549
6921
|
};
|
|
6550
|
-
function lineNumbersFor(text, indices) {
|
|
6551
|
-
const out = [];
|
|
6552
|
-
let pos = 0;
|
|
6553
|
-
let line = 1;
|
|
6554
|
-
for (const target of indices) {
|
|
6555
|
-
while (pos < target) {
|
|
6556
|
-
if (text.charCodeAt(pos) === 10) line++;
|
|
6557
|
-
pos++;
|
|
6558
|
-
}
|
|
6559
|
-
out.push(line);
|
|
6560
|
-
}
|
|
6561
|
-
return out;
|
|
6562
|
-
}
|
|
6563
|
-
function findSimilarity(haystack, needle) {
|
|
6564
|
-
if (needle.length < 20) return void 0;
|
|
6565
|
-
const probe = needle.slice(0, Math.min(40, needle.length));
|
|
6566
|
-
const idx = haystack.indexOf(probe);
|
|
6567
|
-
if (idx === -1) return void 0;
|
|
6568
|
-
let line = 1;
|
|
6569
|
-
for (let i = 0; i < idx; i++) {
|
|
6570
|
-
if (haystack.charCodeAt(i) === 10) line++;
|
|
6571
|
-
}
|
|
6572
|
-
return line;
|
|
6573
|
-
}
|
|
6574
6922
|
var isWin3 = process.platform === "win32";
|
|
6575
6923
|
var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
6576
6924
|
// JS / TS toolchain
|
|
@@ -7302,6 +7650,10 @@ var execTool = {
|
|
|
7302
7650
|
category: "Shell",
|
|
7303
7651
|
description: "Execute a **whitelisted, restricted set of commands** with strict argument validation. This is the **preferred and safer** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It prevents arbitrary command injection and limits what the model can do.",
|
|
7304
7652
|
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).\nThis tool significantly reduces the risk compared to full shell access.",
|
|
7653
|
+
selection: {
|
|
7654
|
+
doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
|
|
7655
|
+
useInstead: ["bash"]
|
|
7656
|
+
},
|
|
7305
7657
|
permission: "confirm",
|
|
7306
7658
|
mutating: true,
|
|
7307
7659
|
riskTier: "standard",
|
|
@@ -7965,13 +8317,13 @@ var formatTool = {
|
|
|
7965
8317
|
}
|
|
7966
8318
|
};
|
|
7967
8319
|
async function detectFixer(cwd) {
|
|
7968
|
-
const { stat:
|
|
8320
|
+
const { stat: stat13 } = await import('node:fs/promises');
|
|
7969
8321
|
try {
|
|
7970
|
-
await
|
|
8322
|
+
await stat13(`${cwd}/biome.json`);
|
|
7971
8323
|
return "biome";
|
|
7972
8324
|
} catch {
|
|
7973
8325
|
try {
|
|
7974
|
-
await
|
|
8326
|
+
await stat13(`${cwd}/.prettierrc`);
|
|
7975
8327
|
return "prettier";
|
|
7976
8328
|
} catch {
|
|
7977
8329
|
return "biome";
|
|
@@ -8132,8 +8484,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
8132
8484
|
let dir = cwd;
|
|
8133
8485
|
for (let i = 0; i < 20; i++) {
|
|
8134
8486
|
try {
|
|
8135
|
-
const
|
|
8136
|
-
if (
|
|
8487
|
+
const stat13 = statSync(`${dir}/.git`);
|
|
8488
|
+
if (stat13.isDirectory() || stat13.isFile()) return dir;
|
|
8137
8489
|
} catch {
|
|
8138
8490
|
}
|
|
8139
8491
|
if (dir === root) break;
|
|
@@ -8280,6 +8632,10 @@ var globTool = {
|
|
|
8280
8632
|
category: "Filesystem",
|
|
8281
8633
|
description: "Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.",
|
|
8282
8634
|
usageHint: "RECOMMENDED FOR SCOPING SEARCHES:\n\n- Use early to get a list of files you actually care about.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
|
|
8635
|
+
selection: {
|
|
8636
|
+
doNotUseWhen: "you need to search inside file contents.",
|
|
8637
|
+
useInstead: ["grep"]
|
|
8638
|
+
},
|
|
8283
8639
|
permission: "auto",
|
|
8284
8640
|
mutating: false,
|
|
8285
8641
|
capabilities: ["fs.read"],
|
|
@@ -8304,8 +8660,9 @@ var globTool = {
|
|
|
8304
8660
|
},
|
|
8305
8661
|
required: ["pattern"]
|
|
8306
8662
|
},
|
|
8307
|
-
async execute(input, ctx) {
|
|
8663
|
+
async execute(input, ctx, opts) {
|
|
8308
8664
|
if (!input?.pattern) throw new Error("glob: pattern is required");
|
|
8665
|
+
const signal = opts?.signal;
|
|
8309
8666
|
const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
|
|
8310
8667
|
const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
|
|
8311
8668
|
const ignored = await readGitignore(base);
|
|
@@ -8329,6 +8686,10 @@ var globTool = {
|
|
|
8329
8686
|
}
|
|
8330
8687
|
};
|
|
8331
8688
|
const walk2 = async (dir, relPrefix) => {
|
|
8689
|
+
if (signal?.aborted) {
|
|
8690
|
+
truncated = true;
|
|
8691
|
+
return;
|
|
8692
|
+
}
|
|
8332
8693
|
if (results.length >= limit) {
|
|
8333
8694
|
truncated = true;
|
|
8334
8695
|
return;
|
|
@@ -8346,7 +8707,7 @@ var globTool = {
|
|
|
8346
8707
|
if (DEFAULT_IGNORE2.includes(name)) continue;
|
|
8347
8708
|
if (ignored.includes(name)) continue;
|
|
8348
8709
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
8349
|
-
const full =
|
|
8710
|
+
const full = path12.join(dir, name);
|
|
8350
8711
|
if (e.isDirectory()) {
|
|
8351
8712
|
subdirs.push({ full, rel });
|
|
8352
8713
|
} else if (e.isFile()) {
|
|
@@ -8390,7 +8751,7 @@ var globTool = {
|
|
|
8390
8751
|
};
|
|
8391
8752
|
async function readGitignore(dir) {
|
|
8392
8753
|
try {
|
|
8393
|
-
const raw = await fs2.readFile(
|
|
8754
|
+
const raw = await fs2.readFile(path12.join(dir, ".gitignore"), "utf8");
|
|
8394
8755
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
8395
8756
|
} catch {
|
|
8396
8757
|
return [];
|
|
@@ -8452,6 +8813,10 @@ var grepTool = {
|
|
|
8452
8813
|
category: "Search",
|
|
8453
8814
|
description: "Search across files using a regular expression. This is one of the primary code search tools. Prefers ripgrep for speed and features when available.",
|
|
8454
8815
|
usageHint: 'POWERFUL CODE SEARCH TOOL:\n\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
|
|
8816
|
+
selection: {
|
|
8817
|
+
doNotUseWhen: "you only need to locate files by name or path pattern.",
|
|
8818
|
+
useInstead: ["glob"]
|
|
8819
|
+
},
|
|
8455
8820
|
permission: "auto",
|
|
8456
8821
|
mutating: false,
|
|
8457
8822
|
capabilities: ["fs.read"],
|
|
@@ -8691,8 +9056,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
8691
9056
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
8692
9057
|
if (globRe) globRe.lastIndex = 0;
|
|
8693
9058
|
try {
|
|
8694
|
-
const
|
|
8695
|
-
if (!
|
|
9059
|
+
const stat13 = await fs2.stat(full);
|
|
9060
|
+
if (!stat13.isFile() || stat13.size > maxBytes || stopped || signal.aborted) return;
|
|
8696
9061
|
const file = await fs2.open(full, "r");
|
|
8697
9062
|
try {
|
|
8698
9063
|
let bytesReadTotal = 0;
|
|
@@ -8782,7 +9147,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
8782
9147
|
if (stopped) return;
|
|
8783
9148
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
8784
9149
|
if (e.isSymbolicLink()) continue;
|
|
8785
|
-
const full =
|
|
9150
|
+
const full = path12.join(dir, e.name);
|
|
8786
9151
|
if (e.isDirectory()) {
|
|
8787
9152
|
subdirs.push(full);
|
|
8788
9153
|
} else if (e.isFile()) {
|
|
@@ -9329,56 +9694,56 @@ function jmespathSearch(data, query) {
|
|
|
9329
9694
|
}
|
|
9330
9695
|
function validateJsonSchema(data, schema) {
|
|
9331
9696
|
const errors = [];
|
|
9332
|
-
function check(value, s,
|
|
9697
|
+
function check(value, s, path24) {
|
|
9333
9698
|
if (s["type"]) {
|
|
9334
9699
|
const expectedType = s["type"];
|
|
9335
9700
|
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
9336
9701
|
if (expectedType === "integer") {
|
|
9337
|
-
if (!Number.isInteger(value)) errors.push(`${
|
|
9702
|
+
if (!Number.isInteger(value)) errors.push(`${path24}: expected integer, got ${actualType}`);
|
|
9338
9703
|
} else if (expectedType !== actualType) {
|
|
9339
|
-
errors.push(`${
|
|
9704
|
+
errors.push(`${path24}: expected ${expectedType}, got ${actualType}`);
|
|
9340
9705
|
}
|
|
9341
9706
|
}
|
|
9342
9707
|
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
9343
9708
|
try {
|
|
9344
9709
|
new URL(value);
|
|
9345
9710
|
} catch {
|
|
9346
|
-
errors.push(`${
|
|
9711
|
+
errors.push(`${path24}: not a valid URI`);
|
|
9347
9712
|
}
|
|
9348
9713
|
}
|
|
9349
9714
|
if (typeof value === "string" && s["pattern"]) {
|
|
9350
9715
|
const re = new RegExp(s["pattern"]);
|
|
9351
|
-
if (!re.test(value)) errors.push(`${
|
|
9716
|
+
if (!re.test(value)) errors.push(`${path24}: does not match pattern ${s["pattern"]}`);
|
|
9352
9717
|
}
|
|
9353
9718
|
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
9354
|
-
errors.push(`${
|
|
9719
|
+
errors.push(`${path24}: string too short (min ${s["minLength"]})`);
|
|
9355
9720
|
}
|
|
9356
9721
|
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
9357
|
-
errors.push(`${
|
|
9722
|
+
errors.push(`${path24}: string too long (max ${s["maxLength"]})`);
|
|
9358
9723
|
}
|
|
9359
9724
|
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
9360
|
-
errors.push(`${
|
|
9725
|
+
errors.push(`${path24}: below minimum ${s["minimum"]}`);
|
|
9361
9726
|
}
|
|
9362
9727
|
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
9363
|
-
errors.push(`${
|
|
9728
|
+
errors.push(`${path24}: above maximum ${s["maximum"]}`);
|
|
9364
9729
|
}
|
|
9365
9730
|
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
9366
9731
|
for (let i = 0; i < value.length; i++) {
|
|
9367
|
-
check(value[i], s["items"], `${
|
|
9732
|
+
check(value[i], s["items"], `${path24}[${i}]`);
|
|
9368
9733
|
}
|
|
9369
9734
|
}
|
|
9370
9735
|
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
9371
9736
|
const props = s["properties"];
|
|
9372
9737
|
for (const [k, propSchema] of Object.entries(props)) {
|
|
9373
|
-
check(value[k], propSchema, `${
|
|
9738
|
+
check(value[k], propSchema, `${path24}.${k}`);
|
|
9374
9739
|
}
|
|
9375
9740
|
}
|
|
9376
9741
|
}
|
|
9377
9742
|
check(data, schema, "$");
|
|
9378
9743
|
return { valid: errors.length === 0, errors };
|
|
9379
9744
|
}
|
|
9380
|
-
function simpleQuery(data,
|
|
9381
|
-
const parts =
|
|
9745
|
+
function simpleQuery(data, path24) {
|
|
9746
|
+
const parts = path24.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
9382
9747
|
let current = data;
|
|
9383
9748
|
for (const part of parts) {
|
|
9384
9749
|
if (current === null || current === void 0) return void 0;
|
|
@@ -10303,11 +10668,11 @@ var lintTool = {
|
|
|
10303
10668
|
}
|
|
10304
10669
|
};
|
|
10305
10670
|
async function detectLinter(cwd) {
|
|
10306
|
-
const { stat:
|
|
10671
|
+
const { stat: stat13 } = await import('node:fs/promises');
|
|
10307
10672
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
10308
10673
|
for (const f of checks) {
|
|
10309
10674
|
try {
|
|
10310
|
-
await
|
|
10675
|
+
await stat13(`${cwd}/${f}`);
|
|
10311
10676
|
if (f.includes("biome")) return "biome";
|
|
10312
10677
|
if (f.includes("eslint")) return "eslint";
|
|
10313
10678
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -10449,7 +10814,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
10449
10814
|
}
|
|
10450
10815
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
10451
10816
|
var MAX_TAIL_LINES = 1e5;
|
|
10452
|
-
async function fileLogs(
|
|
10817
|
+
async function fileLogs(path24, lines, filterRe, stream) {
|
|
10453
10818
|
const { createInterface } = await import('node:readline');
|
|
10454
10819
|
const { createReadStream } = await import('node:fs');
|
|
10455
10820
|
const entries = [];
|
|
@@ -10458,7 +10823,7 @@ async function fileLogs(path23, lines, filterRe, stream) {
|
|
|
10458
10823
|
let writeIdx = 0;
|
|
10459
10824
|
let totalLines = 0;
|
|
10460
10825
|
const rl = createInterface({
|
|
10461
|
-
input: createReadStream(
|
|
10826
|
+
input: createReadStream(path24),
|
|
10462
10827
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
10463
10828
|
});
|
|
10464
10829
|
for await (const line of rl) {
|
|
@@ -10479,7 +10844,7 @@ async function fileLogs(path23, lines, filterRe, stream) {
|
|
|
10479
10844
|
if (parsed) entries.push(parsed);
|
|
10480
10845
|
}
|
|
10481
10846
|
return {
|
|
10482
|
-
source:
|
|
10847
|
+
source: path24,
|
|
10483
10848
|
entries,
|
|
10484
10849
|
total: entries.length,
|
|
10485
10850
|
truncated: totalLines > effLines,
|
|
@@ -10646,6 +11011,10 @@ var patchTool = {
|
|
|
10646
11011
|
category: "Filesystem",
|
|
10647
11012
|
description: "Apply a unified diff (patch) to the project. This is the correct tool when you have a diff that needs to be applied precisely, including handling of rejects.",
|
|
10648
11013
|
usageHint: "Best used when you already have a diff (from generation, external source, or previous step).\n- Use `dry_run: true` to see what would happen without modifying files.\n- On failure it creates .rej and .orig files for manual review.\nOften cleaner than many small `edit` operations for larger changes.",
|
|
11014
|
+
selection: {
|
|
11015
|
+
doNotUseWhen: "you do not already have a unified diff or only need one precise replacement.",
|
|
11016
|
+
useInstead: ["edit"]
|
|
11017
|
+
},
|
|
10649
11018
|
permission: "confirm",
|
|
10650
11019
|
mutating: true,
|
|
10651
11020
|
capabilities: ["fs.write"],
|
|
@@ -10667,12 +11036,13 @@ var patchTool = {
|
|
|
10667
11036
|
const strip = Math.max(1, input.strip ?? 1);
|
|
10668
11037
|
const dryRun = input.dry_run ?? false;
|
|
10669
11038
|
const targets = extractDiffTargets(input.patch);
|
|
11039
|
+
const resolvedTargets = [];
|
|
10670
11040
|
for (const t of targets) {
|
|
10671
11041
|
const stripped = stripPathComponents(t, strip);
|
|
10672
11042
|
if (!stripped) continue;
|
|
10673
|
-
const candidate =
|
|
10674
|
-
const rel =
|
|
10675
|
-
if (rel.startsWith("..") ||
|
|
11043
|
+
const candidate = path12.resolve(dir, stripped);
|
|
11044
|
+
const rel = path12.relative(ctx.projectRoot, candidate);
|
|
11045
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) {
|
|
10676
11046
|
return {
|
|
10677
11047
|
applied: 0,
|
|
10678
11048
|
rejected: 1,
|
|
@@ -10681,12 +11051,19 @@ var patchTool = {
|
|
|
10681
11051
|
message: `patch refused: target "${t}" resolves outside project root`
|
|
10682
11052
|
};
|
|
10683
11053
|
}
|
|
11054
|
+
resolvedTargets.push(candidate);
|
|
10684
11055
|
}
|
|
10685
|
-
const
|
|
11056
|
+
const beforeContents = /* @__PURE__ */ new Map();
|
|
11057
|
+
if (!dryRun) {
|
|
11058
|
+
for (const target of resolvedTargets) {
|
|
11059
|
+
beforeContents.set(target, await readTextForTracking(target));
|
|
11060
|
+
}
|
|
11061
|
+
}
|
|
11062
|
+
const tmpDir = await fs2.mkdtemp(path12.join(os2.tmpdir(), ".wstack_patch_"));
|
|
10686
11063
|
try {
|
|
10687
11064
|
await fs2.chmod(tmpDir, 448).catch(() => {
|
|
10688
11065
|
});
|
|
10689
|
-
const patchFile =
|
|
11066
|
+
const patchFile = path12.join(tmpDir, "in.diff");
|
|
10690
11067
|
await fs2.writeFile(patchFile, input.patch, { mode: 384 });
|
|
10691
11068
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
10692
11069
|
const result = await runPatch(args, dir, opts.signal);
|
|
@@ -10700,6 +11077,21 @@ var patchTool = {
|
|
|
10700
11077
|
};
|
|
10701
11078
|
}
|
|
10702
11079
|
const patched = extractPatchedFiles(result.stdout);
|
|
11080
|
+
if (!dryRun) {
|
|
11081
|
+
for (const target of resolvedTargets) {
|
|
11082
|
+
const before = beforeContents.get(target) ?? null;
|
|
11083
|
+
const after = await readTextForTracking(target);
|
|
11084
|
+
if (after === null || after === before) continue;
|
|
11085
|
+
const stat13 = await fs2.stat(target).catch(() => null);
|
|
11086
|
+
if (stat13) ctx.recordRead?.(target, stat13.mtimeMs, "write", sha256hex(after));
|
|
11087
|
+
ctx.session?.recordFileChange?.({
|
|
11088
|
+
path: target,
|
|
11089
|
+
action: before === null ? "created" : "modified",
|
|
11090
|
+
before,
|
|
11091
|
+
after
|
|
11092
|
+
});
|
|
11093
|
+
}
|
|
11094
|
+
}
|
|
10703
11095
|
return {
|
|
10704
11096
|
applied: patched.length,
|
|
10705
11097
|
rejected: 0,
|
|
@@ -10713,6 +11105,18 @@ var patchTool = {
|
|
|
10713
11105
|
}
|
|
10714
11106
|
}
|
|
10715
11107
|
};
|
|
11108
|
+
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
11109
|
+
async function readTextForTracking(absPath) {
|
|
11110
|
+
try {
|
|
11111
|
+
const stat13 = await fs2.stat(absPath);
|
|
11112
|
+
if (!stat13.isFile() || stat13.size > MAX_TRACKING_BYTES) return null;
|
|
11113
|
+
const buf = await fs2.readFile(absPath);
|
|
11114
|
+
if (buf.includes(0)) return null;
|
|
11115
|
+
return buf.toString("utf8");
|
|
11116
|
+
} catch {
|
|
11117
|
+
return null;
|
|
11118
|
+
}
|
|
11119
|
+
}
|
|
10716
11120
|
function extractDiffTargets(patch) {
|
|
10717
11121
|
const out = [];
|
|
10718
11122
|
const re = /^\+\+\+\s+([^\t\r\n]+)/gm;
|
|
@@ -11020,6 +11424,10 @@ var readTool = {
|
|
|
11020
11424
|
category: "Filesystem",
|
|
11021
11425
|
description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits.",
|
|
11022
11426
|
usageHint: "FOUNDATIONAL TOOL \u2014 call this before almost any edit operation.\n\nBest practices:\n- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\n- Use `offset` + `limit` for very large files instead of reading everything at once.\n- Default limit is generous (2000 lines) but can be increased.\n- The output format is designed to be directly usable as context for `edit` operations.",
|
|
11427
|
+
selection: {
|
|
11428
|
+
doNotUseWhen: "you need to search many files for matching content.",
|
|
11429
|
+
useInstead: ["grep"]
|
|
11430
|
+
},
|
|
11023
11431
|
permission: "auto",
|
|
11024
11432
|
mutating: false,
|
|
11025
11433
|
capabilities: ["fs.read"],
|
|
@@ -11057,9 +11465,9 @@ var readTool = {
|
|
|
11057
11465
|
});
|
|
11058
11466
|
}
|
|
11059
11467
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
11060
|
-
let
|
|
11468
|
+
let stat13;
|
|
11061
11469
|
try {
|
|
11062
|
-
|
|
11470
|
+
stat13 = await fs2.stat(absPath);
|
|
11063
11471
|
} catch (err) {
|
|
11064
11472
|
const code = err.code;
|
|
11065
11473
|
if (code === "ENOENT") {
|
|
@@ -11078,7 +11486,7 @@ var readTool = {
|
|
|
11078
11486
|
cause: err
|
|
11079
11487
|
});
|
|
11080
11488
|
}
|
|
11081
|
-
if (!
|
|
11489
|
+
if (!stat13.isFile()) {
|
|
11082
11490
|
throw new FsError({
|
|
11083
11491
|
message: `read: "${input.path}" is not a regular file`,
|
|
11084
11492
|
code: "FS_READ_FAILED",
|
|
@@ -11086,22 +11494,22 @@ var readTool = {
|
|
|
11086
11494
|
context: { reason: "not-a-regular-file" }
|
|
11087
11495
|
});
|
|
11088
11496
|
}
|
|
11089
|
-
if (
|
|
11497
|
+
if (stat13.size > MAX_BYTES2) {
|
|
11090
11498
|
throw new FsError({
|
|
11091
|
-
message: `read: file too large (${
|
|
11499
|
+
message: `read: file too large (${stat13.size} bytes, limit ${MAX_BYTES2})`,
|
|
11092
11500
|
code: "FS_READ_FAILED",
|
|
11093
11501
|
path: absPath,
|
|
11094
|
-
context: { size:
|
|
11502
|
+
context: { size: stat13.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
11095
11503
|
});
|
|
11096
11504
|
}
|
|
11097
11505
|
const offset = Math.max(1, input.offset ?? 1);
|
|
11098
11506
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
11099
11507
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
11100
11508
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
11101
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
11102
|
-
ctx.recordRead(absPath,
|
|
11509
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat13.mtimeMs, offset, requestedEnd)) {
|
|
11510
|
+
ctx.recordRead(absPath, stat13.mtimeMs);
|
|
11103
11511
|
return {
|
|
11104
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
11512
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat13.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
11105
11513
|
total_lines: prior.totalLines,
|
|
11106
11514
|
encoding: "utf8",
|
|
11107
11515
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -11114,13 +11522,14 @@ var readTool = {
|
|
|
11114
11522
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
11115
11523
|
}
|
|
11116
11524
|
const text = buf.toString("utf8");
|
|
11525
|
+
const contentHash = sha256hex(text);
|
|
11117
11526
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
11118
11527
|
const total = allLines.length;
|
|
11119
11528
|
if (input.mode === "summary") {
|
|
11120
|
-
ctx.recordRead(absPath,
|
|
11121
|
-
rememberReadRange(ctx, absPath,
|
|
11529
|
+
ctx.recordRead(absPath, stat13.mtimeMs, "user", contentHash);
|
|
11530
|
+
rememberReadRange(ctx, absPath, stat13.mtimeMs, total, 1, Math.min(total, 200));
|
|
11122
11531
|
return {
|
|
11123
|
-
text: summarizeFile(input.path,
|
|
11532
|
+
text: summarizeFile(input.path, stat13.size, allLines),
|
|
11124
11533
|
total_lines: total,
|
|
11125
11534
|
encoding: "utf8",
|
|
11126
11535
|
truncated: total > 200,
|
|
@@ -11128,13 +11537,13 @@ var readTool = {
|
|
|
11128
11537
|
};
|
|
11129
11538
|
}
|
|
11130
11539
|
if (limit === 0) {
|
|
11131
|
-
ctx.recordRead(absPath,
|
|
11132
|
-
rememberReadRange(ctx, absPath,
|
|
11540
|
+
ctx.recordRead(absPath, stat13.mtimeMs, "user", contentHash);
|
|
11541
|
+
rememberReadRange(ctx, absPath, stat13.mtimeMs, total, 1, 0);
|
|
11133
11542
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
11134
11543
|
}
|
|
11135
11544
|
if (offset > total) {
|
|
11136
|
-
ctx.recordRead(absPath,
|
|
11137
|
-
rememberReadRange(ctx, absPath,
|
|
11545
|
+
ctx.recordRead(absPath, stat13.mtimeMs, "user", contentHash);
|
|
11546
|
+
rememberReadRange(ctx, absPath, stat13.mtimeMs, total, total + 1, total + 1);
|
|
11138
11547
|
return {
|
|
11139
11548
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
11140
11549
|
total_lines: total,
|
|
@@ -11146,8 +11555,8 @@ var readTool = {
|
|
|
11146
11555
|
const truncated = offset - 1 + slice.length < total;
|
|
11147
11556
|
const width = String(offset + slice.length - 1).length;
|
|
11148
11557
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
11149
|
-
ctx.recordRead(absPath,
|
|
11150
|
-
rememberReadRange(ctx, absPath,
|
|
11558
|
+
ctx.recordRead(absPath, stat13.mtimeMs, "user", contentHash);
|
|
11559
|
+
rememberReadRange(ctx, absPath, stat13.mtimeMs, total, offset, offset + slice.length - 1);
|
|
11151
11560
|
return {
|
|
11152
11561
|
text: numbered,
|
|
11153
11562
|
total_lines: total,
|
|
@@ -11289,10 +11698,10 @@ var replaceTool = {
|
|
|
11289
11698
|
} catch {
|
|
11290
11699
|
continue;
|
|
11291
11700
|
}
|
|
11292
|
-
const rel =
|
|
11293
|
-
if (rel.startsWith("..") ||
|
|
11294
|
-
const
|
|
11295
|
-
if (!
|
|
11701
|
+
const rel = path12.relative(realRoot, realPath);
|
|
11702
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) continue;
|
|
11703
|
+
const stat13 = await fs2.stat(realPath).catch(() => null);
|
|
11704
|
+
if (!stat13?.isFile()) continue;
|
|
11296
11705
|
let content;
|
|
11297
11706
|
try {
|
|
11298
11707
|
const buf = await fs2.readFile(realPath);
|
|
@@ -11317,7 +11726,17 @@ var replaceTool = {
|
|
|
11317
11726
|
totalReplacements += count;
|
|
11318
11727
|
if (!dryRun) {
|
|
11319
11728
|
const newContent = toStyle(newContentLf, style);
|
|
11320
|
-
await atomicWrite(realPath, newContent, { mode:
|
|
11729
|
+
await atomicWrite(realPath, newContent, { mode: stat13.mode & 511 });
|
|
11730
|
+
const written = await fs2.stat(realPath).catch(() => null);
|
|
11731
|
+
if (written) {
|
|
11732
|
+
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
11733
|
+
}
|
|
11734
|
+
ctx.session?.recordFileChange?.({
|
|
11735
|
+
path: realPath,
|
|
11736
|
+
action: "modified",
|
|
11737
|
+
before: content,
|
|
11738
|
+
after: newContent
|
|
11739
|
+
});
|
|
11321
11740
|
}
|
|
11322
11741
|
const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
|
|
11323
11742
|
fromFile: absPath,
|
|
@@ -11347,8 +11766,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
11347
11766
|
const resolved = [];
|
|
11348
11767
|
for (const p of parts) {
|
|
11349
11768
|
const absPath = safeResolve(p, ctx);
|
|
11350
|
-
const
|
|
11351
|
-
if (
|
|
11769
|
+
const stat13 = await fs2.stat(absPath).catch(() => null);
|
|
11770
|
+
if (stat13?.isFile()) {
|
|
11352
11771
|
resolved.push(absPath);
|
|
11353
11772
|
}
|
|
11354
11773
|
}
|
|
@@ -11409,10 +11828,10 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
11409
11828
|
}
|
|
11410
11829
|
for (const e of entries) {
|
|
11411
11830
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
11412
|
-
const full =
|
|
11831
|
+
const full = path12.join(dir, e.name);
|
|
11413
11832
|
try {
|
|
11414
|
-
const
|
|
11415
|
-
if (
|
|
11833
|
+
const stat13 = await fs2.lstat(full);
|
|
11834
|
+
if (stat13.isSymbolicLink()) continue;
|
|
11416
11835
|
} catch {
|
|
11417
11836
|
continue;
|
|
11418
11837
|
}
|
|
@@ -11580,16 +11999,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
11580
11999
|
let filesCreated = 0;
|
|
11581
12000
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
11582
12001
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
11583
|
-
const joinedPath =
|
|
11584
|
-
const root =
|
|
11585
|
-
const target =
|
|
11586
|
-
const rel =
|
|
11587
|
-
if (rel.startsWith("..") ||
|
|
12002
|
+
const joinedPath = path12.join(cwd, resolvedPath);
|
|
12003
|
+
const root = path12.resolve(ctx.projectRoot);
|
|
12004
|
+
const target = path12.resolve(joinedPath);
|
|
12005
|
+
const rel = path12.relative(root, target);
|
|
12006
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) {
|
|
11588
12007
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
11589
12008
|
}
|
|
11590
12009
|
const fullPath = target;
|
|
11591
12010
|
if (!dryRun) {
|
|
11592
|
-
await fs2.mkdir(
|
|
12011
|
+
await fs2.mkdir(path12.dirname(fullPath), { recursive: true });
|
|
11593
12012
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
11594
12013
|
}
|
|
11595
12014
|
files.push(resolvedPath);
|
|
@@ -12489,11 +12908,11 @@ var testTool = {
|
|
|
12489
12908
|
}
|
|
12490
12909
|
};
|
|
12491
12910
|
async function detectRunner(cwd) {
|
|
12492
|
-
const { stat:
|
|
12911
|
+
const { stat: stat13 } = await import('node:fs/promises');
|
|
12493
12912
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
12494
12913
|
for (const f of candidates) {
|
|
12495
12914
|
try {
|
|
12496
|
-
await
|
|
12915
|
+
await stat13(path12.join(cwd, f));
|
|
12497
12916
|
if (f.includes("vitest")) return "vitest";
|
|
12498
12917
|
if (f.includes("jest")) return "jest";
|
|
12499
12918
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -13121,7 +13540,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
13121
13540
|
opts.lines.push(opts.prefix + branch + displayName);
|
|
13122
13541
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
13123
13542
|
const childPrefix = opts.prefix + connector;
|
|
13124
|
-
await walkDir(
|
|
13543
|
+
await walkDir(path12.join(dir, entry.name), depth + 1, {
|
|
13125
13544
|
...opts,
|
|
13126
13545
|
prefix: childPrefix,
|
|
13127
13546
|
isLast
|
|
@@ -13207,12 +13626,12 @@ var typecheckTool = {
|
|
|
13207
13626
|
}
|
|
13208
13627
|
};
|
|
13209
13628
|
async function findTsConfig(cwd) {
|
|
13210
|
-
const { stat:
|
|
13629
|
+
const { stat: stat13 } = await import('node:fs/promises');
|
|
13211
13630
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
13212
13631
|
for (const f of candidates) {
|
|
13213
13632
|
try {
|
|
13214
|
-
const s = await
|
|
13215
|
-
if (s.isFile()) return
|
|
13633
|
+
const s = await stat13(path12.join(cwd, f));
|
|
13634
|
+
if (s.isFile()) return path12.join(cwd, f);
|
|
13216
13635
|
} catch {
|
|
13217
13636
|
}
|
|
13218
13637
|
}
|
|
@@ -13223,6 +13642,10 @@ var writeTool = {
|
|
|
13223
13642
|
category: "Filesystem",
|
|
13224
13643
|
description: "Write or completely overwrite a file on disk. This is a high-privilege operation. For modifying existing files, you should almost always prefer the `edit` tool instead, because `edit` is safer and works on the last-read version of the file.",
|
|
13225
13644
|
usageHint: "RULES FOR CORRECT USAGE:\n- Use `write` primarily for **new files** or when you want to replace the entire content.\n- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n- You MUST have called `read` on the file earlier in the conversation before using `write` on an existing path (the system enforces this for safety).\n- The path is resolved relative to the project root and protected against escaping the workspace.",
|
|
13645
|
+
selection: {
|
|
13646
|
+
doNotUseWhen: "making a precise change to part of an existing file.",
|
|
13647
|
+
useInstead: ["edit"]
|
|
13648
|
+
},
|
|
13226
13649
|
permission: "confirm",
|
|
13227
13650
|
mutating: true,
|
|
13228
13651
|
timeoutMs: 5e3,
|
|
@@ -13242,10 +13665,10 @@ var writeTool = {
|
|
|
13242
13665
|
},
|
|
13243
13666
|
required: ["path", "content"]
|
|
13244
13667
|
},
|
|
13245
|
-
async execute(input, ctx) {
|
|
13246
|
-
return writeFile7(input, ctx);
|
|
13668
|
+
async execute(input, ctx, opts) {
|
|
13669
|
+
return writeFile7(input, ctx, opts?.signal);
|
|
13247
13670
|
},
|
|
13248
|
-
async *executeStream(input, ctx) {
|
|
13671
|
+
async *executeStream(input, ctx, opts) {
|
|
13249
13672
|
const prepared = await prepareWrite(input, ctx);
|
|
13250
13673
|
if (!prepared.existed) {
|
|
13251
13674
|
for (const line of input.content.split("\n")) {
|
|
@@ -13253,11 +13676,11 @@ var writeTool = {
|
|
|
13253
13676
|
`, data: { livePreview: true } };
|
|
13254
13677
|
}
|
|
13255
13678
|
}
|
|
13256
|
-
yield { type: "final", output: await finishWrite(input, ctx, prepared) };
|
|
13679
|
+
yield { type: "final", output: await finishWrite(input, ctx, prepared, opts?.signal) };
|
|
13257
13680
|
}
|
|
13258
13681
|
};
|
|
13259
|
-
async function writeFile7(input, ctx) {
|
|
13260
|
-
return finishWrite(input, ctx, await prepareWrite(input, ctx));
|
|
13682
|
+
async function writeFile7(input, ctx, signal) {
|
|
13683
|
+
return finishWrite(input, ctx, await prepareWrite(input, ctx), signal);
|
|
13261
13684
|
}
|
|
13262
13685
|
async function prepareWrite(input, ctx) {
|
|
13263
13686
|
if (!input?.path) {
|
|
@@ -13276,12 +13699,12 @@ async function prepareWrite(input, ctx) {
|
|
|
13276
13699
|
let existed = false;
|
|
13277
13700
|
let prev = "";
|
|
13278
13701
|
try {
|
|
13279
|
-
const
|
|
13280
|
-
existed =
|
|
13702
|
+
const stat13 = await fs2.stat(absPath);
|
|
13703
|
+
existed = stat13.isFile();
|
|
13281
13704
|
if (existed) {
|
|
13282
13705
|
if (!ctx.hasRead(absPath)) {
|
|
13283
13706
|
prev = await fs2.readFile(absPath, "utf8");
|
|
13284
|
-
ctx.recordRead(absPath,
|
|
13707
|
+
ctx.recordRead(absPath, stat13.mtimeMs, "write", sha256hex(prev));
|
|
13285
13708
|
} else {
|
|
13286
13709
|
prev = await fs2.readFile(absPath, "utf8");
|
|
13287
13710
|
}
|
|
@@ -13293,23 +13716,32 @@ async function prepareWrite(input, ctx) {
|
|
|
13293
13716
|
}
|
|
13294
13717
|
return { absPath, existed, prev };
|
|
13295
13718
|
}
|
|
13296
|
-
async function finishWrite(input, ctx, prepared) {
|
|
13719
|
+
async function finishWrite(input, ctx, prepared, signal) {
|
|
13720
|
+
signal?.throwIfAborted();
|
|
13297
13721
|
await atomicWrite(prepared.absPath, input.content);
|
|
13298
13722
|
const diff = prepared.existed ? unifiedDiff(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
13299
13723
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
13300
|
-
const
|
|
13301
|
-
ctx.recordRead(prepared.absPath,
|
|
13724
|
+
const stat13 = await fs2.stat(prepared.absPath);
|
|
13725
|
+
ctx.recordRead(prepared.absPath, stat13.mtimeMs, "write", sha256hex(input.content));
|
|
13302
13726
|
ctx.session.recordFileChange({
|
|
13303
13727
|
path: prepared.absPath,
|
|
13304
13728
|
action: prepared.existed ? "modified" : "created",
|
|
13305
13729
|
before: prepared.existed ? prepared.prev : null,
|
|
13306
13730
|
after: input.content
|
|
13307
13731
|
});
|
|
13732
|
+
const syntax = await checkSyntax(
|
|
13733
|
+
prepared.absPath,
|
|
13734
|
+
input.content,
|
|
13735
|
+
prepared.existed ? prepared.prev : void 0
|
|
13736
|
+
).catch(() => void 0);
|
|
13737
|
+
const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
|
|
13308
13738
|
return {
|
|
13309
13739
|
path: prepared.absPath,
|
|
13310
13740
|
bytes_written: Buffer.byteLength(input.content, "utf8"),
|
|
13311
13741
|
created: !prepared.existed,
|
|
13312
|
-
diff
|
|
13742
|
+
diff,
|
|
13743
|
+
syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
|
|
13744
|
+
note: hasSyntaxErrors ? syntax.preExisting ? "Syntax check: the file still has parse errors (they pre-date this write) \u2014 see syntax_errors." : `Syntax check: the written content has ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.` : void 0
|
|
13313
13745
|
};
|
|
13314
13746
|
}
|
|
13315
13747
|
|
|
@@ -13718,6 +14150,7 @@ var ProcessGuardian = class {
|
|
|
13718
14150
|
heartbeatTimer = null;
|
|
13719
14151
|
isRunning = false;
|
|
13720
14152
|
instanceId;
|
|
14153
|
+
sigtermCount = 0;
|
|
13721
14154
|
constructor(config = {}) {
|
|
13722
14155
|
this.registry = getPersistentProcessRegistry();
|
|
13723
14156
|
this.config = {
|
|
@@ -13725,7 +14158,8 @@ var ProcessGuardian = class {
|
|
|
13725
14158
|
autoResurrect: config.autoResurrect ?? false,
|
|
13726
14159
|
// Disabled by default
|
|
13727
14160
|
maxResurrectionAttempts: config.maxResurrectionAttempts ?? 3,
|
|
13728
|
-
protectedPatterns: config.protectedPatterns ?? ["node", "wrongstack"]
|
|
14161
|
+
protectedPatterns: config.protectedPatterns ?? ["node", "wrongstack"],
|
|
14162
|
+
sigtermThreshold: config.sigtermThreshold ?? 3
|
|
13729
14163
|
};
|
|
13730
14164
|
this.instanceId = this.registry.getInstanceId();
|
|
13731
14165
|
}
|
|
@@ -13868,15 +14302,43 @@ var ProcessGuardian = class {
|
|
|
13868
14302
|
this.stop();
|
|
13869
14303
|
process.exit(1);
|
|
13870
14304
|
});
|
|
13871
|
-
process.on("SIGTERM", (
|
|
14305
|
+
process.on("SIGTERM", () => {
|
|
14306
|
+
this.sigtermCount++;
|
|
14307
|
+
const threshold = this.config.sigtermThreshold;
|
|
14308
|
+
if (threshold === Infinity) {
|
|
14309
|
+
console.log(JSON.stringify({
|
|
14310
|
+
level: "warn",
|
|
14311
|
+
event: "process_guardian.sigterm_ignored",
|
|
14312
|
+
pid: process.pid,
|
|
14313
|
+
instanceId: this.instanceId,
|
|
14314
|
+
sigtermCount: this.sigtermCount,
|
|
14315
|
+
message: "SIGTERM received but sigtermThreshold is Infinity \u2014 ignoring (use graceful shutdown instead)"
|
|
14316
|
+
}));
|
|
14317
|
+
return;
|
|
14318
|
+
}
|
|
14319
|
+
if (this.sigtermCount < threshold) {
|
|
14320
|
+
console.log(JSON.stringify({
|
|
14321
|
+
level: "warn",
|
|
14322
|
+
event: "process_guardian.sigterm_deferred",
|
|
14323
|
+
pid: process.pid,
|
|
14324
|
+
instanceId: this.instanceId,
|
|
14325
|
+
sigtermCount: this.sigtermCount,
|
|
14326
|
+
threshold,
|
|
14327
|
+
message: `SIGTERM received (${this.sigtermCount}/${threshold}) \u2014 ignoring until threshold is reached`
|
|
14328
|
+
}));
|
|
14329
|
+
return;
|
|
14330
|
+
}
|
|
13872
14331
|
console.log(JSON.stringify({
|
|
13873
14332
|
level: "warn",
|
|
13874
|
-
event: "process_guardian.
|
|
13875
|
-
origin,
|
|
14333
|
+
event: "process_guardian.sigterm_exiting",
|
|
13876
14334
|
pid: process.pid,
|
|
13877
14335
|
instanceId: this.instanceId,
|
|
13878
|
-
|
|
14336
|
+
sigtermCount: this.sigtermCount,
|
|
14337
|
+
threshold,
|
|
14338
|
+
message: `SIGTERM threshold (${threshold}) reached \u2014 shutting down gracefully`
|
|
13879
14339
|
}));
|
|
14340
|
+
this.stop();
|
|
14341
|
+
process.exit(0);
|
|
13880
14342
|
});
|
|
13881
14343
|
process.on("SIGHUP", () => {
|
|
13882
14344
|
console.log(JSON.stringify({
|
|
@@ -14284,7 +14746,7 @@ function makeSkillTool(skillLoader) {
|
|
|
14284
14746
|
field: "name"
|
|
14285
14747
|
});
|
|
14286
14748
|
}
|
|
14287
|
-
const dir =
|
|
14749
|
+
const dir = path12.dirname(manifest.path);
|
|
14288
14750
|
let loadedResource;
|
|
14289
14751
|
if (input.resource?.trim()) {
|
|
14290
14752
|
loadedResource = await loadResource(dir, input.resource.trim());
|
|
@@ -14334,15 +14796,15 @@ ${listing}`;
|
|
|
14334
14796
|
}
|
|
14335
14797
|
async function loadResource(skillDir, rel) {
|
|
14336
14798
|
const norm = rel.replace(/\\/g, "/");
|
|
14337
|
-
if (
|
|
14799
|
+
if (path12.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
|
|
14338
14800
|
throw new ToolValidationError({
|
|
14339
14801
|
message: `skill: invalid resource path "${rel}"`,
|
|
14340
14802
|
field: "resource"
|
|
14341
14803
|
});
|
|
14342
14804
|
}
|
|
14343
|
-
const absPath =
|
|
14344
|
-
const root =
|
|
14345
|
-
if (absPath !== root && !absPath.startsWith(root +
|
|
14805
|
+
const absPath = path12.resolve(skillDir, rel);
|
|
14806
|
+
const root = path12.resolve(skillDir);
|
|
14807
|
+
if (absPath !== root && !absPath.startsWith(root + path12.sep)) {
|
|
14346
14808
|
throw new ToolValidationError({
|
|
14347
14809
|
message: `skill: resource "${rel}" escapes the skill directory`,
|
|
14348
14810
|
field: "resource"
|
|
@@ -14383,7 +14845,7 @@ async function walk(root, dir, out) {
|
|
|
14383
14845
|
}
|
|
14384
14846
|
for (const e of entries) {
|
|
14385
14847
|
if (out.length >= MAX_LISTED_RESOURCES) return;
|
|
14386
|
-
const fullPath =
|
|
14848
|
+
const fullPath = path12.join(dir, e.name);
|
|
14387
14849
|
let isDir = e.isDirectory();
|
|
14388
14850
|
if (e.isSymbolicLink()) {
|
|
14389
14851
|
try {
|
|
@@ -14398,9 +14860,9 @@ async function walk(root, dir, out) {
|
|
|
14398
14860
|
} else if (e.isFile()) {
|
|
14399
14861
|
if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
|
|
14400
14862
|
try {
|
|
14401
|
-
const
|
|
14402
|
-
const rel =
|
|
14403
|
-
out.push({ path: rel, bytes:
|
|
14863
|
+
const stat13 = await fs2.stat(fullPath);
|
|
14864
|
+
const rel = path12.relative(root, fullPath).split(path12.sep).join("/");
|
|
14865
|
+
out.push({ path: rel, bytes: stat13.size });
|
|
14404
14866
|
} catch {
|
|
14405
14867
|
}
|
|
14406
14868
|
}
|