@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/builtin.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { spawn, execFileSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { 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,
|
|
3
|
+
import { 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, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import { statSync, mkdirSync, createWriteStream } from 'node:fs';
|
|
6
6
|
import * as fs2 from 'node:fs/promises';
|
|
7
|
-
import * as
|
|
7
|
+
import * as path12 from 'node:path';
|
|
8
8
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
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,14 +18,14 @@ 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/_spawn-stream.ts
|
|
23
24
|
var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
24
25
|
var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
|
|
25
26
|
var sweepStarted = false;
|
|
26
27
|
function toolOutputDir() {
|
|
27
|
-
return
|
|
28
|
+
return path12.join(wstackGlobalRoot(), "tool-output");
|
|
28
29
|
}
|
|
29
30
|
function sweepOldSpoolFiles(dir) {
|
|
30
31
|
if (sweepStarted) return;
|
|
@@ -34,7 +35,7 @@ function sweepOldSpoolFiles(dir) {
|
|
|
34
35
|
const now = Date.now();
|
|
35
36
|
for (const name of await fs2.readdir(dir)) {
|
|
36
37
|
if (!name.endsWith(".log")) continue;
|
|
37
|
-
const p =
|
|
38
|
+
const p = path12.join(dir, name);
|
|
38
39
|
try {
|
|
39
40
|
const st = await fs2.stat(p);
|
|
40
41
|
if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fs2.unlink(p);
|
|
@@ -69,7 +70,7 @@ function createOutputSpool(opts) {
|
|
|
69
70
|
sweepOldSpoolFiles(dir);
|
|
70
71
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
71
72
|
const rand = Math.random().toString(36).slice(2, 6);
|
|
72
|
-
filePath =
|
|
73
|
+
filePath = path12.join(dir, `${stamp}-${safeTool}-${rand}.log`);
|
|
73
74
|
stream = createWriteStream(filePath, { flags: "w", encoding: "utf8" });
|
|
74
75
|
stream.on("error", () => {
|
|
75
76
|
failed = true;
|
|
@@ -702,13 +703,13 @@ function getProcessRegistry() {
|
|
|
702
703
|
}
|
|
703
704
|
function resolveWin32Command(cmd) {
|
|
704
705
|
if (process.platform !== "win32") return cmd;
|
|
705
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
706
|
+
if (cmd.includes("/") || cmd.includes("\\") || path12.extname(cmd.replace(/\//g, "\\"))) {
|
|
706
707
|
return cmd;
|
|
707
708
|
}
|
|
708
709
|
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
709
|
-
const pathDirs = (process.env["PATH"] ?? "").split(
|
|
710
|
+
const pathDirs = (process.env["PATH"] ?? "").split(path12.delimiter);
|
|
710
711
|
for (const dir of pathDirs) {
|
|
711
|
-
const base =
|
|
712
|
+
const base = path12.join(dir, cmd);
|
|
712
713
|
for (const ext of pathext) {
|
|
713
714
|
const full = `${base}${ext}`;
|
|
714
715
|
try {
|
|
@@ -922,37 +923,40 @@ async function* spawnStream(opts) {
|
|
|
922
923
|
}
|
|
923
924
|
}
|
|
924
925
|
}
|
|
926
|
+
function sha256hex(content) {
|
|
927
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
928
|
+
}
|
|
925
929
|
async function detectPackageManager(cwd) {
|
|
926
|
-
const { stat:
|
|
930
|
+
const { stat: stat12 } = await import('node:fs/promises');
|
|
927
931
|
try {
|
|
928
|
-
await
|
|
932
|
+
await stat12(`${cwd}/pnpm-lock.yaml`);
|
|
929
933
|
return "pnpm";
|
|
930
934
|
} catch {
|
|
931
935
|
}
|
|
932
936
|
try {
|
|
933
|
-
await
|
|
937
|
+
await stat12(`${cwd}/yarn.lock`);
|
|
934
938
|
return "yarn";
|
|
935
939
|
} catch {
|
|
936
940
|
}
|
|
937
941
|
return "npm";
|
|
938
942
|
}
|
|
939
943
|
function resolvePath(input, ctx) {
|
|
940
|
-
return
|
|
944
|
+
return path12.isAbsolute(input) ? path12.normalize(input) : path12.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
941
945
|
}
|
|
942
946
|
function allowedRoots(ctx) {
|
|
943
|
-
return [
|
|
947
|
+
return [path12.resolve(ctx.projectRoot), path12.resolve(Core.wstackGlobalRoot())];
|
|
944
948
|
}
|
|
945
949
|
function isInsideAny(target, roots) {
|
|
946
950
|
return roots.some((root) => {
|
|
947
|
-
const rel =
|
|
948
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
951
|
+
const rel = path12.relative(root, target);
|
|
952
|
+
return rel === "" || !rel.startsWith("..") && !path12.isAbsolute(rel);
|
|
949
953
|
});
|
|
950
954
|
}
|
|
951
955
|
function ensureInsideRoot(absPath, ctx) {
|
|
952
|
-
const target =
|
|
956
|
+
const target = path12.resolve(absPath);
|
|
953
957
|
if (ctx.allowOutsideProjectRoot) return target;
|
|
954
958
|
if (isInsideAny(target, allowedRoots(ctx))) return target;
|
|
955
|
-
throw new Error(`Path "${absPath}" is outside project root "${
|
|
959
|
+
throw new Error(`Path "${absPath}" is outside project root "${path12.resolve(ctx.projectRoot)}"`);
|
|
956
960
|
}
|
|
957
961
|
function safeResolve(input, ctx) {
|
|
958
962
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
@@ -960,7 +964,7 @@ function safeResolve(input, ctx) {
|
|
|
960
964
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
961
965
|
if (ctx.allowOutsideProjectRoot) return;
|
|
962
966
|
const realRoots = await Promise.all(
|
|
963
|
-
allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() =>
|
|
967
|
+
allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() => path12.resolve(r)))
|
|
964
968
|
);
|
|
965
969
|
let probe = absPath;
|
|
966
970
|
for (; ; ) {
|
|
@@ -969,7 +973,7 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
969
973
|
real = await fs2.realpath(probe);
|
|
970
974
|
} catch (err) {
|
|
971
975
|
if (err.code === "ENOENT") {
|
|
972
|
-
const parent =
|
|
976
|
+
const parent = path12.dirname(probe);
|
|
973
977
|
if (parent === probe) return;
|
|
974
978
|
probe = parent;
|
|
975
979
|
continue;
|
|
@@ -1282,15 +1286,15 @@ var PersistentProcessRegistry = class {
|
|
|
1282
1286
|
constructor(baseRegistry) {
|
|
1283
1287
|
this.instanceId = generateInstanceId();
|
|
1284
1288
|
const homeDir = os2.homedir();
|
|
1285
|
-
this.registryPath =
|
|
1286
|
-
this.lockPath =
|
|
1289
|
+
this.registryPath = path12.join(homeDir, REGISTRY_FILE);
|
|
1290
|
+
this.lockPath = path12.join(homeDir, LOCKFILE);
|
|
1287
1291
|
this.baseRegistry = baseRegistry ?? getProcessRegistry();
|
|
1288
1292
|
this.ensureDirectory().catch((err) => {
|
|
1289
1293
|
emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
|
|
1290
1294
|
});
|
|
1291
1295
|
}
|
|
1292
1296
|
async ensureDirectory() {
|
|
1293
|
-
const dir =
|
|
1297
|
+
const dir = path12.dirname(this.registryPath);
|
|
1294
1298
|
try {
|
|
1295
1299
|
await fs2.mkdir(dir, { recursive: true });
|
|
1296
1300
|
} catch (err) {
|
|
@@ -1870,6 +1874,10 @@ var bashTool = {
|
|
|
1870
1874
|
category: "Shell",
|
|
1871
1875
|
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.",
|
|
1872
1876
|
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.",
|
|
1877
|
+
selection: {
|
|
1878
|
+
doNotUseWhen: "the command is allowlisted and does not require pipes, redirection, or shell expansion.",
|
|
1879
|
+
useInstead: ["exec"]
|
|
1880
|
+
},
|
|
1873
1881
|
permission: "confirm",
|
|
1874
1882
|
mutating: true,
|
|
1875
1883
|
riskTier: "destructive",
|
|
@@ -2731,7 +2739,7 @@ var IndexStore = class {
|
|
|
2731
2739
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
2732
2740
|
fs.mkdirSync(this.indexDir, { recursive: true });
|
|
2733
2741
|
const Database = loadDatabaseSync();
|
|
2734
|
-
this.db = new Database(
|
|
2742
|
+
this.db = new Database(path12.join(this.indexDir, DB_FILE));
|
|
2735
2743
|
try {
|
|
2736
2744
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
2737
2745
|
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -3310,7 +3318,7 @@ var IndexStore = class {
|
|
|
3310
3318
|
}));
|
|
3311
3319
|
}
|
|
3312
3320
|
sizeBytes() {
|
|
3313
|
-
const dbPath =
|
|
3321
|
+
const dbPath = path12.join(this.indexDir, DB_FILE);
|
|
3314
3322
|
try {
|
|
3315
3323
|
return fs.statSync(dbPath).size;
|
|
3316
3324
|
} catch {
|
|
@@ -3809,9 +3817,9 @@ func formatType(t ast.Expr) string {
|
|
|
3809
3817
|
}
|
|
3810
3818
|
`;
|
|
3811
3819
|
async function syncGoParse(filePath, content, lang) {
|
|
3812
|
-
const tmpDir = await fs2.mkdtemp(
|
|
3820
|
+
const tmpDir = await fs2.mkdtemp(path12.join(os2.tmpdir(), "ws-go-parse-"));
|
|
3813
3821
|
try {
|
|
3814
|
-
const scriptPath =
|
|
3822
|
+
const scriptPath = path12.join(tmpDir, "parse.go");
|
|
3815
3823
|
await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
3816
3824
|
const proc = spawn("go", ["run", scriptPath], {
|
|
3817
3825
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -4073,9 +4081,9 @@ var _cachedScriptPath = null;
|
|
|
4073
4081
|
async function syncPyParse(filePath, content, lang) {
|
|
4074
4082
|
try {
|
|
4075
4083
|
if (!_cachedScriptPath) {
|
|
4076
|
-
const tmpDir =
|
|
4084
|
+
const tmpDir = path12.join(os2.tmpdir(), "ws-py-parse");
|
|
4077
4085
|
await fs2.mkdir(tmpDir, { recursive: true });
|
|
4078
|
-
_cachedScriptPath =
|
|
4086
|
+
_cachedScriptPath = path12.join(tmpDir, "parse.py");
|
|
4079
4087
|
await fs2.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
4080
4088
|
}
|
|
4081
4089
|
const proc = spawn("python", [_cachedScriptPath, filePath], {
|
|
@@ -4133,7 +4141,7 @@ async function parseSymbols4(opts) {
|
|
|
4133
4141
|
function checkNativeParser() {
|
|
4134
4142
|
try {
|
|
4135
4143
|
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
4136
|
-
const toolsDir =
|
|
4144
|
+
const toolsDir = path12.join(process.cwd(), "tools");
|
|
4137
4145
|
try {
|
|
4138
4146
|
execFileSync(
|
|
4139
4147
|
"cargo",
|
|
@@ -4143,7 +4151,7 @@ function checkNativeParser() {
|
|
|
4143
4151
|
"--format-version",
|
|
4144
4152
|
"1",
|
|
4145
4153
|
"--manifest-path",
|
|
4146
|
-
|
|
4154
|
+
path12.join(toolsDir, "Cargo.toml")
|
|
4147
4155
|
],
|
|
4148
4156
|
{ stdio: "pipe", windowsHide: true }
|
|
4149
4157
|
);
|
|
@@ -4157,13 +4165,13 @@ function checkNativeParser() {
|
|
|
4157
4165
|
}
|
|
4158
4166
|
async function tryNativeParse(file, content) {
|
|
4159
4167
|
try {
|
|
4160
|
-
const toolsDir =
|
|
4161
|
-
const crateDir =
|
|
4162
|
-
const tmpFile =
|
|
4168
|
+
const toolsDir = path12.join(process.cwd(), "tools");
|
|
4169
|
+
const crateDir = path12.join(toolsDir, "syn-parser");
|
|
4170
|
+
const tmpFile = path12.join(crateDir, "src", "input.rs");
|
|
4163
4171
|
await fs2.writeFile(tmpFile, content, "utf8");
|
|
4164
4172
|
const proc = spawn(
|
|
4165
4173
|
"cargo",
|
|
4166
|
-
["run", "--manifest-path",
|
|
4174
|
+
["run", "--manifest-path", path12.join(toolsDir, "Cargo.toml")],
|
|
4167
4175
|
{
|
|
4168
4176
|
cwd: process.cwd(),
|
|
4169
4177
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -4213,16 +4221,16 @@ function regexParse(opts) {
|
|
|
4213
4221
|
const { file, content, lang } = opts;
|
|
4214
4222
|
const symbols = [];
|
|
4215
4223
|
const lines = content.split("\n");
|
|
4216
|
-
const
|
|
4224
|
+
const lineOffsets2 = [0];
|
|
4217
4225
|
for (let i = 0; i < lines.length; i++) {
|
|
4218
|
-
|
|
4226
|
+
lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
|
|
4219
4227
|
}
|
|
4220
4228
|
function lineFromOffset(offset) {
|
|
4221
4229
|
let lo = 0;
|
|
4222
|
-
let hi =
|
|
4230
|
+
let hi = lineOffsets2.length - 1;
|
|
4223
4231
|
while (lo < hi) {
|
|
4224
4232
|
const mid = lo + hi + 1 >>> 1;
|
|
4225
|
-
if (expectDefined(
|
|
4233
|
+
if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
|
|
4226
4234
|
else hi = mid - 1;
|
|
4227
4235
|
}
|
|
4228
4236
|
return lo + 1;
|
|
@@ -4237,7 +4245,7 @@ function regexParse(opts) {
|
|
|
4237
4245
|
const name = expectDefined(match[1]);
|
|
4238
4246
|
const offset = match.index ?? 0;
|
|
4239
4247
|
const line = lineFromOffset(offset);
|
|
4240
|
-
const col = offset - (
|
|
4248
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4241
4249
|
const lineIdx = line - 1;
|
|
4242
4250
|
const signature = extractDeclaration(lineIdx);
|
|
4243
4251
|
symbols.push({
|
|
@@ -4275,22 +4283,22 @@ function parseSymbols5(opts) {
|
|
|
4275
4283
|
function regexParse2(opts) {
|
|
4276
4284
|
const { file, content, lang } = opts;
|
|
4277
4285
|
const symbols = [];
|
|
4278
|
-
const
|
|
4279
|
-
const isPackageJson =
|
|
4280
|
-
const isTsconfig =
|
|
4286
|
+
const basename4 = path12.basename(file).toLowerCase();
|
|
4287
|
+
const isPackageJson = basename4 === "package.json";
|
|
4288
|
+
const isTsconfig = basename4 === "tsconfig.json" || basename4 === "tsconfig.build.json";
|
|
4281
4289
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
4282
4290
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
4283
4291
|
const lines = content.split("\n");
|
|
4284
|
-
const
|
|
4292
|
+
const lineOffsets2 = [0];
|
|
4285
4293
|
for (let i = 0; i < lines.length; i++) {
|
|
4286
|
-
|
|
4294
|
+
lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
|
|
4287
4295
|
}
|
|
4288
4296
|
function lineFromOffset(offset) {
|
|
4289
4297
|
let lo = 0;
|
|
4290
|
-
let hi =
|
|
4298
|
+
let hi = lineOffsets2.length - 1;
|
|
4291
4299
|
while (lo < hi) {
|
|
4292
4300
|
const mid = lo + hi + 1 >>> 1;
|
|
4293
|
-
if (expectDefined(
|
|
4301
|
+
if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
|
|
4294
4302
|
else hi = mid - 1;
|
|
4295
4303
|
}
|
|
4296
4304
|
return lo + 1;
|
|
@@ -4301,11 +4309,11 @@ function regexParse2(opts) {
|
|
|
4301
4309
|
const line = lineFromOffset(offset);
|
|
4302
4310
|
symbols.push(
|
|
4303
4311
|
makeSymbol({
|
|
4304
|
-
name:
|
|
4312
|
+
name: path12.basename(file),
|
|
4305
4313
|
kind: "object",
|
|
4306
4314
|
line,
|
|
4307
4315
|
col: 0,
|
|
4308
|
-
signature: `"${
|
|
4316
|
+
signature: `"${path12.basename(file)}" = { ... }`,
|
|
4309
4317
|
file,
|
|
4310
4318
|
lang
|
|
4311
4319
|
})
|
|
@@ -4316,7 +4324,7 @@ function regexParse2(opts) {
|
|
|
4316
4324
|
const key = expectDefined(match[1]);
|
|
4317
4325
|
const offset = match.index ?? 0;
|
|
4318
4326
|
const line = lineFromOffset(offset);
|
|
4319
|
-
const col = offset - (
|
|
4327
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4320
4328
|
let kind = "property";
|
|
4321
4329
|
let signature = `"${key}": ..."`;
|
|
4322
4330
|
if (isPackageJson) {
|
|
@@ -4351,10 +4359,10 @@ function regexParse2(opts) {
|
|
|
4351
4359
|
})
|
|
4352
4360
|
);
|
|
4353
4361
|
if (isPackageJson && key === "scripts") {
|
|
4354
|
-
extractPackageScripts(content, symbols, file, lang,
|
|
4362
|
+
extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset);
|
|
4355
4363
|
}
|
|
4356
4364
|
if (isTsconfig && key === "compilerOptions") {
|
|
4357
|
-
extractCompilerOptions(content, symbols, file, lang,
|
|
4365
|
+
extractCompilerOptions(content, symbols, file, lang, lineOffsets2, line, lineFromOffset);
|
|
4358
4366
|
}
|
|
4359
4367
|
}
|
|
4360
4368
|
const defsRegex = /"\$defs"\s*:|"\$defs"\s*:/g;
|
|
@@ -4367,7 +4375,7 @@ function regexParse2(opts) {
|
|
|
4367
4375
|
name: "$defs",
|
|
4368
4376
|
kind: "property",
|
|
4369
4377
|
line,
|
|
4370
|
-
col: offset - (
|
|
4378
|
+
col: offset - (lineOffsets2[line - 1] ?? 0),
|
|
4371
4379
|
signature: '"$defs": { ... }',
|
|
4372
4380
|
file,
|
|
4373
4381
|
lang
|
|
@@ -4391,7 +4399,7 @@ function regexParse2(opts) {
|
|
|
4391
4399
|
name: key,
|
|
4392
4400
|
kind: "property",
|
|
4393
4401
|
line,
|
|
4394
|
-
col: offset - (
|
|
4402
|
+
col: offset - (lineOffsets2[line - 1] ?? 0),
|
|
4395
4403
|
signature: `"${key}": { ... }`,
|
|
4396
4404
|
file,
|
|
4397
4405
|
lang
|
|
@@ -4401,7 +4409,7 @@ function regexParse2(opts) {
|
|
|
4401
4409
|
}
|
|
4402
4410
|
return { file, lang, symbols, mtimeMs: Date.now() };
|
|
4403
4411
|
}
|
|
4404
|
-
function extractPackageScripts(content, symbols, file, lang,
|
|
4412
|
+
function extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset) {
|
|
4405
4413
|
const scriptsBlockRegex = /"scripts"\s*:\s*\{([^}]+)\}/g;
|
|
4406
4414
|
for (let match = scriptsBlockRegex.exec(content); match !== null; match = scriptsBlockRegex.exec(content)) {
|
|
4407
4415
|
const blockContent = expectDefined(match[0]);
|
|
@@ -4416,7 +4424,7 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFr
|
|
|
4416
4424
|
name: key,
|
|
4417
4425
|
kind: "function",
|
|
4418
4426
|
line,
|
|
4419
|
-
col: keyOffset - (
|
|
4427
|
+
col: keyOffset - (lineOffsets2[line - 1] ?? 0),
|
|
4420
4428
|
signature: `"${key}": "..."`,
|
|
4421
4429
|
file,
|
|
4422
4430
|
lang
|
|
@@ -4425,7 +4433,7 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFr
|
|
|
4425
4433
|
}
|
|
4426
4434
|
}
|
|
4427
4435
|
}
|
|
4428
|
-
function extractCompilerOptions(content, symbols, file, lang,
|
|
4436
|
+
function extractCompilerOptions(content, symbols, file, lang, lineOffsets2, parentLine, lineFromOffset) {
|
|
4429
4437
|
const optsBlockRegex = /"compilerOptions"\s*:\s*\{([^}]+)\}/g;
|
|
4430
4438
|
for (let match = optsBlockRegex.exec(content); match !== null; match = optsBlockRegex.exec(content)) {
|
|
4431
4439
|
const blockContent = expectDefined(match[0]);
|
|
@@ -4441,7 +4449,7 @@ function extractCompilerOptions(content, symbols, file, lang, lineOffsets, paren
|
|
|
4441
4449
|
name: key,
|
|
4442
4450
|
kind: "property",
|
|
4443
4451
|
line,
|
|
4444
|
-
col: keyOffset - (
|
|
4452
|
+
col: keyOffset - (lineOffsets2[line - 1] ?? 0),
|
|
4445
4453
|
signature: `"${key}": ...`,
|
|
4446
4454
|
file,
|
|
4447
4455
|
lang
|
|
@@ -4477,16 +4485,16 @@ function regexParse3(opts) {
|
|
|
4477
4485
|
const { file, content, lang } = opts;
|
|
4478
4486
|
const symbols = [];
|
|
4479
4487
|
const lines = content.split("\n");
|
|
4480
|
-
const
|
|
4488
|
+
const lineOffsets2 = [0];
|
|
4481
4489
|
for (let i = 0; i < lines.length; i++) {
|
|
4482
|
-
|
|
4490
|
+
lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
|
|
4483
4491
|
}
|
|
4484
4492
|
function lineFromOffset(offset) {
|
|
4485
4493
|
let lo = 0;
|
|
4486
|
-
let hi =
|
|
4494
|
+
let hi = lineOffsets2.length - 1;
|
|
4487
4495
|
while (lo < hi) {
|
|
4488
4496
|
const mid = lo + hi + 1 >>> 1;
|
|
4489
|
-
if (expectDefined(
|
|
4497
|
+
if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
|
|
4490
4498
|
else hi = mid - 1;
|
|
4491
4499
|
}
|
|
4492
4500
|
return lo + 1;
|
|
@@ -4496,7 +4504,7 @@ function regexParse3(opts) {
|
|
|
4496
4504
|
const name = expectDefined(match[1]);
|
|
4497
4505
|
const offset = match.index ?? 0;
|
|
4498
4506
|
const line = lineFromOffset(offset);
|
|
4499
|
-
const col = offset - (
|
|
4507
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4500
4508
|
symbols.push(
|
|
4501
4509
|
makeSymbol2({
|
|
4502
4510
|
name,
|
|
@@ -4514,7 +4522,7 @@ function regexParse3(opts) {
|
|
|
4514
4522
|
const name = expectDefined(match[1]);
|
|
4515
4523
|
const offset = match.index ?? 0;
|
|
4516
4524
|
const line = lineFromOffset(offset);
|
|
4517
|
-
const col = offset - (
|
|
4525
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4518
4526
|
symbols.push(
|
|
4519
4527
|
makeSymbol2({
|
|
4520
4528
|
name,
|
|
@@ -4534,7 +4542,7 @@ function regexParse3(opts) {
|
|
|
4534
4542
|
if (!key) continue;
|
|
4535
4543
|
const offset = match.index ?? 0;
|
|
4536
4544
|
const line = lineFromOffset(offset);
|
|
4537
|
-
const col = offset - (
|
|
4545
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4538
4546
|
const lineContent = lines[line - 1] ?? "";
|
|
4539
4547
|
if (/^[|&>]/.test(lineContent.trim())) continue;
|
|
4540
4548
|
if (key === "---" || key === "...") continue;
|
|
@@ -4549,7 +4557,7 @@ function regexParse3(opts) {
|
|
|
4549
4557
|
const key = expectDefined(match[2]);
|
|
4550
4558
|
const offset = match.index ?? 0;
|
|
4551
4559
|
const line = lineFromOffset(offset);
|
|
4552
|
-
const col = offset - (
|
|
4560
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4553
4561
|
const value = extractValue(content, offset + match[0]?.length);
|
|
4554
4562
|
const kind = isScalar(value) ? "literal" : "property";
|
|
4555
4563
|
symbols.push(
|
|
@@ -4569,7 +4577,7 @@ function regexParse3(opts) {
|
|
|
4569
4577
|
const key = expectDefined(match[2]);
|
|
4570
4578
|
const offset = match.index ?? 0;
|
|
4571
4579
|
const line = lineFromOffset(offset);
|
|
4572
|
-
const col = offset - (
|
|
4580
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4573
4581
|
symbols.push(
|
|
4574
4582
|
makeSymbol2({
|
|
4575
4583
|
name: key,
|
|
@@ -4655,7 +4663,7 @@ function compileGitignore(lines) {
|
|
|
4655
4663
|
async function loadGitignoreMatcher(projectRoot) {
|
|
4656
4664
|
let lines = [];
|
|
4657
4665
|
try {
|
|
4658
|
-
const raw = await fs2.readFile(
|
|
4666
|
+
const raw = await fs2.readFile(path12.join(projectRoot, ".gitignore"), "utf8");
|
|
4659
4667
|
lines = raw.split("\n");
|
|
4660
4668
|
} catch {
|
|
4661
4669
|
}
|
|
@@ -4720,14 +4728,14 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
4720
4728
|
dirCount++;
|
|
4721
4729
|
for (const e of entries) {
|
|
4722
4730
|
if (ignoreSet.has(e.name)) continue;
|
|
4723
|
-
const full =
|
|
4724
|
-
const rel =
|
|
4731
|
+
const full = path12.join(dir, e.name);
|
|
4732
|
+
const rel = path12.relative(projectRoot, full).replace(/\\/g, "/");
|
|
4725
4733
|
if (e.isDirectory()) {
|
|
4726
4734
|
if (isGitIgnored(rel, true)) continue;
|
|
4727
4735
|
await walk(full);
|
|
4728
4736
|
} else if (e.isFile()) {
|
|
4729
4737
|
if (isGitIgnored(rel, false)) continue;
|
|
4730
|
-
const ext =
|
|
4738
|
+
const ext = path12.extname(e.name);
|
|
4731
4739
|
for (const { ext: extName, pat } of globs) {
|
|
4732
4740
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
4733
4741
|
results.push(full);
|
|
@@ -4783,7 +4791,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4783
4791
|
let files;
|
|
4784
4792
|
let discoveredFiles = null;
|
|
4785
4793
|
if (opts.files && opts.files.length > 0) {
|
|
4786
|
-
files = opts.files.map((f) =>
|
|
4794
|
+
files = opts.files.map((f) => path12.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path12.relative(projectRoot, f).replace(/\\/g, "/"), false));
|
|
4787
4795
|
} else {
|
|
4788
4796
|
files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
4789
4797
|
discoveredFiles = new Set(files);
|
|
@@ -4811,34 +4819,34 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4811
4819
|
const statOpts = signal ? { signal } : {};
|
|
4812
4820
|
const statReadParse = await Promise.allSettled(
|
|
4813
4821
|
batchFiles.map(async (file) => {
|
|
4814
|
-
let
|
|
4822
|
+
let stat12;
|
|
4815
4823
|
try {
|
|
4816
|
-
|
|
4824
|
+
stat12 = await fs2.stat(file, statOpts);
|
|
4817
4825
|
} catch (e) {
|
|
4818
4826
|
if (isAbortError(e)) throw e;
|
|
4819
4827
|
return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
|
|
4820
4828
|
}
|
|
4821
|
-
if (!
|
|
4829
|
+
if (!stat12.isFile()) return { file, stat: stat12, lang: "", parsed: null };
|
|
4822
4830
|
const lang = detectLang(file);
|
|
4823
|
-
if (!lang) return { file, stat:
|
|
4831
|
+
if (!lang) return { file, stat: stat12, lang: "", parsed: null };
|
|
4824
4832
|
const meta = existingMeta.get(file);
|
|
4825
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
4826
|
-
return { file, stat:
|
|
4833
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat12.mtimeMs)) {
|
|
4834
|
+
return { file, stat: stat12, lang, parsed: null, skippedMeta: meta };
|
|
4827
4835
|
}
|
|
4828
4836
|
let content;
|
|
4829
4837
|
try {
|
|
4830
4838
|
content = await fs2.readFile(file, { encoding: "utf8", signal });
|
|
4831
4839
|
} catch (e) {
|
|
4832
4840
|
if (isAbortError(e)) throw e;
|
|
4833
|
-
return { file, stat:
|
|
4841
|
+
return { file, stat: stat12, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
|
|
4834
4842
|
}
|
|
4835
4843
|
let parsed;
|
|
4836
4844
|
try {
|
|
4837
4845
|
parsed = await parseFile(file, content, lang);
|
|
4838
4846
|
} catch (e) {
|
|
4839
|
-
return { file, stat:
|
|
4847
|
+
return { file, stat: stat12, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
|
|
4840
4848
|
}
|
|
4841
|
-
return { file, stat:
|
|
4849
|
+
return { file, stat: stat12, lang, parsed, content };
|
|
4842
4850
|
})
|
|
4843
4851
|
);
|
|
4844
4852
|
const batchEntries = [];
|
|
@@ -4858,7 +4866,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4858
4866
|
if (result.error.includes("error:")) errors.push(result.error);
|
|
4859
4867
|
continue;
|
|
4860
4868
|
}
|
|
4861
|
-
const { stat:
|
|
4869
|
+
const { stat: stat12, lang, parsed } = result;
|
|
4862
4870
|
if (result.skippedMeta) {
|
|
4863
4871
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
4864
4872
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -4870,7 +4878,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4870
4878
|
store.upsertFile({
|
|
4871
4879
|
file,
|
|
4872
4880
|
lang,
|
|
4873
|
-
mtimeMs: Math.floor(
|
|
4881
|
+
mtimeMs: Math.floor(stat12.mtimeMs),
|
|
4874
4882
|
symbolCount: 0,
|
|
4875
4883
|
lastIndexed: Date.now()
|
|
4876
4884
|
});
|
|
@@ -4882,7 +4890,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4882
4890
|
store.upsertFile({
|
|
4883
4891
|
file,
|
|
4884
4892
|
lang,
|
|
4885
|
-
mtimeMs: Math.floor(
|
|
4893
|
+
mtimeMs: Math.floor(stat12.mtimeMs),
|
|
4886
4894
|
symbolCount: 0,
|
|
4887
4895
|
lastIndexed: Date.now()
|
|
4888
4896
|
});
|
|
@@ -4898,7 +4906,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4898
4906
|
lang,
|
|
4899
4907
|
symbols: parsed.symbols,
|
|
4900
4908
|
refs,
|
|
4901
|
-
mtimeMs: Math.floor(
|
|
4909
|
+
mtimeMs: Math.floor(stat12.mtimeMs),
|
|
4902
4910
|
symbolCount: parsed.symbols.length
|
|
4903
4911
|
});
|
|
4904
4912
|
deleteForFiles.push(file);
|
|
@@ -5047,7 +5055,7 @@ function statsService(args) {
|
|
|
5047
5055
|
}
|
|
5048
5056
|
|
|
5049
5057
|
// src/codebase-index/background-indexer.ts
|
|
5050
|
-
var DEFAULT_FULL_INDEX_TIMEOUT_MS =
|
|
5058
|
+
var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
|
|
5051
5059
|
var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
|
|
5052
5060
|
var _ready = false;
|
|
5053
5061
|
var _indexing = false;
|
|
@@ -5504,6 +5512,25 @@ var codebaseStatsTool = {
|
|
|
5504
5512
|
};
|
|
5505
5513
|
}
|
|
5506
5514
|
};
|
|
5515
|
+
async function resolveReal(p) {
|
|
5516
|
+
const resolved = path12.resolve(p);
|
|
5517
|
+
let probe = resolved;
|
|
5518
|
+
const missing = [];
|
|
5519
|
+
for (; ; ) {
|
|
5520
|
+
try {
|
|
5521
|
+
return path12.resolve(await fs2.realpath(probe), ...missing);
|
|
5522
|
+
} catch (err) {
|
|
5523
|
+
if (err.code === "ENOENT") {
|
|
5524
|
+
const parent = path12.dirname(probe);
|
|
5525
|
+
if (parent === probe) return resolved;
|
|
5526
|
+
missing.unshift(path12.basename(probe));
|
|
5527
|
+
probe = parent;
|
|
5528
|
+
continue;
|
|
5529
|
+
}
|
|
5530
|
+
return resolved;
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5507
5534
|
function normalizeOverrides(set) {
|
|
5508
5535
|
const out = {};
|
|
5509
5536
|
if (set && typeof set === "object") {
|
|
@@ -5661,10 +5688,12 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
|
|
|
5661
5688
|
kitId: active.kit,
|
|
5662
5689
|
outPath: input.out
|
|
5663
5690
|
});
|
|
5664
|
-
const root =
|
|
5665
|
-
const
|
|
5666
|
-
const
|
|
5667
|
-
|
|
5691
|
+
const root = await resolveReal(ctx.projectRoot);
|
|
5692
|
+
const absResolved = path12.isAbsolute(result.path) ? path12.resolve(result.path) : path12.resolve(path12.join(ctx.projectRoot, result.path));
|
|
5693
|
+
const absParent = await resolveReal(path12.dirname(absResolved));
|
|
5694
|
+
const abs = path12.join(absParent, path12.basename(absResolved));
|
|
5695
|
+
const rel = path12.relative(root, abs);
|
|
5696
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) {
|
|
5668
5697
|
throw new Error(
|
|
5669
5698
|
`design: materialize path "${result.path}" would escape the project root`
|
|
5670
5699
|
);
|
|
@@ -5688,7 +5717,7 @@ ${result.content}
|
|
|
5688
5717
|
\`\`\``
|
|
5689
5718
|
};
|
|
5690
5719
|
}
|
|
5691
|
-
await fs2.mkdir(
|
|
5720
|
+
await fs2.mkdir(path12.dirname(abs), { recursive: true });
|
|
5692
5721
|
await fs2.writeFile(abs, result.content);
|
|
5693
5722
|
return {
|
|
5694
5723
|
action,
|
|
@@ -5816,11 +5845,11 @@ function findGitDir(cwd) {
|
|
|
5816
5845
|
let dir = cwd;
|
|
5817
5846
|
for (let i = 0; i < 20; i++) {
|
|
5818
5847
|
try {
|
|
5819
|
-
const
|
|
5820
|
-
if (
|
|
5848
|
+
const stat12 = statSync(path12.join(dir, ".git"));
|
|
5849
|
+
if (stat12.isDirectory()) return dir;
|
|
5821
5850
|
} catch {
|
|
5822
5851
|
}
|
|
5823
|
-
const parent =
|
|
5852
|
+
const parent = path12.dirname(dir);
|
|
5824
5853
|
if (parent === dir) break;
|
|
5825
5854
|
dir = parent;
|
|
5826
5855
|
}
|
|
@@ -5861,8 +5890,8 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
5861
5890
|
const results = [];
|
|
5862
5891
|
for (const file of files) {
|
|
5863
5892
|
const absPath = safeResolve(file, ctx);
|
|
5864
|
-
const
|
|
5865
|
-
if (!
|
|
5893
|
+
const stat12 = await fs2.stat(absPath).catch(() => null);
|
|
5894
|
+
if (!stat12?.isFile()) continue;
|
|
5866
5895
|
const content = await fs2.readFile(absPath, "utf8");
|
|
5867
5896
|
const lines = content.split(/\r?\n/);
|
|
5868
5897
|
results.push(formatWithLineNumbers(file, lines));
|
|
@@ -5959,8 +5988,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
5959
5988
|
for (const f of files) {
|
|
5960
5989
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
5961
5990
|
try {
|
|
5962
|
-
const
|
|
5963
|
-
if (
|
|
5991
|
+
const stat12 = await fs2.stat(absPath);
|
|
5992
|
+
if (stat12.isFile()) resolved.push(absPath);
|
|
5964
5993
|
} catch {
|
|
5965
5994
|
}
|
|
5966
5995
|
}
|
|
@@ -6026,11 +6055,303 @@ function processFile(content, absPath, _style, _overwrite, target) {
|
|
|
6026
6055
|
}
|
|
6027
6056
|
return results;
|
|
6028
6057
|
}
|
|
6058
|
+
|
|
6059
|
+
// src/_edit-match.ts
|
|
6060
|
+
var TIER_LABEL = {
|
|
6061
|
+
exact: "exact match",
|
|
6062
|
+
"trailing-whitespace": "whitespace-normalized match (trailing whitespace ignored)",
|
|
6063
|
+
"whitespace-normalized": "whitespace-normalized match (indentation ignored, replacement re-indented)",
|
|
6064
|
+
fuzzy: "fuzzy match (block anchors exact, interior \u226590% similar)"
|
|
6065
|
+
};
|
|
6066
|
+
var TIER_CONFIDENCE = {
|
|
6067
|
+
exact: "exact",
|
|
6068
|
+
"trailing-whitespace": "high",
|
|
6069
|
+
"whitespace-normalized": "medium",
|
|
6070
|
+
fuzzy: "low"
|
|
6071
|
+
};
|
|
6072
|
+
var MIN_NORMALIZED_NEEDLE_CHARS = 16;
|
|
6073
|
+
var FUZZY_MIN_SIMILARITY = 0.9;
|
|
6074
|
+
var FUZZY_AMBIGUITY_MARGIN = 0.05;
|
|
6075
|
+
var FUZZY_MAX_INTERIOR_CHARS = 2e3;
|
|
6076
|
+
function findLadderMatches(fileLf, oldLf) {
|
|
6077
|
+
const exact = [];
|
|
6078
|
+
let idx = fileLf.indexOf(oldLf);
|
|
6079
|
+
while (idx !== -1) {
|
|
6080
|
+
exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });
|
|
6081
|
+
idx = fileLf.indexOf(oldLf, idx + 1);
|
|
6082
|
+
}
|
|
6083
|
+
if (exact.length > 0) return { tier: "exact", matches: exact };
|
|
6084
|
+
const fileLines = fileLf.split("\n");
|
|
6085
|
+
const needleLines = oldLf.split("\n");
|
|
6086
|
+
if (needleLines.length > fileLines.length) return void 0;
|
|
6087
|
+
const offsets = lineOffsets(fileLines);
|
|
6088
|
+
const trailing = windowScan(
|
|
6089
|
+
fileLines,
|
|
6090
|
+
needleLines,
|
|
6091
|
+
offsets,
|
|
6092
|
+
(a, b) => a.trimEnd() === b.trimEnd()
|
|
6093
|
+
);
|
|
6094
|
+
if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
|
|
6095
|
+
const normalizedLen = needleLines.reduce((n, l) => n + l.trim().length, 0);
|
|
6096
|
+
if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
|
|
6097
|
+
const normalized = windowScan(fileLines, needleLines, offsets, (a, b) => a.trim() === b.trim());
|
|
6098
|
+
if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
|
|
6099
|
+
return fuzzyScan(fileLines, needleLines, offsets);
|
|
6100
|
+
}
|
|
6101
|
+
function lineAt(text, pos) {
|
|
6102
|
+
let line = 1;
|
|
6103
|
+
for (let i = 0; i < pos; i++) {
|
|
6104
|
+
if (text.charCodeAt(i) === 10) line++;
|
|
6105
|
+
}
|
|
6106
|
+
return line;
|
|
6107
|
+
}
|
|
6108
|
+
function lineOffsets(lines) {
|
|
6109
|
+
const out = new Array(lines.length);
|
|
6110
|
+
let pos = 0;
|
|
6111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6112
|
+
out[i] = pos;
|
|
6113
|
+
pos += lines[i].length + 1;
|
|
6114
|
+
}
|
|
6115
|
+
return out;
|
|
6116
|
+
}
|
|
6117
|
+
function windowToMatch(fileLines, offsets, start, windowLen) {
|
|
6118
|
+
const lastLine = start + windowLen - 1;
|
|
6119
|
+
return {
|
|
6120
|
+
start: offsets[start],
|
|
6121
|
+
end: offsets[lastLine] + fileLines[lastLine].length,
|
|
6122
|
+
startLine: start + 1
|
|
6123
|
+
};
|
|
6124
|
+
}
|
|
6125
|
+
function windowScan(fileLines, needleLines, offsets, eq) {
|
|
6126
|
+
const n = needleLines.length;
|
|
6127
|
+
const out = [];
|
|
6128
|
+
for (let i = 0; i + n <= fileLines.length; i++) {
|
|
6129
|
+
let all = true;
|
|
6130
|
+
for (let j = 0; j < n; j++) {
|
|
6131
|
+
if (!eq(fileLines[i + j], needleLines[j])) {
|
|
6132
|
+
all = false;
|
|
6133
|
+
break;
|
|
6134
|
+
}
|
|
6135
|
+
}
|
|
6136
|
+
if (all) {
|
|
6137
|
+
out.push(windowToMatch(fileLines, offsets, i, n));
|
|
6138
|
+
i += n - 1;
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
return out;
|
|
6142
|
+
}
|
|
6143
|
+
function fuzzyScan(fileLines, needleLines, offsets) {
|
|
6144
|
+
const n = needleLines.length;
|
|
6145
|
+
if (n < 3) return void 0;
|
|
6146
|
+
const firstNeedle = needleLines[0].trim();
|
|
6147
|
+
const lastNeedle = needleLines[n - 1].trim();
|
|
6148
|
+
const needleInterior = needleLines.slice(1, -1).map((l) => l.trim()).join("\n");
|
|
6149
|
+
if (needleInterior.length > FUZZY_MAX_INTERIOR_CHARS) return void 0;
|
|
6150
|
+
const candidates = [];
|
|
6151
|
+
for (let i = 0; i + n <= fileLines.length; i++) {
|
|
6152
|
+
if (fileLines[i].trim() !== firstNeedle) continue;
|
|
6153
|
+
if (fileLines[i + n - 1].trim() !== lastNeedle) continue;
|
|
6154
|
+
const windowInterior = fileLines.slice(i + 1, i + n - 1).map((l) => l.trim()).join("\n");
|
|
6155
|
+
if (windowInterior.length > FUZZY_MAX_INTERIOR_CHARS) continue;
|
|
6156
|
+
const score = similarity(needleInterior, windowInterior);
|
|
6157
|
+
if (score >= FUZZY_MIN_SIMILARITY) {
|
|
6158
|
+
candidates.push({ match: windowToMatch(fileLines, offsets, i, n), score });
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
if (candidates.length === 0) return void 0;
|
|
6162
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
6163
|
+
const best = candidates[0];
|
|
6164
|
+
const runnerUp = candidates[1];
|
|
6165
|
+
if (runnerUp && best.score - runnerUp.score < FUZZY_AMBIGUITY_MARGIN) {
|
|
6166
|
+
return {
|
|
6167
|
+
tier: "fuzzy",
|
|
6168
|
+
matches: candidates.map((c) => c.match),
|
|
6169
|
+
score: best.score,
|
|
6170
|
+
ambiguous: true
|
|
6171
|
+
};
|
|
6172
|
+
}
|
|
6173
|
+
return { tier: "fuzzy", matches: [best.match], score: best.score };
|
|
6174
|
+
}
|
|
6175
|
+
function similarity(a, b) {
|
|
6176
|
+
if (a === b) return 1;
|
|
6177
|
+
const max = Math.max(a.length, b.length);
|
|
6178
|
+
if (max === 0) return 1;
|
|
6179
|
+
if (Math.abs(a.length - b.length) / max > 1 - FUZZY_MIN_SIMILARITY + 0.05) {
|
|
6180
|
+
return 1 - Math.abs(a.length - b.length) / max;
|
|
6181
|
+
}
|
|
6182
|
+
return 1 - levenshtein(a, b) / max;
|
|
6183
|
+
}
|
|
6184
|
+
function levenshtein(a, b) {
|
|
6185
|
+
if (a.length === 0) return b.length;
|
|
6186
|
+
if (b.length === 0) return a.length;
|
|
6187
|
+
let prev = new Array(b.length + 1);
|
|
6188
|
+
let cur = new Array(b.length + 1);
|
|
6189
|
+
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
6190
|
+
for (let i = 1; i <= a.length; i++) {
|
|
6191
|
+
cur[0] = i;
|
|
6192
|
+
const ca = a.charCodeAt(i - 1);
|
|
6193
|
+
for (let j = 1; j <= b.length; j++) {
|
|
6194
|
+
const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
|
|
6195
|
+
cur[j] = Math.min(
|
|
6196
|
+
prev[j] + 1,
|
|
6197
|
+
cur[j - 1] + 1,
|
|
6198
|
+
prev[j - 1] + cost
|
|
6199
|
+
);
|
|
6200
|
+
}
|
|
6201
|
+
[prev, cur] = [cur, prev];
|
|
6202
|
+
}
|
|
6203
|
+
return prev[b.length];
|
|
6204
|
+
}
|
|
6205
|
+
function firstLineIndent(text) {
|
|
6206
|
+
for (const line of text.split("\n")) {
|
|
6207
|
+
if (line.trim() === "") continue;
|
|
6208
|
+
return /^[ \t]*/.exec(line)[0];
|
|
6209
|
+
}
|
|
6210
|
+
return "";
|
|
6211
|
+
}
|
|
6212
|
+
function adjustIndent(newLf, fromIndent, toIndent) {
|
|
6213
|
+
if (fromIndent === toIndent) return { text: newLf, adjusted: false };
|
|
6214
|
+
const lines = newLf.split("\n");
|
|
6215
|
+
if (toIndent.startsWith(fromIndent)) {
|
|
6216
|
+
const extra = toIndent.slice(fromIndent.length);
|
|
6217
|
+
return {
|
|
6218
|
+
text: lines.map((l) => l.trim() === "" ? l : extra + l).join("\n"),
|
|
6219
|
+
adjusted: true
|
|
6220
|
+
};
|
|
6221
|
+
}
|
|
6222
|
+
if (fromIndent.startsWith(toIndent)) {
|
|
6223
|
+
const remove = fromIndent.slice(toIndent.length);
|
|
6224
|
+
return {
|
|
6225
|
+
text: lines.map((l) => l.startsWith(remove) ? l.slice(remove.length) : l).join("\n"),
|
|
6226
|
+
adjusted: true
|
|
6227
|
+
};
|
|
6228
|
+
}
|
|
6229
|
+
return { text: newLf, adjusted: false };
|
|
6230
|
+
}
|
|
6231
|
+
function nearestMatchHint(fileLf, oldLf) {
|
|
6232
|
+
const fileLines = fileLf.split("\n");
|
|
6233
|
+
const needleLines = oldLf.split("\n");
|
|
6234
|
+
if (needleLines.length > fileLines.length) return void 0;
|
|
6235
|
+
const needleTrimmed = needleLines.map((l) => l.trim());
|
|
6236
|
+
let bestScore = 0;
|
|
6237
|
+
let bestStart = -1;
|
|
6238
|
+
for (let i = 0; i + needleLines.length <= fileLines.length; i++) {
|
|
6239
|
+
let sum = 0;
|
|
6240
|
+
for (let j = 0; j < needleLines.length; j++) {
|
|
6241
|
+
sum += prefixSimilarity(needleTrimmed[j], fileLines[i + j].trim());
|
|
6242
|
+
}
|
|
6243
|
+
const score = sum / needleLines.length;
|
|
6244
|
+
if (score > bestScore) {
|
|
6245
|
+
bestScore = score;
|
|
6246
|
+
bestStart = i;
|
|
6247
|
+
}
|
|
6248
|
+
}
|
|
6249
|
+
if (bestStart === -1 || bestScore < 0.5) return void 0;
|
|
6250
|
+
const snippet = fileLines.slice(bestStart, bestStart + Math.min(3, needleLines.length)).map((l) => l.length > 120 ? `${l.slice(0, 120)}\u2026` : l).join("\n");
|
|
6251
|
+
return { line: bestStart + 1, snippet };
|
|
6252
|
+
}
|
|
6253
|
+
function prefixSimilarity(a, b) {
|
|
6254
|
+
if (a === b) return 1;
|
|
6255
|
+
const max = Math.max(a.length, b.length);
|
|
6256
|
+
if (max === 0) return 1;
|
|
6257
|
+
let p = 0;
|
|
6258
|
+
const lim = Math.min(a.length, b.length);
|
|
6259
|
+
while (p < lim && a.charCodeAt(p) === b.charCodeAt(p)) p++;
|
|
6260
|
+
return p / max;
|
|
6261
|
+
}
|
|
6262
|
+
var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
6263
|
+
var MAX_CHECK_CHARS = 15e5;
|
|
6264
|
+
var MAX_ERRORS = 5;
|
|
6265
|
+
var tsLoad = null;
|
|
6266
|
+
function loadTypescript() {
|
|
6267
|
+
tsLoad ??= import('typescript').then(
|
|
6268
|
+
(m) => m.default ?? m,
|
|
6269
|
+
() => null
|
|
6270
|
+
);
|
|
6271
|
+
return tsLoad;
|
|
6272
|
+
}
|
|
6273
|
+
async function checkSyntax(filePath, content, previousContent) {
|
|
6274
|
+
const errors = await parseErrors(filePath, content);
|
|
6275
|
+
if (errors === void 0) return void 0;
|
|
6276
|
+
if (errors.length === 0) return { errors, preExisting: false };
|
|
6277
|
+
let preExisting = false;
|
|
6278
|
+
if (previousContent !== void 0) {
|
|
6279
|
+
const prevErrors = await parseErrors(filePath, previousContent);
|
|
6280
|
+
preExisting = prevErrors !== void 0 && prevErrors.length > 0;
|
|
6281
|
+
}
|
|
6282
|
+
return { errors, preExisting };
|
|
6283
|
+
}
|
|
6284
|
+
function isJsoncFile(filePath) {
|
|
6285
|
+
const base = path12.basename(filePath).toLowerCase();
|
|
6286
|
+
if (base.endsWith(".jsonc")) return true;
|
|
6287
|
+
if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
|
|
6288
|
+
const dir = path12.basename(path12.dirname(filePath)).toLowerCase();
|
|
6289
|
+
return dir === ".vscode";
|
|
6290
|
+
}
|
|
6291
|
+
async function parseErrors(filePath, content) {
|
|
6292
|
+
if (content.length > MAX_CHECK_CHARS) return void 0;
|
|
6293
|
+
const ext = path12.extname(filePath).toLowerCase();
|
|
6294
|
+
if (ext === ".json" || ext === ".jsonc") {
|
|
6295
|
+
try {
|
|
6296
|
+
JSON.parse(content);
|
|
6297
|
+
return [];
|
|
6298
|
+
} catch (err) {
|
|
6299
|
+
if (isJsoncFile(filePath)) {
|
|
6300
|
+
const ts3 = await loadTypescript();
|
|
6301
|
+
if (ts3) {
|
|
6302
|
+
const jsonc = ts3.parseConfigFileTextToJson(filePath, content);
|
|
6303
|
+
if (!jsonc.error) return [];
|
|
6304
|
+
return [formatDiag(ts3, jsonc.error, content)];
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
return [`JSON parse error: ${err.message}`];
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
if (!TS_LIKE.has(ext)) return void 0;
|
|
6311
|
+
const ts2 = await loadTypescript();
|
|
6312
|
+
if (!ts2) return void 0;
|
|
6313
|
+
const scriptKind = ext === ".tsx" ? ts2.ScriptKind.TSX : ext === ".ts" || ext === ".mts" || ext === ".cts" ? ts2.ScriptKind.TS : (
|
|
6314
|
+
// Plain JS may legitimately contain JSX; the JSX grammar is a
|
|
6315
|
+
// superset for untyped code, so parsing .js/.jsx as JSX avoids
|
|
6316
|
+
// false positives on React files.
|
|
6317
|
+
ts2.ScriptKind.JSX
|
|
6318
|
+
);
|
|
6319
|
+
const sourceFile = ts2.createSourceFile(
|
|
6320
|
+
path12.basename(filePath),
|
|
6321
|
+
content,
|
|
6322
|
+
ts2.ScriptTarget.Latest,
|
|
6323
|
+
/* setParentNodes */
|
|
6324
|
+
false,
|
|
6325
|
+
scriptKind
|
|
6326
|
+
);
|
|
6327
|
+
const diags = sourceFile.parseDiagnostics ?? [];
|
|
6328
|
+
return diags.slice(0, MAX_ERRORS).map((d) => formatDiag(ts2, d, content, sourceFile));
|
|
6329
|
+
}
|
|
6330
|
+
function formatDiag(ts2, diag, content, sourceFile) {
|
|
6331
|
+
const message = ts2.flattenDiagnosticMessageText(diag.messageText, " ");
|
|
6332
|
+
if (diag.start === void 0) return message;
|
|
6333
|
+
let line;
|
|
6334
|
+
if (sourceFile) {
|
|
6335
|
+
line = sourceFile.getLineAndCharacterOfPosition(diag.start).line + 1;
|
|
6336
|
+
} else {
|
|
6337
|
+
line = 1;
|
|
6338
|
+
for (let i = 0; i < diag.start && i < content.length; i++) {
|
|
6339
|
+
if (content.charCodeAt(i) === 10) line++;
|
|
6340
|
+
}
|
|
6341
|
+
}
|
|
6342
|
+
return `line ${line}: ${message}`;
|
|
6343
|
+
}
|
|
6344
|
+
|
|
6345
|
+
// src/edit.ts
|
|
6029
6346
|
var editTool = {
|
|
6030
6347
|
name: "edit",
|
|
6031
6348
|
category: "Filesystem",
|
|
6032
6349
|
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.",
|
|
6033
|
-
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.",
|
|
6350
|
+
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.",
|
|
6351
|
+
selection: {
|
|
6352
|
+
doNotUseWhen: "creating a new file, replacing the whole file, or applying an existing unified diff.",
|
|
6353
|
+
useInstead: ["write", "patch"]
|
|
6354
|
+
},
|
|
6034
6355
|
permission: "confirm",
|
|
6035
6356
|
mutating: true,
|
|
6036
6357
|
capabilities: ["fs.write"],
|
|
@@ -6046,7 +6367,7 @@ var editTool = {
|
|
|
6046
6367
|
},
|
|
6047
6368
|
required: ["path", "old_string", "new_string"]
|
|
6048
6369
|
},
|
|
6049
|
-
async execute(input, ctx) {
|
|
6370
|
+
async execute(input, ctx, opts) {
|
|
6050
6371
|
if (!input?.path) {
|
|
6051
6372
|
throw new ToolValidationError({ message: "edit: path is required", field: "path" });
|
|
6052
6373
|
}
|
|
@@ -6069,7 +6390,7 @@ var editTool = {
|
|
|
6069
6390
|
});
|
|
6070
6391
|
}
|
|
6071
6392
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
6072
|
-
const
|
|
6393
|
+
const stat12 = await fs2.stat(absPath).catch((err) => {
|
|
6073
6394
|
if (err.code === "ENOENT") {
|
|
6074
6395
|
throw new ToolValidationError({
|
|
6075
6396
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
@@ -6079,7 +6400,7 @@ var editTool = {
|
|
|
6079
6400
|
}
|
|
6080
6401
|
throw err;
|
|
6081
6402
|
});
|
|
6082
|
-
if (!
|
|
6403
|
+
if (!stat12.isFile()) {
|
|
6083
6404
|
throw new ToolValidationError({
|
|
6084
6405
|
message: `edit: "${input.path}" is not a regular file`,
|
|
6085
6406
|
field: "path"
|
|
@@ -6089,15 +6410,27 @@ var editTool = {
|
|
|
6089
6410
|
const original = await fs2.readFile(absPath, "utf8");
|
|
6090
6411
|
const updated = await fs2.stat(absPath);
|
|
6091
6412
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
6092
|
-
const
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6413
|
+
const originalHash = sha256hex(original);
|
|
6414
|
+
const lastReadHash = ctx.lastReadHash?.(absPath);
|
|
6415
|
+
if (lastReadHash !== void 0) {
|
|
6416
|
+
if (lastReadHash !== originalHash) {
|
|
6417
|
+
throw new ToolValidationError({
|
|
6418
|
+
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
6419
|
+
field: "path",
|
|
6420
|
+
context: { reason: "external_modification" }
|
|
6421
|
+
});
|
|
6422
|
+
}
|
|
6423
|
+
} else {
|
|
6424
|
+
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
6425
|
+
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
6426
|
+
throw new ToolValidationError({
|
|
6427
|
+
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
6428
|
+
field: "path",
|
|
6429
|
+
context: { reason: "external_modification" }
|
|
6430
|
+
});
|
|
6431
|
+
}
|
|
6099
6432
|
}
|
|
6100
|
-
if (autoRead && updated.mtimeMs >
|
|
6433
|
+
if (autoRead && updated.mtimeMs > stat12.mtimeMs + mtimeTolerance) {
|
|
6101
6434
|
throw new ToolValidationError({
|
|
6102
6435
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
6103
6436
|
field: "path",
|
|
@@ -6110,42 +6443,78 @@ var editTool = {
|
|
|
6110
6443
|
const oldLf = normalizeToLf(input.old_string);
|
|
6111
6444
|
const newLf = normalizeToLf(input.new_string);
|
|
6112
6445
|
if (oldLf === newLf) {
|
|
6113
|
-
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs);
|
|
6446
|
+
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
|
|
6114
6447
|
return {
|
|
6115
6448
|
path: absPath,
|
|
6116
6449
|
replacements: 0,
|
|
6117
6450
|
diff: "(no-op: old and new are identical)",
|
|
6451
|
+
matched_by: "exact",
|
|
6118
6452
|
note: autoReadNote
|
|
6119
6453
|
};
|
|
6120
6454
|
}
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
while (idx !== -1) {
|
|
6125
|
-
matches.push(idx);
|
|
6126
|
-
count++;
|
|
6127
|
-
idx = fileLf.indexOf(oldLf, idx + 1);
|
|
6128
|
-
}
|
|
6129
|
-
if (count === 0) {
|
|
6130
|
-
const hint = findSimilarity(fileLf, oldLf);
|
|
6455
|
+
const ladder = findLadderMatches(fileLf, oldLf);
|
|
6456
|
+
if (!ladder) {
|
|
6457
|
+
const hint = nearestMatchHint(fileLf, oldLf);
|
|
6131
6458
|
throw new ToolValidationError({
|
|
6132
|
-
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}
|
|
6459
|
+
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
|
|
6460
|
+
${hint.snippet}
|
|
6461
|
+
Compare this against your old_string and retry with the file's actual text.` : ""}`,
|
|
6133
6462
|
field: "old_string"
|
|
6134
6463
|
});
|
|
6135
6464
|
}
|
|
6465
|
+
const { tier, matches } = ladder;
|
|
6466
|
+
const count = matches.length;
|
|
6467
|
+
if (ladder.ambiguous) {
|
|
6468
|
+
const lines = matches.map((m) => m.startLine);
|
|
6469
|
+
throw new ToolValidationError({
|
|
6470
|
+
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.`,
|
|
6471
|
+
field: "old_string",
|
|
6472
|
+
context: { occurrences: count, matchTier: tier }
|
|
6473
|
+
});
|
|
6474
|
+
}
|
|
6475
|
+
if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
|
|
6476
|
+
throw new ToolValidationError({
|
|
6477
|
+
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.`,
|
|
6478
|
+
field: "old_string",
|
|
6479
|
+
context: { matchTier: tier }
|
|
6480
|
+
});
|
|
6481
|
+
}
|
|
6136
6482
|
if (count > 1 && !input.replace_all) {
|
|
6137
|
-
const lines =
|
|
6483
|
+
const lines = matches.map((m) => m.startLine);
|
|
6138
6484
|
throw new ToolValidationError({
|
|
6139
|
-
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.`,
|
|
6485
|
+
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.`,
|
|
6140
6486
|
field: "old_string",
|
|
6141
|
-
context: { occurrences: count }
|
|
6487
|
+
context: { occurrences: count, matchTier: tier }
|
|
6142
6488
|
});
|
|
6143
6489
|
}
|
|
6144
|
-
|
|
6490
|
+
let newFileLf;
|
|
6491
|
+
let tierNote;
|
|
6492
|
+
if (tier === "exact") {
|
|
6493
|
+
newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
6494
|
+
} else {
|
|
6495
|
+
let replacement = newLf;
|
|
6496
|
+
let indentSuffix = "";
|
|
6497
|
+
if (tier === "whitespace-normalized" || tier === "fuzzy") {
|
|
6498
|
+
const first = matches[0];
|
|
6499
|
+
const matchedText = fileLf.slice(first.start, first.end);
|
|
6500
|
+
const adjusted = adjustIndent(newLf, firstLineIndent(oldLf), firstLineIndent(matchedText));
|
|
6501
|
+
replacement = adjusted.text;
|
|
6502
|
+
if (adjusted.adjusted) indentSuffix = "; replacement re-indented to match the file";
|
|
6503
|
+
}
|
|
6504
|
+
newFileLf = fileLf;
|
|
6505
|
+
const applied = input.replace_all ? matches : matches.slice(0, 1);
|
|
6506
|
+
for (let i = applied.length - 1; i >= 0; i--) {
|
|
6507
|
+
const m = applied[i];
|
|
6508
|
+
newFileLf = newFileLf.slice(0, m.start) + replacement + newFileLf.slice(m.end);
|
|
6509
|
+
}
|
|
6510
|
+
const scoreSuffix = ladder.score !== void 0 ? `, similarity ${(ladder.score * 100).toFixed(1)}%` : "";
|
|
6511
|
+
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.`;
|
|
6512
|
+
}
|
|
6145
6513
|
const newFile = toStyle(newFileLf, style);
|
|
6514
|
+
opts?.signal?.throwIfAborted();
|
|
6146
6515
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
6147
6516
|
const written = await fs2.stat(absPath);
|
|
6148
|
-
ctx.recordRead(absPath, written.mtimeMs, "write");
|
|
6517
|
+
ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
|
|
6149
6518
|
ctx.session.recordFileChange({
|
|
6150
6519
|
path: absPath,
|
|
6151
6520
|
action: "modified",
|
|
@@ -6156,38 +6525,22 @@ var editTool = {
|
|
|
6156
6525
|
fromFile: input.path,
|
|
6157
6526
|
toFile: input.path
|
|
6158
6527
|
});
|
|
6528
|
+
const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
|
|
6529
|
+
let syntaxNote;
|
|
6530
|
+
if (syntax && syntax.errors.length > 0) {
|
|
6531
|
+
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.`;
|
|
6532
|
+
}
|
|
6533
|
+
const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
|
|
6159
6534
|
return {
|
|
6160
6535
|
path: absPath,
|
|
6161
6536
|
replacements: input.replace_all ? count : 1,
|
|
6162
6537
|
diff,
|
|
6163
|
-
|
|
6538
|
+
matched_by: tier,
|
|
6539
|
+
syntax_errors: syntax && syntax.errors.length > 0 ? syntax.errors : void 0,
|
|
6540
|
+
note: notes.length > 0 ? notes.join("\n") : void 0
|
|
6164
6541
|
};
|
|
6165
6542
|
}
|
|
6166
6543
|
};
|
|
6167
|
-
function lineNumbersFor(text, indices) {
|
|
6168
|
-
const out = [];
|
|
6169
|
-
let pos = 0;
|
|
6170
|
-
let line = 1;
|
|
6171
|
-
for (const target of indices) {
|
|
6172
|
-
while (pos < target) {
|
|
6173
|
-
if (text.charCodeAt(pos) === 10) line++;
|
|
6174
|
-
pos++;
|
|
6175
|
-
}
|
|
6176
|
-
out.push(line);
|
|
6177
|
-
}
|
|
6178
|
-
return out;
|
|
6179
|
-
}
|
|
6180
|
-
function findSimilarity(haystack, needle) {
|
|
6181
|
-
if (needle.length < 20) return void 0;
|
|
6182
|
-
const probe = needle.slice(0, Math.min(40, needle.length));
|
|
6183
|
-
const idx = haystack.indexOf(probe);
|
|
6184
|
-
if (idx === -1) return void 0;
|
|
6185
|
-
let line = 1;
|
|
6186
|
-
for (let i = 0; i < idx; i++) {
|
|
6187
|
-
if (haystack.charCodeAt(i) === 10) line++;
|
|
6188
|
-
}
|
|
6189
|
-
return line;
|
|
6190
|
-
}
|
|
6191
6544
|
|
|
6192
6545
|
// src/_danger-detect.ts
|
|
6193
6546
|
var argHas = (args, value) => args.includes(value);
|
|
@@ -6400,23 +6753,18 @@ var RULES = [
|
|
|
6400
6753
|
},
|
|
6401
6754
|
reason: "inline script evaluation (-c / -e / --eval)"
|
|
6402
6755
|
},
|
|
6403
|
-
// ----- pipe-to-shell (
|
|
6756
|
+
// ----- pipe-to-shell (destructive — download-and-run pattern) -----
|
|
6404
6757
|
// The classic `curl https://... | sh` download-and-run vector. Detected by
|
|
6405
|
-
// looking for a known fetcher
|
|
6406
|
-
//
|
|
6407
|
-
//
|
|
6758
|
+
// looking for a known fetcher piped into a shell or expression evaluator.
|
|
6759
|
+
// A shell command passed through `bash -c` / `pwsh -Command` arrives as one
|
|
6760
|
+
// argv string, so scan the reconstructed argv text rather than individual
|
|
6761
|
+
// tokens only.
|
|
6408
6762
|
{
|
|
6409
6763
|
id: "pipe-to-shell",
|
|
6410
|
-
level: "
|
|
6411
|
-
test: (
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
);
|
|
6415
|
-
const hasShellSink = args.some(
|
|
6416
|
-
(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")
|
|
6417
|
-
);
|
|
6418
|
-
return hasFetcher && hasShellSink;
|
|
6419
|
-
},
|
|
6764
|
+
level: "destructive",
|
|
6765
|
+
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(
|
|
6766
|
+
[cmd, ...args].join(" ")
|
|
6767
|
+
),
|
|
6420
6768
|
reason: "network fetch piped to a shell (download-and-run pattern)"
|
|
6421
6769
|
},
|
|
6422
6770
|
// ----- privilege escalation (caution) -----
|
|
@@ -7178,6 +7526,10 @@ var execTool = {
|
|
|
7178
7526
|
category: "Shell",
|
|
7179
7527
|
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.",
|
|
7180
7528
|
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.",
|
|
7529
|
+
selection: {
|
|
7530
|
+
doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
|
|
7531
|
+
useInstead: ["bash"]
|
|
7532
|
+
},
|
|
7181
7533
|
permission: "confirm",
|
|
7182
7534
|
mutating: true,
|
|
7183
7535
|
riskTier: "standard",
|
|
@@ -7841,13 +8193,13 @@ var formatTool = {
|
|
|
7841
8193
|
}
|
|
7842
8194
|
};
|
|
7843
8195
|
async function detectFixer(cwd) {
|
|
7844
|
-
const { stat:
|
|
8196
|
+
const { stat: stat12 } = await import('node:fs/promises');
|
|
7845
8197
|
try {
|
|
7846
|
-
await
|
|
8198
|
+
await stat12(`${cwd}/biome.json`);
|
|
7847
8199
|
return "biome";
|
|
7848
8200
|
} catch {
|
|
7849
8201
|
try {
|
|
7850
|
-
await
|
|
8202
|
+
await stat12(`${cwd}/.prettierrc`);
|
|
7851
8203
|
return "prettier";
|
|
7852
8204
|
} catch {
|
|
7853
8205
|
return "biome";
|
|
@@ -8008,8 +8360,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
8008
8360
|
let dir = cwd;
|
|
8009
8361
|
for (let i = 0; i < 20; i++) {
|
|
8010
8362
|
try {
|
|
8011
|
-
const
|
|
8012
|
-
if (
|
|
8363
|
+
const stat12 = statSync(`${dir}/.git`);
|
|
8364
|
+
if (stat12.isDirectory() || stat12.isFile()) return dir;
|
|
8013
8365
|
} catch {
|
|
8014
8366
|
}
|
|
8015
8367
|
if (dir === root) break;
|
|
@@ -8156,6 +8508,10 @@ var globTool = {
|
|
|
8156
8508
|
category: "Filesystem",
|
|
8157
8509
|
description: "Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.",
|
|
8158
8510
|
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.",
|
|
8511
|
+
selection: {
|
|
8512
|
+
doNotUseWhen: "you need to search inside file contents.",
|
|
8513
|
+
useInstead: ["grep"]
|
|
8514
|
+
},
|
|
8159
8515
|
permission: "auto",
|
|
8160
8516
|
mutating: false,
|
|
8161
8517
|
capabilities: ["fs.read"],
|
|
@@ -8180,8 +8536,9 @@ var globTool = {
|
|
|
8180
8536
|
},
|
|
8181
8537
|
required: ["pattern"]
|
|
8182
8538
|
},
|
|
8183
|
-
async execute(input, ctx) {
|
|
8539
|
+
async execute(input, ctx, opts) {
|
|
8184
8540
|
if (!input?.pattern) throw new Error("glob: pattern is required");
|
|
8541
|
+
const signal = opts?.signal;
|
|
8185
8542
|
const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
|
|
8186
8543
|
const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
|
|
8187
8544
|
const ignored = await readGitignore(base);
|
|
@@ -8205,6 +8562,10 @@ var globTool = {
|
|
|
8205
8562
|
}
|
|
8206
8563
|
};
|
|
8207
8564
|
const walk = async (dir, relPrefix) => {
|
|
8565
|
+
if (signal?.aborted) {
|
|
8566
|
+
truncated = true;
|
|
8567
|
+
return;
|
|
8568
|
+
}
|
|
8208
8569
|
if (results.length >= limit) {
|
|
8209
8570
|
truncated = true;
|
|
8210
8571
|
return;
|
|
@@ -8222,7 +8583,7 @@ var globTool = {
|
|
|
8222
8583
|
if (DEFAULT_IGNORE2.includes(name)) continue;
|
|
8223
8584
|
if (ignored.includes(name)) continue;
|
|
8224
8585
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
8225
|
-
const full =
|
|
8586
|
+
const full = path12.join(dir, name);
|
|
8226
8587
|
if (e.isDirectory()) {
|
|
8227
8588
|
subdirs.push({ full, rel });
|
|
8228
8589
|
} else if (e.isFile()) {
|
|
@@ -8266,7 +8627,7 @@ var globTool = {
|
|
|
8266
8627
|
};
|
|
8267
8628
|
async function readGitignore(dir) {
|
|
8268
8629
|
try {
|
|
8269
|
-
const raw = await fs2.readFile(
|
|
8630
|
+
const raw = await fs2.readFile(path12.join(dir, ".gitignore"), "utf8");
|
|
8270
8631
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
8271
8632
|
} catch {
|
|
8272
8633
|
return [];
|
|
@@ -8328,6 +8689,10 @@ var grepTool = {
|
|
|
8328
8689
|
category: "Search",
|
|
8329
8690
|
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.",
|
|
8330
8691
|
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.',
|
|
8692
|
+
selection: {
|
|
8693
|
+
doNotUseWhen: "you only need to locate files by name or path pattern.",
|
|
8694
|
+
useInstead: ["glob"]
|
|
8695
|
+
},
|
|
8331
8696
|
permission: "auto",
|
|
8332
8697
|
mutating: false,
|
|
8333
8698
|
capabilities: ["fs.read"],
|
|
@@ -8567,8 +8932,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
8567
8932
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
8568
8933
|
if (globRe) globRe.lastIndex = 0;
|
|
8569
8934
|
try {
|
|
8570
|
-
const
|
|
8571
|
-
if (!
|
|
8935
|
+
const stat12 = await fs2.stat(full);
|
|
8936
|
+
if (!stat12.isFile() || stat12.size > maxBytes || stopped || signal.aborted) return;
|
|
8572
8937
|
const file = await fs2.open(full, "r");
|
|
8573
8938
|
try {
|
|
8574
8939
|
let bytesReadTotal = 0;
|
|
@@ -8658,7 +9023,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
8658
9023
|
if (stopped) return;
|
|
8659
9024
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
8660
9025
|
if (e.isSymbolicLink()) continue;
|
|
8661
|
-
const full =
|
|
9026
|
+
const full = path12.join(dir, e.name);
|
|
8662
9027
|
if (e.isDirectory()) {
|
|
8663
9028
|
subdirs.push(full);
|
|
8664
9029
|
} else if (e.isFile()) {
|
|
@@ -9205,56 +9570,56 @@ function jmespathSearch(data, query) {
|
|
|
9205
9570
|
}
|
|
9206
9571
|
function validateJsonSchema(data, schema) {
|
|
9207
9572
|
const errors = [];
|
|
9208
|
-
function check(value, s,
|
|
9573
|
+
function check(value, s, path23) {
|
|
9209
9574
|
if (s["type"]) {
|
|
9210
9575
|
const expectedType = s["type"];
|
|
9211
9576
|
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
9212
9577
|
if (expectedType === "integer") {
|
|
9213
|
-
if (!Number.isInteger(value)) errors.push(`${
|
|
9578
|
+
if (!Number.isInteger(value)) errors.push(`${path23}: expected integer, got ${actualType}`);
|
|
9214
9579
|
} else if (expectedType !== actualType) {
|
|
9215
|
-
errors.push(`${
|
|
9580
|
+
errors.push(`${path23}: expected ${expectedType}, got ${actualType}`);
|
|
9216
9581
|
}
|
|
9217
9582
|
}
|
|
9218
9583
|
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
9219
9584
|
try {
|
|
9220
9585
|
new URL(value);
|
|
9221
9586
|
} catch {
|
|
9222
|
-
errors.push(`${
|
|
9587
|
+
errors.push(`${path23}: not a valid URI`);
|
|
9223
9588
|
}
|
|
9224
9589
|
}
|
|
9225
9590
|
if (typeof value === "string" && s["pattern"]) {
|
|
9226
9591
|
const re = new RegExp(s["pattern"]);
|
|
9227
|
-
if (!re.test(value)) errors.push(`${
|
|
9592
|
+
if (!re.test(value)) errors.push(`${path23}: does not match pattern ${s["pattern"]}`);
|
|
9228
9593
|
}
|
|
9229
9594
|
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
9230
|
-
errors.push(`${
|
|
9595
|
+
errors.push(`${path23}: string too short (min ${s["minLength"]})`);
|
|
9231
9596
|
}
|
|
9232
9597
|
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
9233
|
-
errors.push(`${
|
|
9598
|
+
errors.push(`${path23}: string too long (max ${s["maxLength"]})`);
|
|
9234
9599
|
}
|
|
9235
9600
|
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
9236
|
-
errors.push(`${
|
|
9601
|
+
errors.push(`${path23}: below minimum ${s["minimum"]}`);
|
|
9237
9602
|
}
|
|
9238
9603
|
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
9239
|
-
errors.push(`${
|
|
9604
|
+
errors.push(`${path23}: above maximum ${s["maximum"]}`);
|
|
9240
9605
|
}
|
|
9241
9606
|
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
9242
9607
|
for (let i = 0; i < value.length; i++) {
|
|
9243
|
-
check(value[i], s["items"], `${
|
|
9608
|
+
check(value[i], s["items"], `${path23}[${i}]`);
|
|
9244
9609
|
}
|
|
9245
9610
|
}
|
|
9246
9611
|
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
9247
9612
|
const props = s["properties"];
|
|
9248
9613
|
for (const [k, propSchema] of Object.entries(props)) {
|
|
9249
|
-
check(value[k], propSchema, `${
|
|
9614
|
+
check(value[k], propSchema, `${path23}.${k}`);
|
|
9250
9615
|
}
|
|
9251
9616
|
}
|
|
9252
9617
|
}
|
|
9253
9618
|
check(data, schema, "$");
|
|
9254
9619
|
return { valid: errors.length === 0, errors };
|
|
9255
9620
|
}
|
|
9256
|
-
function simpleQuery(data,
|
|
9257
|
-
const parts =
|
|
9621
|
+
function simpleQuery(data, path23) {
|
|
9622
|
+
const parts = path23.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
9258
9623
|
let current = data;
|
|
9259
9624
|
for (const part of parts) {
|
|
9260
9625
|
if (current === null || current === void 0) return void 0;
|
|
@@ -10179,11 +10544,11 @@ var lintTool = {
|
|
|
10179
10544
|
}
|
|
10180
10545
|
};
|
|
10181
10546
|
async function detectLinter(cwd) {
|
|
10182
|
-
const { stat:
|
|
10547
|
+
const { stat: stat12 } = await import('node:fs/promises');
|
|
10183
10548
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
10184
10549
|
for (const f of checks) {
|
|
10185
10550
|
try {
|
|
10186
|
-
await
|
|
10551
|
+
await stat12(`${cwd}/${f}`);
|
|
10187
10552
|
if (f.includes("biome")) return "biome";
|
|
10188
10553
|
if (f.includes("eslint")) return "eslint";
|
|
10189
10554
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -10325,7 +10690,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
10325
10690
|
}
|
|
10326
10691
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
10327
10692
|
var MAX_TAIL_LINES = 1e5;
|
|
10328
|
-
async function fileLogs(
|
|
10693
|
+
async function fileLogs(path23, lines, filterRe, stream) {
|
|
10329
10694
|
const { createInterface } = await import('node:readline');
|
|
10330
10695
|
const { createReadStream } = await import('node:fs');
|
|
10331
10696
|
const entries = [];
|
|
@@ -10334,7 +10699,7 @@ async function fileLogs(path22, lines, filterRe, stream) {
|
|
|
10334
10699
|
let writeIdx = 0;
|
|
10335
10700
|
let totalLines = 0;
|
|
10336
10701
|
const rl = createInterface({
|
|
10337
|
-
input: createReadStream(
|
|
10702
|
+
input: createReadStream(path23),
|
|
10338
10703
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
10339
10704
|
});
|
|
10340
10705
|
for await (const line of rl) {
|
|
@@ -10355,7 +10720,7 @@ async function fileLogs(path22, lines, filterRe, stream) {
|
|
|
10355
10720
|
if (parsed) entries.push(parsed);
|
|
10356
10721
|
}
|
|
10357
10722
|
return {
|
|
10358
|
-
source:
|
|
10723
|
+
source: path23,
|
|
10359
10724
|
entries,
|
|
10360
10725
|
total: entries.length,
|
|
10361
10726
|
truncated: totalLines > effLines,
|
|
@@ -10522,6 +10887,10 @@ var patchTool = {
|
|
|
10522
10887
|
category: "Filesystem",
|
|
10523
10888
|
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.",
|
|
10524
10889
|
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.",
|
|
10890
|
+
selection: {
|
|
10891
|
+
doNotUseWhen: "you do not already have a unified diff or only need one precise replacement.",
|
|
10892
|
+
useInstead: ["edit"]
|
|
10893
|
+
},
|
|
10525
10894
|
permission: "confirm",
|
|
10526
10895
|
mutating: true,
|
|
10527
10896
|
capabilities: ["fs.write"],
|
|
@@ -10543,12 +10912,13 @@ var patchTool = {
|
|
|
10543
10912
|
const strip = Math.max(1, input.strip ?? 1);
|
|
10544
10913
|
const dryRun = input.dry_run ?? false;
|
|
10545
10914
|
const targets = extractDiffTargets(input.patch);
|
|
10915
|
+
const resolvedTargets = [];
|
|
10546
10916
|
for (const t of targets) {
|
|
10547
10917
|
const stripped = stripPathComponents(t, strip);
|
|
10548
10918
|
if (!stripped) continue;
|
|
10549
|
-
const candidate =
|
|
10550
|
-
const rel =
|
|
10551
|
-
if (rel.startsWith("..") ||
|
|
10919
|
+
const candidate = path12.resolve(dir, stripped);
|
|
10920
|
+
const rel = path12.relative(ctx.projectRoot, candidate);
|
|
10921
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) {
|
|
10552
10922
|
return {
|
|
10553
10923
|
applied: 0,
|
|
10554
10924
|
rejected: 1,
|
|
@@ -10557,12 +10927,19 @@ var patchTool = {
|
|
|
10557
10927
|
message: `patch refused: target "${t}" resolves outside project root`
|
|
10558
10928
|
};
|
|
10559
10929
|
}
|
|
10930
|
+
resolvedTargets.push(candidate);
|
|
10560
10931
|
}
|
|
10561
|
-
const
|
|
10932
|
+
const beforeContents = /* @__PURE__ */ new Map();
|
|
10933
|
+
if (!dryRun) {
|
|
10934
|
+
for (const target of resolvedTargets) {
|
|
10935
|
+
beforeContents.set(target, await readTextForTracking(target));
|
|
10936
|
+
}
|
|
10937
|
+
}
|
|
10938
|
+
const tmpDir = await fs2.mkdtemp(path12.join(os2.tmpdir(), ".wstack_patch_"));
|
|
10562
10939
|
try {
|
|
10563
10940
|
await fs2.chmod(tmpDir, 448).catch(() => {
|
|
10564
10941
|
});
|
|
10565
|
-
const patchFile =
|
|
10942
|
+
const patchFile = path12.join(tmpDir, "in.diff");
|
|
10566
10943
|
await fs2.writeFile(patchFile, input.patch, { mode: 384 });
|
|
10567
10944
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
10568
10945
|
const result = await runPatch(args, dir, opts.signal);
|
|
@@ -10576,6 +10953,21 @@ var patchTool = {
|
|
|
10576
10953
|
};
|
|
10577
10954
|
}
|
|
10578
10955
|
const patched = extractPatchedFiles(result.stdout);
|
|
10956
|
+
if (!dryRun) {
|
|
10957
|
+
for (const target of resolvedTargets) {
|
|
10958
|
+
const before = beforeContents.get(target) ?? null;
|
|
10959
|
+
const after = await readTextForTracking(target);
|
|
10960
|
+
if (after === null || after === before) continue;
|
|
10961
|
+
const stat12 = await fs2.stat(target).catch(() => null);
|
|
10962
|
+
if (stat12) ctx.recordRead?.(target, stat12.mtimeMs, "write", sha256hex(after));
|
|
10963
|
+
ctx.session?.recordFileChange?.({
|
|
10964
|
+
path: target,
|
|
10965
|
+
action: before === null ? "created" : "modified",
|
|
10966
|
+
before,
|
|
10967
|
+
after
|
|
10968
|
+
});
|
|
10969
|
+
}
|
|
10970
|
+
}
|
|
10579
10971
|
return {
|
|
10580
10972
|
applied: patched.length,
|
|
10581
10973
|
rejected: 0,
|
|
@@ -10589,6 +10981,18 @@ var patchTool = {
|
|
|
10589
10981
|
}
|
|
10590
10982
|
}
|
|
10591
10983
|
};
|
|
10984
|
+
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
10985
|
+
async function readTextForTracking(absPath) {
|
|
10986
|
+
try {
|
|
10987
|
+
const stat12 = await fs2.stat(absPath);
|
|
10988
|
+
if (!stat12.isFile() || stat12.size > MAX_TRACKING_BYTES) return null;
|
|
10989
|
+
const buf = await fs2.readFile(absPath);
|
|
10990
|
+
if (buf.includes(0)) return null;
|
|
10991
|
+
return buf.toString("utf8");
|
|
10992
|
+
} catch {
|
|
10993
|
+
return null;
|
|
10994
|
+
}
|
|
10995
|
+
}
|
|
10592
10996
|
function extractDiffTargets(patch) {
|
|
10593
10997
|
const out = [];
|
|
10594
10998
|
const re = /^\+\+\+\s+([^\t\r\n]+)/gm;
|
|
@@ -10896,6 +11300,10 @@ var readTool = {
|
|
|
10896
11300
|
category: "Filesystem",
|
|
10897
11301
|
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.",
|
|
10898
11302
|
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.",
|
|
11303
|
+
selection: {
|
|
11304
|
+
doNotUseWhen: "you need to search many files for matching content.",
|
|
11305
|
+
useInstead: ["grep"]
|
|
11306
|
+
},
|
|
10899
11307
|
permission: "auto",
|
|
10900
11308
|
mutating: false,
|
|
10901
11309
|
capabilities: ["fs.read"],
|
|
@@ -10933,9 +11341,9 @@ var readTool = {
|
|
|
10933
11341
|
});
|
|
10934
11342
|
}
|
|
10935
11343
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
10936
|
-
let
|
|
11344
|
+
let stat12;
|
|
10937
11345
|
try {
|
|
10938
|
-
|
|
11346
|
+
stat12 = await fs2.stat(absPath);
|
|
10939
11347
|
} catch (err) {
|
|
10940
11348
|
const code = err.code;
|
|
10941
11349
|
if (code === "ENOENT") {
|
|
@@ -10954,7 +11362,7 @@ var readTool = {
|
|
|
10954
11362
|
cause: err
|
|
10955
11363
|
});
|
|
10956
11364
|
}
|
|
10957
|
-
if (!
|
|
11365
|
+
if (!stat12.isFile()) {
|
|
10958
11366
|
throw new FsError({
|
|
10959
11367
|
message: `read: "${input.path}" is not a regular file`,
|
|
10960
11368
|
code: "FS_READ_FAILED",
|
|
@@ -10962,22 +11370,22 @@ var readTool = {
|
|
|
10962
11370
|
context: { reason: "not-a-regular-file" }
|
|
10963
11371
|
});
|
|
10964
11372
|
}
|
|
10965
|
-
if (
|
|
11373
|
+
if (stat12.size > MAX_BYTES2) {
|
|
10966
11374
|
throw new FsError({
|
|
10967
|
-
message: `read: file too large (${
|
|
11375
|
+
message: `read: file too large (${stat12.size} bytes, limit ${MAX_BYTES2})`,
|
|
10968
11376
|
code: "FS_READ_FAILED",
|
|
10969
11377
|
path: absPath,
|
|
10970
|
-
context: { size:
|
|
11378
|
+
context: { size: stat12.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
10971
11379
|
});
|
|
10972
11380
|
}
|
|
10973
11381
|
const offset = Math.max(1, input.offset ?? 1);
|
|
10974
11382
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
10975
11383
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
10976
11384
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
10977
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
10978
|
-
ctx.recordRead(absPath,
|
|
11385
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat12.mtimeMs, offset, requestedEnd)) {
|
|
11386
|
+
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
10979
11387
|
return {
|
|
10980
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
11388
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat12.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
10981
11389
|
total_lines: prior.totalLines,
|
|
10982
11390
|
encoding: "utf8",
|
|
10983
11391
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -10990,13 +11398,14 @@ var readTool = {
|
|
|
10990
11398
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
10991
11399
|
}
|
|
10992
11400
|
const text = buf.toString("utf8");
|
|
11401
|
+
const contentHash = sha256hex(text);
|
|
10993
11402
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
10994
11403
|
const total = allLines.length;
|
|
10995
11404
|
if (input.mode === "summary") {
|
|
10996
|
-
ctx.recordRead(absPath,
|
|
10997
|
-
rememberReadRange(ctx, absPath,
|
|
11405
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "user", contentHash);
|
|
11406
|
+
rememberReadRange(ctx, absPath, stat12.mtimeMs, total, 1, Math.min(total, 200));
|
|
10998
11407
|
return {
|
|
10999
|
-
text: summarizeFile(input.path,
|
|
11408
|
+
text: summarizeFile(input.path, stat12.size, allLines),
|
|
11000
11409
|
total_lines: total,
|
|
11001
11410
|
encoding: "utf8",
|
|
11002
11411
|
truncated: total > 200,
|
|
@@ -11004,13 +11413,13 @@ var readTool = {
|
|
|
11004
11413
|
};
|
|
11005
11414
|
}
|
|
11006
11415
|
if (limit === 0) {
|
|
11007
|
-
ctx.recordRead(absPath,
|
|
11008
|
-
rememberReadRange(ctx, absPath,
|
|
11416
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "user", contentHash);
|
|
11417
|
+
rememberReadRange(ctx, absPath, stat12.mtimeMs, total, 1, 0);
|
|
11009
11418
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
11010
11419
|
}
|
|
11011
11420
|
if (offset > total) {
|
|
11012
|
-
ctx.recordRead(absPath,
|
|
11013
|
-
rememberReadRange(ctx, absPath,
|
|
11421
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "user", contentHash);
|
|
11422
|
+
rememberReadRange(ctx, absPath, stat12.mtimeMs, total, total + 1, total + 1);
|
|
11014
11423
|
return {
|
|
11015
11424
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
11016
11425
|
total_lines: total,
|
|
@@ -11022,8 +11431,8 @@ var readTool = {
|
|
|
11022
11431
|
const truncated = offset - 1 + slice.length < total;
|
|
11023
11432
|
const width = String(offset + slice.length - 1).length;
|
|
11024
11433
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
11025
|
-
ctx.recordRead(absPath,
|
|
11026
|
-
rememberReadRange(ctx, absPath,
|
|
11434
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "user", contentHash);
|
|
11435
|
+
rememberReadRange(ctx, absPath, stat12.mtimeMs, total, offset, offset + slice.length - 1);
|
|
11027
11436
|
return {
|
|
11028
11437
|
text: numbered,
|
|
11029
11438
|
total_lines: total,
|
|
@@ -11165,10 +11574,10 @@ var replaceTool = {
|
|
|
11165
11574
|
} catch {
|
|
11166
11575
|
continue;
|
|
11167
11576
|
}
|
|
11168
|
-
const rel =
|
|
11169
|
-
if (rel.startsWith("..") ||
|
|
11170
|
-
const
|
|
11171
|
-
if (!
|
|
11577
|
+
const rel = path12.relative(realRoot, realPath);
|
|
11578
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) continue;
|
|
11579
|
+
const stat12 = await fs2.stat(realPath).catch(() => null);
|
|
11580
|
+
if (!stat12?.isFile()) continue;
|
|
11172
11581
|
let content;
|
|
11173
11582
|
try {
|
|
11174
11583
|
const buf = await fs2.readFile(realPath);
|
|
@@ -11193,7 +11602,17 @@ var replaceTool = {
|
|
|
11193
11602
|
totalReplacements += count;
|
|
11194
11603
|
if (!dryRun) {
|
|
11195
11604
|
const newContent = toStyle(newContentLf, style);
|
|
11196
|
-
await atomicWrite(realPath, newContent, { mode:
|
|
11605
|
+
await atomicWrite(realPath, newContent, { mode: stat12.mode & 511 });
|
|
11606
|
+
const written = await fs2.stat(realPath).catch(() => null);
|
|
11607
|
+
if (written) {
|
|
11608
|
+
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
11609
|
+
}
|
|
11610
|
+
ctx.session?.recordFileChange?.({
|
|
11611
|
+
path: realPath,
|
|
11612
|
+
action: "modified",
|
|
11613
|
+
before: content,
|
|
11614
|
+
after: newContent
|
|
11615
|
+
});
|
|
11197
11616
|
}
|
|
11198
11617
|
const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
|
|
11199
11618
|
fromFile: absPath,
|
|
@@ -11223,8 +11642,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
11223
11642
|
const resolved = [];
|
|
11224
11643
|
for (const p of parts) {
|
|
11225
11644
|
const absPath = safeResolve(p, ctx);
|
|
11226
|
-
const
|
|
11227
|
-
if (
|
|
11645
|
+
const stat12 = await fs2.stat(absPath).catch(() => null);
|
|
11646
|
+
if (stat12?.isFile()) {
|
|
11228
11647
|
resolved.push(absPath);
|
|
11229
11648
|
}
|
|
11230
11649
|
}
|
|
@@ -11285,10 +11704,10 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
11285
11704
|
}
|
|
11286
11705
|
for (const e of entries) {
|
|
11287
11706
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
11288
|
-
const full =
|
|
11707
|
+
const full = path12.join(dir, e.name);
|
|
11289
11708
|
try {
|
|
11290
|
-
const
|
|
11291
|
-
if (
|
|
11709
|
+
const stat12 = await fs2.lstat(full);
|
|
11710
|
+
if (stat12.isSymbolicLink()) continue;
|
|
11292
11711
|
} catch {
|
|
11293
11712
|
continue;
|
|
11294
11713
|
}
|
|
@@ -11456,16 +11875,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
11456
11875
|
let filesCreated = 0;
|
|
11457
11876
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
11458
11877
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
11459
|
-
const joinedPath =
|
|
11460
|
-
const root =
|
|
11461
|
-
const target =
|
|
11462
|
-
const rel =
|
|
11463
|
-
if (rel.startsWith("..") ||
|
|
11878
|
+
const joinedPath = path12.join(cwd, resolvedPath);
|
|
11879
|
+
const root = path12.resolve(ctx.projectRoot);
|
|
11880
|
+
const target = path12.resolve(joinedPath);
|
|
11881
|
+
const rel = path12.relative(root, target);
|
|
11882
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) {
|
|
11464
11883
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
11465
11884
|
}
|
|
11466
11885
|
const fullPath = target;
|
|
11467
11886
|
if (!dryRun) {
|
|
11468
|
-
await fs2.mkdir(
|
|
11887
|
+
await fs2.mkdir(path12.dirname(fullPath), { recursive: true });
|
|
11469
11888
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
11470
11889
|
}
|
|
11471
11890
|
files.push(resolvedPath);
|
|
@@ -12365,11 +12784,11 @@ var testTool = {
|
|
|
12365
12784
|
}
|
|
12366
12785
|
};
|
|
12367
12786
|
async function detectRunner(cwd) {
|
|
12368
|
-
const { stat:
|
|
12787
|
+
const { stat: stat12 } = await import('node:fs/promises');
|
|
12369
12788
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
12370
12789
|
for (const f of candidates) {
|
|
12371
12790
|
try {
|
|
12372
|
-
await
|
|
12791
|
+
await stat12(path12.join(cwd, f));
|
|
12373
12792
|
if (f.includes("vitest")) return "vitest";
|
|
12374
12793
|
if (f.includes("jest")) return "jest";
|
|
12375
12794
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -12997,7 +13416,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
12997
13416
|
opts.lines.push(opts.prefix + branch + displayName);
|
|
12998
13417
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
12999
13418
|
const childPrefix = opts.prefix + connector;
|
|
13000
|
-
await walkDir(
|
|
13419
|
+
await walkDir(path12.join(dir, entry.name), depth + 1, {
|
|
13001
13420
|
...opts,
|
|
13002
13421
|
prefix: childPrefix,
|
|
13003
13422
|
isLast
|
|
@@ -13083,12 +13502,12 @@ var typecheckTool = {
|
|
|
13083
13502
|
}
|
|
13084
13503
|
};
|
|
13085
13504
|
async function findTsConfig(cwd) {
|
|
13086
|
-
const { stat:
|
|
13505
|
+
const { stat: stat12 } = await import('node:fs/promises');
|
|
13087
13506
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
13088
13507
|
for (const f of candidates) {
|
|
13089
13508
|
try {
|
|
13090
|
-
const s = await
|
|
13091
|
-
if (s.isFile()) return
|
|
13509
|
+
const s = await stat12(path12.join(cwd, f));
|
|
13510
|
+
if (s.isFile()) return path12.join(cwd, f);
|
|
13092
13511
|
} catch {
|
|
13093
13512
|
}
|
|
13094
13513
|
}
|
|
@@ -13099,6 +13518,10 @@ var writeTool = {
|
|
|
13099
13518
|
category: "Filesystem",
|
|
13100
13519
|
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.",
|
|
13101
13520
|
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.",
|
|
13521
|
+
selection: {
|
|
13522
|
+
doNotUseWhen: "making a precise change to part of an existing file.",
|
|
13523
|
+
useInstead: ["edit"]
|
|
13524
|
+
},
|
|
13102
13525
|
permission: "confirm",
|
|
13103
13526
|
mutating: true,
|
|
13104
13527
|
timeoutMs: 5e3,
|
|
@@ -13118,10 +13541,10 @@ var writeTool = {
|
|
|
13118
13541
|
},
|
|
13119
13542
|
required: ["path", "content"]
|
|
13120
13543
|
},
|
|
13121
|
-
async execute(input, ctx) {
|
|
13122
|
-
return writeFile7(input, ctx);
|
|
13544
|
+
async execute(input, ctx, opts) {
|
|
13545
|
+
return writeFile7(input, ctx, opts?.signal);
|
|
13123
13546
|
},
|
|
13124
|
-
async *executeStream(input, ctx) {
|
|
13547
|
+
async *executeStream(input, ctx, opts) {
|
|
13125
13548
|
const prepared = await prepareWrite(input, ctx);
|
|
13126
13549
|
if (!prepared.existed) {
|
|
13127
13550
|
for (const line of input.content.split("\n")) {
|
|
@@ -13129,11 +13552,11 @@ var writeTool = {
|
|
|
13129
13552
|
`, data: { livePreview: true } };
|
|
13130
13553
|
}
|
|
13131
13554
|
}
|
|
13132
|
-
yield { type: "final", output: await finishWrite(input, ctx, prepared) };
|
|
13555
|
+
yield { type: "final", output: await finishWrite(input, ctx, prepared, opts?.signal) };
|
|
13133
13556
|
}
|
|
13134
13557
|
};
|
|
13135
|
-
async function writeFile7(input, ctx) {
|
|
13136
|
-
return finishWrite(input, ctx, await prepareWrite(input, ctx));
|
|
13558
|
+
async function writeFile7(input, ctx, signal) {
|
|
13559
|
+
return finishWrite(input, ctx, await prepareWrite(input, ctx), signal);
|
|
13137
13560
|
}
|
|
13138
13561
|
async function prepareWrite(input, ctx) {
|
|
13139
13562
|
if (!input?.path) {
|
|
@@ -13152,12 +13575,12 @@ async function prepareWrite(input, ctx) {
|
|
|
13152
13575
|
let existed = false;
|
|
13153
13576
|
let prev = "";
|
|
13154
13577
|
try {
|
|
13155
|
-
const
|
|
13156
|
-
existed =
|
|
13578
|
+
const stat12 = await fs2.stat(absPath);
|
|
13579
|
+
existed = stat12.isFile();
|
|
13157
13580
|
if (existed) {
|
|
13158
13581
|
if (!ctx.hasRead(absPath)) {
|
|
13159
13582
|
prev = await fs2.readFile(absPath, "utf8");
|
|
13160
|
-
ctx.recordRead(absPath,
|
|
13583
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "write", sha256hex(prev));
|
|
13161
13584
|
} else {
|
|
13162
13585
|
prev = await fs2.readFile(absPath, "utf8");
|
|
13163
13586
|
}
|
|
@@ -13169,23 +13592,32 @@ async function prepareWrite(input, ctx) {
|
|
|
13169
13592
|
}
|
|
13170
13593
|
return { absPath, existed, prev };
|
|
13171
13594
|
}
|
|
13172
|
-
async function finishWrite(input, ctx, prepared) {
|
|
13595
|
+
async function finishWrite(input, ctx, prepared, signal) {
|
|
13596
|
+
signal?.throwIfAborted();
|
|
13173
13597
|
await atomicWrite(prepared.absPath, input.content);
|
|
13174
13598
|
const diff = prepared.existed ? unifiedDiff(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
13175
13599
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
13176
|
-
const
|
|
13177
|
-
ctx.recordRead(prepared.absPath,
|
|
13600
|
+
const stat12 = await fs2.stat(prepared.absPath);
|
|
13601
|
+
ctx.recordRead(prepared.absPath, stat12.mtimeMs, "write", sha256hex(input.content));
|
|
13178
13602
|
ctx.session.recordFileChange({
|
|
13179
13603
|
path: prepared.absPath,
|
|
13180
13604
|
action: prepared.existed ? "modified" : "created",
|
|
13181
13605
|
before: prepared.existed ? prepared.prev : null,
|
|
13182
13606
|
after: input.content
|
|
13183
13607
|
});
|
|
13608
|
+
const syntax = await checkSyntax(
|
|
13609
|
+
prepared.absPath,
|
|
13610
|
+
input.content,
|
|
13611
|
+
prepared.existed ? prepared.prev : void 0
|
|
13612
|
+
).catch(() => void 0);
|
|
13613
|
+
const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
|
|
13184
13614
|
return {
|
|
13185
13615
|
path: prepared.absPath,
|
|
13186
13616
|
bytes_written: Buffer.byteLength(input.content, "utf8"),
|
|
13187
13617
|
created: !prepared.existed,
|
|
13188
|
-
diff
|
|
13618
|
+
diff,
|
|
13619
|
+
syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
|
|
13620
|
+
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
|
|
13189
13621
|
};
|
|
13190
13622
|
}
|
|
13191
13623
|
|