@wrongstack/tools 0.283.0 → 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 +710 -259
- package/dist/builtin.js.map +1 -1
- package/dist/codebase-index/index.d.ts +11 -0
- package/dist/codebase-index/index.js +27 -8
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +25 -6
- package/dist/codebase-index/worker.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 +755 -274
- 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 +710 -259
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +43 -0
- package/dist/patch.js.map +1 -1
- package/dist/read.js +13 -4
- package/dist/read.js.map +1 -1
- package/dist/replace.js +14 -0
- package/dist/replace.js.map +1 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/test.js.map +1 -1
- package/dist/tool-diff.d.ts +88 -0
- package/dist/tool-diff.js +229 -0
- package/dist/tool-diff.js.map +1 -0
- package/dist/tool-summary.d.ts +24 -0
- package/dist/tool-summary.js +208 -0
- package/dist/tool-summary.js.map +1 -0
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js.map +1 -1
- package/dist/write.d.ts +6 -0
- package/dist/write.js +118 -19
- package/dist/write.js.map +1 -1
- package/package.json +15 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import { statSync, mkdirSync, createWriteStream } from 'node:fs';
|
|
3
|
-
import * as
|
|
3
|
+
import * as path12 from 'node:path';
|
|
4
4
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
5
5
|
import { spawn, execFileSync } from 'node:child_process';
|
|
6
6
|
import * as Core from '@wrongstack/core';
|
|
7
|
-
import { SKILL_LIMITS, buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, ToolValidationError, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, ToolError, FetchError, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, deepMerge,
|
|
7
|
+
import { SKILL_LIMITS, buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, ToolValidationError, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, ToolError, FetchError, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, deepMerge, deserializeTaskGraph, serializeTaskGraph, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, FsError, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, stripFrontmatter, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
8
8
|
import * as fs2 from 'node:fs/promises';
|
|
9
9
|
import * as os2 from 'node:os';
|
|
10
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
10
11
|
import { toErrorMessage as toErrorMessage$3 } from '@wrongstack/core/utils';
|
|
11
12
|
import { createRequire } from 'node:module';
|
|
12
13
|
import { fileURLToPath } from 'node:url';
|
|
@@ -17,7 +18,7 @@ import * as dns from 'node:dns/promises';
|
|
|
17
18
|
import * as net from 'node:net';
|
|
18
19
|
import { Agent, fetch } from 'undici';
|
|
19
20
|
import TurndownService from 'turndown';
|
|
20
|
-
import {
|
|
21
|
+
import { addLinkToTask, addNoteToTask, updateCheckOnTask, addCheckToTask, updateGoalMetricOnTask, addGoalMetricToTask, addDependency, getKanbanQueueHealth, listKanbanEvents, recoverStaleTaskAssignments, heartbeatTaskAssignment, updateTaskAssignment, assignTask, releaseTaskClaim, claimReadyTask, getTaskChain, setTaskChain, removeTask, moveTask, updateTask, getTask, transferTaskToBoard, copyTaskToBoard, mergeTasks, splitTask, addTask, removeColumn, updateColumn, addColumn, getKanbanOrchestrationSnapshot, listReadyTasks, searchKanban, syncBoardFromTaskGraph, exportBoardToTaskGraph, exportBoardAsMarkdown, generateBoardFromDescription, createBoard, parseLinesIntoTasks, getBoard, removeBoard, duplicateBoard, updateBoard, listBoards } from '@wrongstack/kanban';
|
|
21
22
|
|
|
22
23
|
// src/_danger-detect.ts
|
|
23
24
|
var argHas = (args, value) => args.includes(value);
|
|
@@ -230,23 +231,18 @@ var RULES = [
|
|
|
230
231
|
},
|
|
231
232
|
reason: "inline script evaluation (-c / -e / --eval)"
|
|
232
233
|
},
|
|
233
|
-
// ----- pipe-to-shell (
|
|
234
|
+
// ----- pipe-to-shell (destructive — download-and-run pattern) -----
|
|
234
235
|
// The classic `curl https://... | sh` download-and-run vector. Detected by
|
|
235
|
-
// looking for a known fetcher
|
|
236
|
-
//
|
|
237
|
-
//
|
|
236
|
+
// looking for a known fetcher piped into a shell or expression evaluator.
|
|
237
|
+
// A shell command passed through `bash -c` / `pwsh -Command` arrives as one
|
|
238
|
+
// argv string, so scan the reconstructed argv text rather than individual
|
|
239
|
+
// tokens only.
|
|
238
240
|
{
|
|
239
241
|
id: "pipe-to-shell",
|
|
240
|
-
level: "
|
|
241
|
-
test: (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
);
|
|
245
|
-
const hasShellSink = args.some(
|
|
246
|
-
(a) => a === "sh" || a === "bash" || a === "zsh" || a === "fish" || a === "pwsh" || a === "powershell" || a.endsWith("/sh") || a.endsWith("/bash") || a.endsWith("/zsh") || a.endsWith("/pwsh")
|
|
247
|
-
);
|
|
248
|
-
return hasFetcher && hasShellSink;
|
|
249
|
-
},
|
|
242
|
+
level: "destructive",
|
|
243
|
+
test: (cmd, args) => /\b(?:curl|wget|fetch|httpie|http|irm|iwr|Invoke-WebRequest|Invoke-RestMethod)\b[\s\S]{0,300}\|\s*(?:sudo\s+)?(?:sh|bash|zsh|fish|pwsh|powershell|iex|Invoke-Expression)\b/i.test(
|
|
244
|
+
[cmd, ...args].join(" ")
|
|
245
|
+
),
|
|
250
246
|
reason: "network fetch piped to a shell (download-and-run pattern)"
|
|
251
247
|
},
|
|
252
248
|
// ----- privilege escalation (caution) -----
|
|
@@ -306,13 +302,13 @@ function levelRank(level) {
|
|
|
306
302
|
}
|
|
307
303
|
function resolveWin32Command(cmd) {
|
|
308
304
|
if (process.platform !== "win32") return cmd;
|
|
309
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
305
|
+
if (cmd.includes("/") || cmd.includes("\\") || path12.extname(cmd.replace(/\//g, "\\"))) {
|
|
310
306
|
return cmd;
|
|
311
307
|
}
|
|
312
308
|
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
313
|
-
const pathDirs = (process.env["PATH"] ?? "").split(
|
|
309
|
+
const pathDirs = (process.env["PATH"] ?? "").split(path12.delimiter);
|
|
314
310
|
for (const dir of pathDirs) {
|
|
315
|
-
const base =
|
|
311
|
+
const base = path12.join(dir, cmd);
|
|
316
312
|
for (const ext of pathext) {
|
|
317
313
|
const full = `${base}${ext}`;
|
|
318
314
|
try {
|
|
@@ -393,7 +389,7 @@ var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
|
393
389
|
var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
|
|
394
390
|
var sweepStarted = false;
|
|
395
391
|
function toolOutputDir() {
|
|
396
|
-
return
|
|
392
|
+
return path12.join(wstackGlobalRoot(), "tool-output");
|
|
397
393
|
}
|
|
398
394
|
function sweepOldSpoolFiles(dir) {
|
|
399
395
|
if (sweepStarted) return;
|
|
@@ -403,7 +399,7 @@ function sweepOldSpoolFiles(dir) {
|
|
|
403
399
|
const now2 = Date.now();
|
|
404
400
|
for (const name of await fs2.readdir(dir)) {
|
|
405
401
|
if (!name.endsWith(".log")) continue;
|
|
406
|
-
const p =
|
|
402
|
+
const p = path12.join(dir, name);
|
|
407
403
|
try {
|
|
408
404
|
const st = await fs2.stat(p);
|
|
409
405
|
if (now2 - st.mtimeMs > SPOOL_RETENTION_MS) await fs2.unlink(p);
|
|
@@ -438,7 +434,7 @@ function createOutputSpool(opts) {
|
|
|
438
434
|
sweepOldSpoolFiles(dir);
|
|
439
435
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
440
436
|
const rand = Math.random().toString(36).slice(2, 6);
|
|
441
|
-
filePath =
|
|
437
|
+
filePath = path12.join(dir, `${stamp}-${safeTool}-${rand}.log`);
|
|
442
438
|
stream = createWriteStream(filePath, { flags: "w", encoding: "utf8" });
|
|
443
439
|
stream.on("error", () => {
|
|
444
440
|
failed = true;
|
|
@@ -1237,37 +1233,40 @@ async function* spawnStream(opts) {
|
|
|
1237
1233
|
}
|
|
1238
1234
|
}
|
|
1239
1235
|
}
|
|
1236
|
+
function sha256hex(content) {
|
|
1237
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1238
|
+
}
|
|
1240
1239
|
async function detectPackageManager(cwd) {
|
|
1241
|
-
const { stat:
|
|
1240
|
+
const { stat: stat13 } = await import('node:fs/promises');
|
|
1242
1241
|
try {
|
|
1243
|
-
await
|
|
1242
|
+
await stat13(`${cwd}/pnpm-lock.yaml`);
|
|
1244
1243
|
return "pnpm";
|
|
1245
1244
|
} catch {
|
|
1246
1245
|
}
|
|
1247
1246
|
try {
|
|
1248
|
-
await
|
|
1247
|
+
await stat13(`${cwd}/yarn.lock`);
|
|
1249
1248
|
return "yarn";
|
|
1250
1249
|
} catch {
|
|
1251
1250
|
}
|
|
1252
1251
|
return "npm";
|
|
1253
1252
|
}
|
|
1254
1253
|
function resolvePath(input, ctx) {
|
|
1255
|
-
return
|
|
1254
|
+
return path12.isAbsolute(input) ? path12.normalize(input) : path12.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
1256
1255
|
}
|
|
1257
1256
|
function allowedRoots(ctx) {
|
|
1258
|
-
return [
|
|
1257
|
+
return [path12.resolve(ctx.projectRoot), path12.resolve(Core.wstackGlobalRoot())];
|
|
1259
1258
|
}
|
|
1260
1259
|
function isInsideAny(target, roots) {
|
|
1261
1260
|
return roots.some((root) => {
|
|
1262
|
-
const rel =
|
|
1263
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
1261
|
+
const rel = path12.relative(root, target);
|
|
1262
|
+
return rel === "" || !rel.startsWith("..") && !path12.isAbsolute(rel);
|
|
1264
1263
|
});
|
|
1265
1264
|
}
|
|
1266
1265
|
function ensureInsideRoot(absPath, ctx) {
|
|
1267
|
-
const target =
|
|
1266
|
+
const target = path12.resolve(absPath);
|
|
1268
1267
|
if (ctx.allowOutsideProjectRoot) return target;
|
|
1269
1268
|
if (isInsideAny(target, allowedRoots(ctx))) return target;
|
|
1270
|
-
throw new Error(`Path "${absPath}" is outside project root "${
|
|
1269
|
+
throw new Error(`Path "${absPath}" is outside project root "${path12.resolve(ctx.projectRoot)}"`);
|
|
1271
1270
|
}
|
|
1272
1271
|
function safeResolve(input, ctx) {
|
|
1273
1272
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
@@ -1275,7 +1274,7 @@ function safeResolve(input, ctx) {
|
|
|
1275
1274
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
1276
1275
|
if (ctx.allowOutsideProjectRoot) return;
|
|
1277
1276
|
const realRoots = await Promise.all(
|
|
1278
|
-
allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() =>
|
|
1277
|
+
allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() => path12.resolve(r)))
|
|
1279
1278
|
);
|
|
1280
1279
|
let probe = absPath;
|
|
1281
1280
|
for (; ; ) {
|
|
@@ -1284,7 +1283,7 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
1284
1283
|
real = await fs2.realpath(probe);
|
|
1285
1284
|
} catch (err) {
|
|
1286
1285
|
if (err.code === "ENOENT") {
|
|
1287
|
-
const parent =
|
|
1286
|
+
const parent = path12.dirname(probe);
|
|
1288
1287
|
if (parent === probe) return;
|
|
1289
1288
|
probe = parent;
|
|
1290
1289
|
continue;
|
|
@@ -1597,15 +1596,15 @@ var PersistentProcessRegistry = class {
|
|
|
1597
1596
|
constructor(baseRegistry) {
|
|
1598
1597
|
this.instanceId = generateInstanceId();
|
|
1599
1598
|
const homeDir = os2.homedir();
|
|
1600
|
-
this.registryPath =
|
|
1601
|
-
this.lockPath =
|
|
1599
|
+
this.registryPath = path12.join(homeDir, REGISTRY_FILE);
|
|
1600
|
+
this.lockPath = path12.join(homeDir, LOCKFILE);
|
|
1602
1601
|
this.baseRegistry = baseRegistry ?? getProcessRegistry();
|
|
1603
1602
|
this.ensureDirectory().catch((err) => {
|
|
1604
1603
|
emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
|
|
1605
1604
|
});
|
|
1606
1605
|
}
|
|
1607
1606
|
async ensureDirectory() {
|
|
1608
|
-
const dir =
|
|
1607
|
+
const dir = path12.dirname(this.registryPath);
|
|
1609
1608
|
try {
|
|
1610
1609
|
await fs2.mkdir(dir, { recursive: true });
|
|
1611
1610
|
} catch (err) {
|
|
@@ -2191,6 +2190,10 @@ var bashTool = {
|
|
|
2191
2190
|
category: "Shell",
|
|
2192
2191
|
description: "Execute an arbitrary command in the user's default shell (bash/zsh/pwsh/cmd). stdout and stderr are merged into one stream. This is the most powerful and dangerous tool \u2014 it gives the model full access to the developer's machine. Prefer specialized tools whenever possible.",
|
|
2193
2192
|
usageHint: "SECURITY WARNING: This tool runs with the full privileges of the current user.\n\nBest practices for the model:\n- Strongly prefer `exec` for known safe commands (node, npm, pnpm, tsc, git, etc.).\n- Use bash only when you genuinely need shell features (pipes, redirection, complex one-liners).\n- Prefer single focused commands over huge `&&` chains.\n- Use `background: true` only for long-running processes (dev servers, watchers).\n- The working directory is the project root.\n- Output may be truncated in the middle for very large results.",
|
|
2193
|
+
selection: {
|
|
2194
|
+
doNotUseWhen: "the command is allowlisted and does not require pipes, redirection, or shell expansion.",
|
|
2195
|
+
useInstead: ["exec"]
|
|
2196
|
+
},
|
|
2194
2197
|
permission: "confirm",
|
|
2195
2198
|
mutating: true,
|
|
2196
2199
|
riskTier: "destructive",
|
|
@@ -2999,6 +3002,24 @@ var IndexStore = class {
|
|
|
2999
3002
|
* When false, ranked search falls back to the LIKE + in-process BM25 path.
|
|
3000
3003
|
*/
|
|
3001
3004
|
ftsAvailable = false;
|
|
3005
|
+
/**
|
|
3006
|
+
* Cache of prepared statements keyed by their SQL text. `DatabaseSync`
|
|
3007
|
+
* compiles SQL on every `.prepare()` call; for the fixed-SQL methods
|
|
3008
|
+
* (upsertFile, getFileMeta, deleteFile, insertRefs, …) that runs thousands
|
|
3009
|
+
* of times during a full reindex. `StatementSync` objects are reusable
|
|
3010
|
+
* across calls on the same connection, so we compile each distinct SQL once
|
|
3011
|
+
* and reuse it. Cleared in {@link close} when the connection is torn down.
|
|
3012
|
+
*/
|
|
3013
|
+
stmtCache = /* @__PURE__ */ new Map();
|
|
3014
|
+
/** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
|
|
3015
|
+
stmt(sql) {
|
|
3016
|
+
let s = this.stmtCache.get(sql);
|
|
3017
|
+
if (s === void 0) {
|
|
3018
|
+
s = this.db.prepare(sql);
|
|
3019
|
+
this.stmtCache.set(sql, s);
|
|
3020
|
+
}
|
|
3021
|
+
return s;
|
|
3022
|
+
}
|
|
3002
3023
|
/**
|
|
3003
3024
|
* Execute a SQLite write operation with automatic retry on lock conflicts.
|
|
3004
3025
|
*
|
|
@@ -3037,7 +3058,7 @@ var IndexStore = class {
|
|
|
3037
3058
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
3038
3059
|
fs.mkdirSync(this.indexDir, { recursive: true });
|
|
3039
3060
|
const Database = loadDatabaseSync();
|
|
3040
|
-
this.db = new Database(
|
|
3061
|
+
this.db = new Database(path12.join(this.indexDir, DB_FILE));
|
|
3041
3062
|
try {
|
|
3042
3063
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
3043
3064
|
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -3168,9 +3189,9 @@ var IndexStore = class {
|
|
|
3168
3189
|
deleteSymbolsForFile(file) {
|
|
3169
3190
|
this.runWithRetry(() => {
|
|
3170
3191
|
if (this.ftsAvailable) {
|
|
3171
|
-
this.
|
|
3192
|
+
this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
|
|
3172
3193
|
}
|
|
3173
|
-
this.
|
|
3194
|
+
this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
3174
3195
|
});
|
|
3175
3196
|
}
|
|
3176
3197
|
/**
|
|
@@ -3182,13 +3203,13 @@ var IndexStore = class {
|
|
|
3182
3203
|
this.runWithRetry(() => {
|
|
3183
3204
|
this.deleteRefsForFile(file);
|
|
3184
3205
|
this.deleteSymbolsForFile(file);
|
|
3185
|
-
this.
|
|
3206
|
+
this.stmt("DELETE FROM files WHERE file = ?").run(file);
|
|
3186
3207
|
});
|
|
3187
3208
|
}
|
|
3188
3209
|
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
3189
3210
|
upsertFile(meta) {
|
|
3190
3211
|
this.runWithRetry(() => {
|
|
3191
|
-
this.
|
|
3212
|
+
this.stmt(
|
|
3192
3213
|
`INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
|
|
3193
3214
|
VALUES (?, ?, ?, ?, ?)
|
|
3194
3215
|
ON CONFLICT(file) DO UPDATE SET
|
|
@@ -3200,7 +3221,7 @@ var IndexStore = class {
|
|
|
3200
3221
|
});
|
|
3201
3222
|
}
|
|
3202
3223
|
getFileMeta(file) {
|
|
3203
|
-
const rows = this.
|
|
3224
|
+
const rows = this.stmt(
|
|
3204
3225
|
"SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files WHERE file = ?"
|
|
3205
3226
|
).all(file);
|
|
3206
3227
|
if (!rows.length) return null;
|
|
@@ -3423,7 +3444,7 @@ var IndexStore = class {
|
|
|
3423
3444
|
*/
|
|
3424
3445
|
insertRefs(fromId, refs) {
|
|
3425
3446
|
this.runWithRetry(() => {
|
|
3426
|
-
this.
|
|
3447
|
+
this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
|
|
3427
3448
|
if (refs.length === 0) return;
|
|
3428
3449
|
const stmt = this.db.prepare(
|
|
3429
3450
|
`INSERT INTO refs(from_id, to_name, to_id, call_type, line)
|
|
@@ -3616,7 +3637,7 @@ var IndexStore = class {
|
|
|
3616
3637
|
}));
|
|
3617
3638
|
}
|
|
3618
3639
|
sizeBytes() {
|
|
3619
|
-
const dbPath =
|
|
3640
|
+
const dbPath = path12.join(this.indexDir, DB_FILE);
|
|
3620
3641
|
try {
|
|
3621
3642
|
return fs.statSync(dbPath).size;
|
|
3622
3643
|
} catch {
|
|
@@ -3624,6 +3645,7 @@ var IndexStore = class {
|
|
|
3624
3645
|
}
|
|
3625
3646
|
}
|
|
3626
3647
|
close() {
|
|
3648
|
+
this.stmtCache.clear();
|
|
3627
3649
|
try {
|
|
3628
3650
|
this.db.close();
|
|
3629
3651
|
} catch {
|
|
@@ -4114,9 +4136,9 @@ func formatType(t ast.Expr) string {
|
|
|
4114
4136
|
}
|
|
4115
4137
|
`;
|
|
4116
4138
|
async function syncGoParse(filePath, content, lang) {
|
|
4117
|
-
const tmpDir = await fs2.mkdtemp(
|
|
4139
|
+
const tmpDir = await fs2.mkdtemp(path12.join(os2.tmpdir(), "ws-go-parse-"));
|
|
4118
4140
|
try {
|
|
4119
|
-
const scriptPath =
|
|
4141
|
+
const scriptPath = path12.join(tmpDir, "parse.go");
|
|
4120
4142
|
await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
4121
4143
|
const proc = spawn("go", ["run", scriptPath], {
|
|
4122
4144
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -4378,9 +4400,9 @@ var _cachedScriptPath = null;
|
|
|
4378
4400
|
async function syncPyParse(filePath, content, lang) {
|
|
4379
4401
|
try {
|
|
4380
4402
|
if (!_cachedScriptPath) {
|
|
4381
|
-
const tmpDir =
|
|
4403
|
+
const tmpDir = path12.join(os2.tmpdir(), "ws-py-parse");
|
|
4382
4404
|
await fs2.mkdir(tmpDir, { recursive: true });
|
|
4383
|
-
_cachedScriptPath =
|
|
4405
|
+
_cachedScriptPath = path12.join(tmpDir, "parse.py");
|
|
4384
4406
|
await fs2.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
4385
4407
|
}
|
|
4386
4408
|
const proc = spawn("python", [_cachedScriptPath, filePath], {
|
|
@@ -4438,7 +4460,7 @@ async function parseSymbols4(opts) {
|
|
|
4438
4460
|
function checkNativeParser() {
|
|
4439
4461
|
try {
|
|
4440
4462
|
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
4441
|
-
const toolsDir =
|
|
4463
|
+
const toolsDir = path12.join(process.cwd(), "tools");
|
|
4442
4464
|
try {
|
|
4443
4465
|
execFileSync(
|
|
4444
4466
|
"cargo",
|
|
@@ -4448,7 +4470,7 @@ function checkNativeParser() {
|
|
|
4448
4470
|
"--format-version",
|
|
4449
4471
|
"1",
|
|
4450
4472
|
"--manifest-path",
|
|
4451
|
-
|
|
4473
|
+
path12.join(toolsDir, "Cargo.toml")
|
|
4452
4474
|
],
|
|
4453
4475
|
{ stdio: "pipe", windowsHide: true }
|
|
4454
4476
|
);
|
|
@@ -4462,13 +4484,13 @@ function checkNativeParser() {
|
|
|
4462
4484
|
}
|
|
4463
4485
|
async function tryNativeParse(file, content) {
|
|
4464
4486
|
try {
|
|
4465
|
-
const toolsDir =
|
|
4466
|
-
const crateDir =
|
|
4467
|
-
const tmpFile =
|
|
4487
|
+
const toolsDir = path12.join(process.cwd(), "tools");
|
|
4488
|
+
const crateDir = path12.join(toolsDir, "syn-parser");
|
|
4489
|
+
const tmpFile = path12.join(crateDir, "src", "input.rs");
|
|
4468
4490
|
await fs2.writeFile(tmpFile, content, "utf8");
|
|
4469
4491
|
const proc = spawn(
|
|
4470
4492
|
"cargo",
|
|
4471
|
-
["run", "--manifest-path",
|
|
4493
|
+
["run", "--manifest-path", path12.join(toolsDir, "Cargo.toml")],
|
|
4472
4494
|
{
|
|
4473
4495
|
cwd: process.cwd(),
|
|
4474
4496
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -4518,16 +4540,16 @@ function regexParse(opts) {
|
|
|
4518
4540
|
const { file, content, lang } = opts;
|
|
4519
4541
|
const symbols = [];
|
|
4520
4542
|
const lines = content.split("\n");
|
|
4521
|
-
const
|
|
4543
|
+
const lineOffsets2 = [0];
|
|
4522
4544
|
for (let i = 0; i < lines.length; i++) {
|
|
4523
|
-
|
|
4545
|
+
lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
|
|
4524
4546
|
}
|
|
4525
4547
|
function lineFromOffset(offset) {
|
|
4526
4548
|
let lo = 0;
|
|
4527
|
-
let hi =
|
|
4549
|
+
let hi = lineOffsets2.length - 1;
|
|
4528
4550
|
while (lo < hi) {
|
|
4529
4551
|
const mid = lo + hi + 1 >>> 1;
|
|
4530
|
-
if (expectDefined(
|
|
4552
|
+
if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
|
|
4531
4553
|
else hi = mid - 1;
|
|
4532
4554
|
}
|
|
4533
4555
|
return lo + 1;
|
|
@@ -4542,7 +4564,7 @@ function regexParse(opts) {
|
|
|
4542
4564
|
const name = expectDefined(match[1]);
|
|
4543
4565
|
const offset = match.index ?? 0;
|
|
4544
4566
|
const line = lineFromOffset(offset);
|
|
4545
|
-
const col = offset - (
|
|
4567
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4546
4568
|
const lineIdx = line - 1;
|
|
4547
4569
|
const signature = extractDeclaration(lineIdx);
|
|
4548
4570
|
symbols.push({
|
|
@@ -4580,22 +4602,22 @@ function parseSymbols5(opts) {
|
|
|
4580
4602
|
function regexParse2(opts) {
|
|
4581
4603
|
const { file, content, lang } = opts;
|
|
4582
4604
|
const symbols = [];
|
|
4583
|
-
const
|
|
4584
|
-
const isPackageJson =
|
|
4585
|
-
const isTsconfig =
|
|
4605
|
+
const basename4 = path12.basename(file).toLowerCase();
|
|
4606
|
+
const isPackageJson = basename4 === "package.json";
|
|
4607
|
+
const isTsconfig = basename4 === "tsconfig.json" || basename4 === "tsconfig.build.json";
|
|
4586
4608
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
4587
4609
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
4588
4610
|
const lines = content.split("\n");
|
|
4589
|
-
const
|
|
4611
|
+
const lineOffsets2 = [0];
|
|
4590
4612
|
for (let i = 0; i < lines.length; i++) {
|
|
4591
|
-
|
|
4613
|
+
lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
|
|
4592
4614
|
}
|
|
4593
4615
|
function lineFromOffset(offset) {
|
|
4594
4616
|
let lo = 0;
|
|
4595
|
-
let hi =
|
|
4617
|
+
let hi = lineOffsets2.length - 1;
|
|
4596
4618
|
while (lo < hi) {
|
|
4597
4619
|
const mid = lo + hi + 1 >>> 1;
|
|
4598
|
-
if (expectDefined(
|
|
4620
|
+
if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
|
|
4599
4621
|
else hi = mid - 1;
|
|
4600
4622
|
}
|
|
4601
4623
|
return lo + 1;
|
|
@@ -4606,11 +4628,11 @@ function regexParse2(opts) {
|
|
|
4606
4628
|
const line = lineFromOffset(offset);
|
|
4607
4629
|
symbols.push(
|
|
4608
4630
|
makeSymbol({
|
|
4609
|
-
name:
|
|
4631
|
+
name: path12.basename(file),
|
|
4610
4632
|
kind: "object",
|
|
4611
4633
|
line,
|
|
4612
4634
|
col: 0,
|
|
4613
|
-
signature: `"${
|
|
4635
|
+
signature: `"${path12.basename(file)}" = { ... }`,
|
|
4614
4636
|
file,
|
|
4615
4637
|
lang
|
|
4616
4638
|
})
|
|
@@ -4621,7 +4643,7 @@ function regexParse2(opts) {
|
|
|
4621
4643
|
const key = expectDefined(match[1]);
|
|
4622
4644
|
const offset = match.index ?? 0;
|
|
4623
4645
|
const line = lineFromOffset(offset);
|
|
4624
|
-
const col = offset - (
|
|
4646
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4625
4647
|
let kind = "property";
|
|
4626
4648
|
let signature = `"${key}": ..."`;
|
|
4627
4649
|
if (isPackageJson) {
|
|
@@ -4656,10 +4678,10 @@ function regexParse2(opts) {
|
|
|
4656
4678
|
})
|
|
4657
4679
|
);
|
|
4658
4680
|
if (isPackageJson && key === "scripts") {
|
|
4659
|
-
extractPackageScripts(content, symbols, file, lang,
|
|
4681
|
+
extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset);
|
|
4660
4682
|
}
|
|
4661
4683
|
if (isTsconfig && key === "compilerOptions") {
|
|
4662
|
-
extractCompilerOptions(content, symbols, file, lang,
|
|
4684
|
+
extractCompilerOptions(content, symbols, file, lang, lineOffsets2, line, lineFromOffset);
|
|
4663
4685
|
}
|
|
4664
4686
|
}
|
|
4665
4687
|
const defsRegex = /"\$defs"\s*:|"\$defs"\s*:/g;
|
|
@@ -4672,7 +4694,7 @@ function regexParse2(opts) {
|
|
|
4672
4694
|
name: "$defs",
|
|
4673
4695
|
kind: "property",
|
|
4674
4696
|
line,
|
|
4675
|
-
col: offset - (
|
|
4697
|
+
col: offset - (lineOffsets2[line - 1] ?? 0),
|
|
4676
4698
|
signature: '"$defs": { ... }',
|
|
4677
4699
|
file,
|
|
4678
4700
|
lang
|
|
@@ -4696,7 +4718,7 @@ function regexParse2(opts) {
|
|
|
4696
4718
|
name: key,
|
|
4697
4719
|
kind: "property",
|
|
4698
4720
|
line,
|
|
4699
|
-
col: offset - (
|
|
4721
|
+
col: offset - (lineOffsets2[line - 1] ?? 0),
|
|
4700
4722
|
signature: `"${key}": { ... }`,
|
|
4701
4723
|
file,
|
|
4702
4724
|
lang
|
|
@@ -4706,7 +4728,7 @@ function regexParse2(opts) {
|
|
|
4706
4728
|
}
|
|
4707
4729
|
return { file, lang, symbols, mtimeMs: Date.now() };
|
|
4708
4730
|
}
|
|
4709
|
-
function extractPackageScripts(content, symbols, file, lang,
|
|
4731
|
+
function extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset) {
|
|
4710
4732
|
const scriptsBlockRegex = /"scripts"\s*:\s*\{([^}]+)\}/g;
|
|
4711
4733
|
for (let match = scriptsBlockRegex.exec(content); match !== null; match = scriptsBlockRegex.exec(content)) {
|
|
4712
4734
|
const blockContent = expectDefined(match[0]);
|
|
@@ -4721,7 +4743,7 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFr
|
|
|
4721
4743
|
name: key,
|
|
4722
4744
|
kind: "function",
|
|
4723
4745
|
line,
|
|
4724
|
-
col: keyOffset - (
|
|
4746
|
+
col: keyOffset - (lineOffsets2[line - 1] ?? 0),
|
|
4725
4747
|
signature: `"${key}": "..."`,
|
|
4726
4748
|
file,
|
|
4727
4749
|
lang
|
|
@@ -4730,7 +4752,7 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFr
|
|
|
4730
4752
|
}
|
|
4731
4753
|
}
|
|
4732
4754
|
}
|
|
4733
|
-
function extractCompilerOptions(content, symbols, file, lang,
|
|
4755
|
+
function extractCompilerOptions(content, symbols, file, lang, lineOffsets2, parentLine, lineFromOffset) {
|
|
4734
4756
|
const optsBlockRegex = /"compilerOptions"\s*:\s*\{([^}]+)\}/g;
|
|
4735
4757
|
for (let match = optsBlockRegex.exec(content); match !== null; match = optsBlockRegex.exec(content)) {
|
|
4736
4758
|
const blockContent = expectDefined(match[0]);
|
|
@@ -4746,7 +4768,7 @@ function extractCompilerOptions(content, symbols, file, lang, lineOffsets, paren
|
|
|
4746
4768
|
name: key,
|
|
4747
4769
|
kind: "property",
|
|
4748
4770
|
line,
|
|
4749
|
-
col: keyOffset - (
|
|
4771
|
+
col: keyOffset - (lineOffsets2[line - 1] ?? 0),
|
|
4750
4772
|
signature: `"${key}": ...`,
|
|
4751
4773
|
file,
|
|
4752
4774
|
lang
|
|
@@ -4782,16 +4804,16 @@ function regexParse3(opts) {
|
|
|
4782
4804
|
const { file, content, lang } = opts;
|
|
4783
4805
|
const symbols = [];
|
|
4784
4806
|
const lines = content.split("\n");
|
|
4785
|
-
const
|
|
4807
|
+
const lineOffsets2 = [0];
|
|
4786
4808
|
for (let i = 0; i < lines.length; i++) {
|
|
4787
|
-
|
|
4809
|
+
lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
|
|
4788
4810
|
}
|
|
4789
4811
|
function lineFromOffset(offset) {
|
|
4790
4812
|
let lo = 0;
|
|
4791
|
-
let hi =
|
|
4813
|
+
let hi = lineOffsets2.length - 1;
|
|
4792
4814
|
while (lo < hi) {
|
|
4793
4815
|
const mid = lo + hi + 1 >>> 1;
|
|
4794
|
-
if (expectDefined(
|
|
4816
|
+
if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
|
|
4795
4817
|
else hi = mid - 1;
|
|
4796
4818
|
}
|
|
4797
4819
|
return lo + 1;
|
|
@@ -4801,7 +4823,7 @@ function regexParse3(opts) {
|
|
|
4801
4823
|
const name = expectDefined(match[1]);
|
|
4802
4824
|
const offset = match.index ?? 0;
|
|
4803
4825
|
const line = lineFromOffset(offset);
|
|
4804
|
-
const col = offset - (
|
|
4826
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4805
4827
|
symbols.push(
|
|
4806
4828
|
makeSymbol2({
|
|
4807
4829
|
name,
|
|
@@ -4819,7 +4841,7 @@ function regexParse3(opts) {
|
|
|
4819
4841
|
const name = expectDefined(match[1]);
|
|
4820
4842
|
const offset = match.index ?? 0;
|
|
4821
4843
|
const line = lineFromOffset(offset);
|
|
4822
|
-
const col = offset - (
|
|
4844
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4823
4845
|
symbols.push(
|
|
4824
4846
|
makeSymbol2({
|
|
4825
4847
|
name,
|
|
@@ -4839,7 +4861,7 @@ function regexParse3(opts) {
|
|
|
4839
4861
|
if (!key) continue;
|
|
4840
4862
|
const offset = match.index ?? 0;
|
|
4841
4863
|
const line = lineFromOffset(offset);
|
|
4842
|
-
const col = offset - (
|
|
4864
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4843
4865
|
const lineContent = lines[line - 1] ?? "";
|
|
4844
4866
|
if (/^[|&>]/.test(lineContent.trim())) continue;
|
|
4845
4867
|
if (key === "---" || key === "...") continue;
|
|
@@ -4854,7 +4876,7 @@ function regexParse3(opts) {
|
|
|
4854
4876
|
const key = expectDefined(match[2]);
|
|
4855
4877
|
const offset = match.index ?? 0;
|
|
4856
4878
|
const line = lineFromOffset(offset);
|
|
4857
|
-
const col = offset - (
|
|
4879
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4858
4880
|
const value = extractValue(content, offset + match[0]?.length);
|
|
4859
4881
|
const kind = isScalar(value) ? "literal" : "property";
|
|
4860
4882
|
symbols.push(
|
|
@@ -4874,7 +4896,7 @@ function regexParse3(opts) {
|
|
|
4874
4896
|
const key = expectDefined(match[2]);
|
|
4875
4897
|
const offset = match.index ?? 0;
|
|
4876
4898
|
const line = lineFromOffset(offset);
|
|
4877
|
-
const col = offset - (
|
|
4899
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4878
4900
|
symbols.push(
|
|
4879
4901
|
makeSymbol2({
|
|
4880
4902
|
name: key,
|
|
@@ -4960,7 +4982,7 @@ function compileGitignore(lines) {
|
|
|
4960
4982
|
async function loadGitignoreMatcher(projectRoot) {
|
|
4961
4983
|
let lines = [];
|
|
4962
4984
|
try {
|
|
4963
|
-
const raw = await fs2.readFile(
|
|
4985
|
+
const raw = await fs2.readFile(path12.join(projectRoot, ".gitignore"), "utf8");
|
|
4964
4986
|
lines = raw.split("\n");
|
|
4965
4987
|
} catch {
|
|
4966
4988
|
}
|
|
@@ -5025,14 +5047,14 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
5025
5047
|
dirCount++;
|
|
5026
5048
|
for (const e of entries) {
|
|
5027
5049
|
if (ignoreSet.has(e.name)) continue;
|
|
5028
|
-
const full =
|
|
5029
|
-
const rel =
|
|
5050
|
+
const full = path12.join(dir, e.name);
|
|
5051
|
+
const rel = path12.relative(projectRoot, full).replace(/\\/g, "/");
|
|
5030
5052
|
if (e.isDirectory()) {
|
|
5031
5053
|
if (isGitIgnored(rel, true)) continue;
|
|
5032
5054
|
await walk2(full);
|
|
5033
5055
|
} else if (e.isFile()) {
|
|
5034
5056
|
if (isGitIgnored(rel, false)) continue;
|
|
5035
|
-
const ext =
|
|
5057
|
+
const ext = path12.extname(e.name);
|
|
5036
5058
|
for (const { ext: extName, pat } of globs) {
|
|
5037
5059
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
5038
5060
|
results.push(full);
|
|
@@ -5088,7 +5110,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5088
5110
|
let files;
|
|
5089
5111
|
let discoveredFiles = null;
|
|
5090
5112
|
if (opts.files && opts.files.length > 0) {
|
|
5091
|
-
files = opts.files.map((f) =>
|
|
5113
|
+
files = opts.files.map((f) => path12.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path12.relative(projectRoot, f).replace(/\\/g, "/"), false));
|
|
5092
5114
|
} else {
|
|
5093
5115
|
files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
5094
5116
|
discoveredFiles = new Set(files);
|
|
@@ -5116,34 +5138,34 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5116
5138
|
const statOpts = signal ? { signal } : {};
|
|
5117
5139
|
const statReadParse = await Promise.allSettled(
|
|
5118
5140
|
batchFiles.map(async (file) => {
|
|
5119
|
-
let
|
|
5141
|
+
let stat13;
|
|
5120
5142
|
try {
|
|
5121
|
-
|
|
5143
|
+
stat13 = await fs2.stat(file, statOpts);
|
|
5122
5144
|
} catch (e) {
|
|
5123
5145
|
if (isAbortError(e)) throw e;
|
|
5124
5146
|
return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
|
|
5125
5147
|
}
|
|
5126
|
-
if (!
|
|
5148
|
+
if (!stat13.isFile()) return { file, stat: stat13, lang: "", parsed: null };
|
|
5127
5149
|
const lang = detectLang(file);
|
|
5128
|
-
if (!lang) return { file, stat:
|
|
5150
|
+
if (!lang) return { file, stat: stat13, lang: "", parsed: null };
|
|
5129
5151
|
const meta = existingMeta.get(file);
|
|
5130
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
5131
|
-
return { file, stat:
|
|
5152
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat13.mtimeMs)) {
|
|
5153
|
+
return { file, stat: stat13, lang, parsed: null, skippedMeta: meta };
|
|
5132
5154
|
}
|
|
5133
5155
|
let content;
|
|
5134
5156
|
try {
|
|
5135
5157
|
content = await fs2.readFile(file, { encoding: "utf8", signal });
|
|
5136
5158
|
} catch (e) {
|
|
5137
5159
|
if (isAbortError(e)) throw e;
|
|
5138
|
-
return { file, stat:
|
|
5160
|
+
return { file, stat: stat13, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
|
|
5139
5161
|
}
|
|
5140
5162
|
let parsed;
|
|
5141
5163
|
try {
|
|
5142
5164
|
parsed = await parseFile(file, content, lang);
|
|
5143
5165
|
} catch (e) {
|
|
5144
|
-
return { file, stat:
|
|
5166
|
+
return { file, stat: stat13, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
|
|
5145
5167
|
}
|
|
5146
|
-
return { file, stat:
|
|
5168
|
+
return { file, stat: stat13, lang, parsed, content };
|
|
5147
5169
|
})
|
|
5148
5170
|
);
|
|
5149
5171
|
const batchEntries = [];
|
|
@@ -5163,7 +5185,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5163
5185
|
if (result.error.includes("error:")) errors.push(result.error);
|
|
5164
5186
|
continue;
|
|
5165
5187
|
}
|
|
5166
|
-
const { stat:
|
|
5188
|
+
const { stat: stat13, lang, parsed } = result;
|
|
5167
5189
|
if (result.skippedMeta) {
|
|
5168
5190
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
5169
5191
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -5175,7 +5197,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5175
5197
|
store.upsertFile({
|
|
5176
5198
|
file,
|
|
5177
5199
|
lang,
|
|
5178
|
-
mtimeMs: Math.floor(
|
|
5200
|
+
mtimeMs: Math.floor(stat13.mtimeMs),
|
|
5179
5201
|
symbolCount: 0,
|
|
5180
5202
|
lastIndexed: Date.now()
|
|
5181
5203
|
});
|
|
@@ -5187,7 +5209,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5187
5209
|
store.upsertFile({
|
|
5188
5210
|
file,
|
|
5189
5211
|
lang,
|
|
5190
|
-
mtimeMs: Math.floor(
|
|
5212
|
+
mtimeMs: Math.floor(stat13.mtimeMs),
|
|
5191
5213
|
symbolCount: 0,
|
|
5192
5214
|
lastIndexed: Date.now()
|
|
5193
5215
|
});
|
|
@@ -5203,7 +5225,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
5203
5225
|
lang,
|
|
5204
5226
|
symbols: parsed.symbols,
|
|
5205
5227
|
refs,
|
|
5206
|
-
mtimeMs: Math.floor(
|
|
5228
|
+
mtimeMs: Math.floor(stat13.mtimeMs),
|
|
5207
5229
|
symbolCount: parsed.symbols.length
|
|
5208
5230
|
});
|
|
5209
5231
|
deleteForFiles.push(file);
|
|
@@ -5352,8 +5374,8 @@ function statsService(args) {
|
|
|
5352
5374
|
}
|
|
5353
5375
|
|
|
5354
5376
|
// src/codebase-index/background-indexer.ts
|
|
5355
|
-
var DEFAULT_FULL_INDEX_TIMEOUT_MS =
|
|
5356
|
-
var DEFAULT_INCREMENTAL_TIMEOUT_MS =
|
|
5377
|
+
var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
|
|
5378
|
+
var DEFAULT_INCREMENTAL_TIMEOUT_MS = 6e4;
|
|
5357
5379
|
var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
|
|
5358
5380
|
var _ready = false;
|
|
5359
5381
|
var _indexing = false;
|
|
@@ -5868,6 +5890,25 @@ var codebaseStatsTool = {
|
|
|
5868
5890
|
};
|
|
5869
5891
|
}
|
|
5870
5892
|
};
|
|
5893
|
+
async function resolveReal(p) {
|
|
5894
|
+
const resolved = path12.resolve(p);
|
|
5895
|
+
let probe = resolved;
|
|
5896
|
+
const missing = [];
|
|
5897
|
+
for (; ; ) {
|
|
5898
|
+
try {
|
|
5899
|
+
return path12.resolve(await fs2.realpath(probe), ...missing);
|
|
5900
|
+
} catch (err) {
|
|
5901
|
+
if (err.code === "ENOENT") {
|
|
5902
|
+
const parent = path12.dirname(probe);
|
|
5903
|
+
if (parent === probe) return resolved;
|
|
5904
|
+
missing.unshift(path12.basename(probe));
|
|
5905
|
+
probe = parent;
|
|
5906
|
+
continue;
|
|
5907
|
+
}
|
|
5908
|
+
return resolved;
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5871
5912
|
function normalizeOverrides(set) {
|
|
5872
5913
|
const out = {};
|
|
5873
5914
|
if (set && typeof set === "object") {
|
|
@@ -6025,10 +6066,12 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
|
|
|
6025
6066
|
kitId: active.kit,
|
|
6026
6067
|
outPath: input.out
|
|
6027
6068
|
});
|
|
6028
|
-
const root =
|
|
6029
|
-
const
|
|
6030
|
-
const
|
|
6031
|
-
|
|
6069
|
+
const root = await resolveReal(ctx.projectRoot);
|
|
6070
|
+
const absResolved = path12.isAbsolute(result.path) ? path12.resolve(result.path) : path12.resolve(path12.join(ctx.projectRoot, result.path));
|
|
6071
|
+
const absParent = await resolveReal(path12.dirname(absResolved));
|
|
6072
|
+
const abs = path12.join(absParent, path12.basename(absResolved));
|
|
6073
|
+
const rel = path12.relative(root, abs);
|
|
6074
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) {
|
|
6032
6075
|
throw new Error(
|
|
6033
6076
|
`design: materialize path "${result.path}" would escape the project root`
|
|
6034
6077
|
);
|
|
@@ -6052,7 +6095,7 @@ ${result.content}
|
|
|
6052
6095
|
\`\`\``
|
|
6053
6096
|
};
|
|
6054
6097
|
}
|
|
6055
|
-
await fs2.mkdir(
|
|
6098
|
+
await fs2.mkdir(path12.dirname(abs), { recursive: true });
|
|
6056
6099
|
await fs2.writeFile(abs, result.content);
|
|
6057
6100
|
return {
|
|
6058
6101
|
action,
|
|
@@ -6180,11 +6223,11 @@ function findGitDir(cwd) {
|
|
|
6180
6223
|
let dir = cwd;
|
|
6181
6224
|
for (let i = 0; i < 20; i++) {
|
|
6182
6225
|
try {
|
|
6183
|
-
const
|
|
6184
|
-
if (
|
|
6226
|
+
const stat13 = statSync(path12.join(dir, ".git"));
|
|
6227
|
+
if (stat13.isDirectory()) return dir;
|
|
6185
6228
|
} catch {
|
|
6186
6229
|
}
|
|
6187
|
-
const parent =
|
|
6230
|
+
const parent = path12.dirname(dir);
|
|
6188
6231
|
if (parent === dir) break;
|
|
6189
6232
|
dir = parent;
|
|
6190
6233
|
}
|
|
@@ -6225,8 +6268,8 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
6225
6268
|
const results = [];
|
|
6226
6269
|
for (const file of files) {
|
|
6227
6270
|
const absPath = safeResolve(file, ctx);
|
|
6228
|
-
const
|
|
6229
|
-
if (!
|
|
6271
|
+
const stat13 = await fs2.stat(absPath).catch(() => null);
|
|
6272
|
+
if (!stat13?.isFile()) continue;
|
|
6230
6273
|
const content = await fs2.readFile(absPath, "utf8");
|
|
6231
6274
|
const lines = content.split(/\r?\n/);
|
|
6232
6275
|
results.push(formatWithLineNumbers(file, lines));
|
|
@@ -6323,8 +6366,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
6323
6366
|
for (const f of files) {
|
|
6324
6367
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
6325
6368
|
try {
|
|
6326
|
-
const
|
|
6327
|
-
if (
|
|
6369
|
+
const stat13 = await fs2.stat(absPath);
|
|
6370
|
+
if (stat13.isFile()) resolved.push(absPath);
|
|
6328
6371
|
} catch {
|
|
6329
6372
|
}
|
|
6330
6373
|
}
|
|
@@ -6390,11 +6433,303 @@ function processFile(content, absPath, _style, _overwrite, target) {
|
|
|
6390
6433
|
}
|
|
6391
6434
|
return results;
|
|
6392
6435
|
}
|
|
6436
|
+
|
|
6437
|
+
// src/_edit-match.ts
|
|
6438
|
+
var TIER_LABEL = {
|
|
6439
|
+
exact: "exact match",
|
|
6440
|
+
"trailing-whitespace": "whitespace-normalized match (trailing whitespace ignored)",
|
|
6441
|
+
"whitespace-normalized": "whitespace-normalized match (indentation ignored, replacement re-indented)",
|
|
6442
|
+
fuzzy: "fuzzy match (block anchors exact, interior \u226590% similar)"
|
|
6443
|
+
};
|
|
6444
|
+
var TIER_CONFIDENCE = {
|
|
6445
|
+
exact: "exact",
|
|
6446
|
+
"trailing-whitespace": "high",
|
|
6447
|
+
"whitespace-normalized": "medium",
|
|
6448
|
+
fuzzy: "low"
|
|
6449
|
+
};
|
|
6450
|
+
var MIN_NORMALIZED_NEEDLE_CHARS = 16;
|
|
6451
|
+
var FUZZY_MIN_SIMILARITY = 0.9;
|
|
6452
|
+
var FUZZY_AMBIGUITY_MARGIN = 0.05;
|
|
6453
|
+
var FUZZY_MAX_INTERIOR_CHARS = 2e3;
|
|
6454
|
+
function findLadderMatches(fileLf, oldLf) {
|
|
6455
|
+
const exact = [];
|
|
6456
|
+
let idx = fileLf.indexOf(oldLf);
|
|
6457
|
+
while (idx !== -1) {
|
|
6458
|
+
exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });
|
|
6459
|
+
idx = fileLf.indexOf(oldLf, idx + 1);
|
|
6460
|
+
}
|
|
6461
|
+
if (exact.length > 0) return { tier: "exact", matches: exact };
|
|
6462
|
+
const fileLines = fileLf.split("\n");
|
|
6463
|
+
const needleLines = oldLf.split("\n");
|
|
6464
|
+
if (needleLines.length > fileLines.length) return void 0;
|
|
6465
|
+
const offsets = lineOffsets(fileLines);
|
|
6466
|
+
const trailing = windowScan(
|
|
6467
|
+
fileLines,
|
|
6468
|
+
needleLines,
|
|
6469
|
+
offsets,
|
|
6470
|
+
(a, b) => a.trimEnd() === b.trimEnd()
|
|
6471
|
+
);
|
|
6472
|
+
if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
|
|
6473
|
+
const normalizedLen = needleLines.reduce((n, l) => n + l.trim().length, 0);
|
|
6474
|
+
if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
|
|
6475
|
+
const normalized = windowScan(fileLines, needleLines, offsets, (a, b) => a.trim() === b.trim());
|
|
6476
|
+
if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
|
|
6477
|
+
return fuzzyScan(fileLines, needleLines, offsets);
|
|
6478
|
+
}
|
|
6479
|
+
function lineAt(text, pos) {
|
|
6480
|
+
let line = 1;
|
|
6481
|
+
for (let i = 0; i < pos; i++) {
|
|
6482
|
+
if (text.charCodeAt(i) === 10) line++;
|
|
6483
|
+
}
|
|
6484
|
+
return line;
|
|
6485
|
+
}
|
|
6486
|
+
function lineOffsets(lines) {
|
|
6487
|
+
const out = new Array(lines.length);
|
|
6488
|
+
let pos = 0;
|
|
6489
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6490
|
+
out[i] = pos;
|
|
6491
|
+
pos += lines[i].length + 1;
|
|
6492
|
+
}
|
|
6493
|
+
return out;
|
|
6494
|
+
}
|
|
6495
|
+
function windowToMatch(fileLines, offsets, start, windowLen) {
|
|
6496
|
+
const lastLine = start + windowLen - 1;
|
|
6497
|
+
return {
|
|
6498
|
+
start: offsets[start],
|
|
6499
|
+
end: offsets[lastLine] + fileLines[lastLine].length,
|
|
6500
|
+
startLine: start + 1
|
|
6501
|
+
};
|
|
6502
|
+
}
|
|
6503
|
+
function windowScan(fileLines, needleLines, offsets, eq) {
|
|
6504
|
+
const n = needleLines.length;
|
|
6505
|
+
const out = [];
|
|
6506
|
+
for (let i = 0; i + n <= fileLines.length; i++) {
|
|
6507
|
+
let all = true;
|
|
6508
|
+
for (let j = 0; j < n; j++) {
|
|
6509
|
+
if (!eq(fileLines[i + j], needleLines[j])) {
|
|
6510
|
+
all = false;
|
|
6511
|
+
break;
|
|
6512
|
+
}
|
|
6513
|
+
}
|
|
6514
|
+
if (all) {
|
|
6515
|
+
out.push(windowToMatch(fileLines, offsets, i, n));
|
|
6516
|
+
i += n - 1;
|
|
6517
|
+
}
|
|
6518
|
+
}
|
|
6519
|
+
return out;
|
|
6520
|
+
}
|
|
6521
|
+
function fuzzyScan(fileLines, needleLines, offsets) {
|
|
6522
|
+
const n = needleLines.length;
|
|
6523
|
+
if (n < 3) return void 0;
|
|
6524
|
+
const firstNeedle = needleLines[0].trim();
|
|
6525
|
+
const lastNeedle = needleLines[n - 1].trim();
|
|
6526
|
+
const needleInterior = needleLines.slice(1, -1).map((l) => l.trim()).join("\n");
|
|
6527
|
+
if (needleInterior.length > FUZZY_MAX_INTERIOR_CHARS) return void 0;
|
|
6528
|
+
const candidates = [];
|
|
6529
|
+
for (let i = 0; i + n <= fileLines.length; i++) {
|
|
6530
|
+
if (fileLines[i].trim() !== firstNeedle) continue;
|
|
6531
|
+
if (fileLines[i + n - 1].trim() !== lastNeedle) continue;
|
|
6532
|
+
const windowInterior = fileLines.slice(i + 1, i + n - 1).map((l) => l.trim()).join("\n");
|
|
6533
|
+
if (windowInterior.length > FUZZY_MAX_INTERIOR_CHARS) continue;
|
|
6534
|
+
const score = similarity(needleInterior, windowInterior);
|
|
6535
|
+
if (score >= FUZZY_MIN_SIMILARITY) {
|
|
6536
|
+
candidates.push({ match: windowToMatch(fileLines, offsets, i, n), score });
|
|
6537
|
+
}
|
|
6538
|
+
}
|
|
6539
|
+
if (candidates.length === 0) return void 0;
|
|
6540
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
6541
|
+
const best = candidates[0];
|
|
6542
|
+
const runnerUp = candidates[1];
|
|
6543
|
+
if (runnerUp && best.score - runnerUp.score < FUZZY_AMBIGUITY_MARGIN) {
|
|
6544
|
+
return {
|
|
6545
|
+
tier: "fuzzy",
|
|
6546
|
+
matches: candidates.map((c) => c.match),
|
|
6547
|
+
score: best.score,
|
|
6548
|
+
ambiguous: true
|
|
6549
|
+
};
|
|
6550
|
+
}
|
|
6551
|
+
return { tier: "fuzzy", matches: [best.match], score: best.score };
|
|
6552
|
+
}
|
|
6553
|
+
function similarity(a, b) {
|
|
6554
|
+
if (a === b) return 1;
|
|
6555
|
+
const max = Math.max(a.length, b.length);
|
|
6556
|
+
if (max === 0) return 1;
|
|
6557
|
+
if (Math.abs(a.length - b.length) / max > 1 - FUZZY_MIN_SIMILARITY + 0.05) {
|
|
6558
|
+
return 1 - Math.abs(a.length - b.length) / max;
|
|
6559
|
+
}
|
|
6560
|
+
return 1 - levenshtein(a, b) / max;
|
|
6561
|
+
}
|
|
6562
|
+
function levenshtein(a, b) {
|
|
6563
|
+
if (a.length === 0) return b.length;
|
|
6564
|
+
if (b.length === 0) return a.length;
|
|
6565
|
+
let prev = new Array(b.length + 1);
|
|
6566
|
+
let cur = new Array(b.length + 1);
|
|
6567
|
+
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
6568
|
+
for (let i = 1; i <= a.length; i++) {
|
|
6569
|
+
cur[0] = i;
|
|
6570
|
+
const ca = a.charCodeAt(i - 1);
|
|
6571
|
+
for (let j = 1; j <= b.length; j++) {
|
|
6572
|
+
const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
|
|
6573
|
+
cur[j] = Math.min(
|
|
6574
|
+
prev[j] + 1,
|
|
6575
|
+
cur[j - 1] + 1,
|
|
6576
|
+
prev[j - 1] + cost
|
|
6577
|
+
);
|
|
6578
|
+
}
|
|
6579
|
+
[prev, cur] = [cur, prev];
|
|
6580
|
+
}
|
|
6581
|
+
return prev[b.length];
|
|
6582
|
+
}
|
|
6583
|
+
function firstLineIndent(text) {
|
|
6584
|
+
for (const line of text.split("\n")) {
|
|
6585
|
+
if (line.trim() === "") continue;
|
|
6586
|
+
return /^[ \t]*/.exec(line)[0];
|
|
6587
|
+
}
|
|
6588
|
+
return "";
|
|
6589
|
+
}
|
|
6590
|
+
function adjustIndent(newLf, fromIndent, toIndent) {
|
|
6591
|
+
if (fromIndent === toIndent) return { text: newLf, adjusted: false };
|
|
6592
|
+
const lines = newLf.split("\n");
|
|
6593
|
+
if (toIndent.startsWith(fromIndent)) {
|
|
6594
|
+
const extra = toIndent.slice(fromIndent.length);
|
|
6595
|
+
return {
|
|
6596
|
+
text: lines.map((l) => l.trim() === "" ? l : extra + l).join("\n"),
|
|
6597
|
+
adjusted: true
|
|
6598
|
+
};
|
|
6599
|
+
}
|
|
6600
|
+
if (fromIndent.startsWith(toIndent)) {
|
|
6601
|
+
const remove = fromIndent.slice(toIndent.length);
|
|
6602
|
+
return {
|
|
6603
|
+
text: lines.map((l) => l.startsWith(remove) ? l.slice(remove.length) : l).join("\n"),
|
|
6604
|
+
adjusted: true
|
|
6605
|
+
};
|
|
6606
|
+
}
|
|
6607
|
+
return { text: newLf, adjusted: false };
|
|
6608
|
+
}
|
|
6609
|
+
function nearestMatchHint(fileLf, oldLf) {
|
|
6610
|
+
const fileLines = fileLf.split("\n");
|
|
6611
|
+
const needleLines = oldLf.split("\n");
|
|
6612
|
+
if (needleLines.length > fileLines.length) return void 0;
|
|
6613
|
+
const needleTrimmed = needleLines.map((l) => l.trim());
|
|
6614
|
+
let bestScore = 0;
|
|
6615
|
+
let bestStart = -1;
|
|
6616
|
+
for (let i = 0; i + needleLines.length <= fileLines.length; i++) {
|
|
6617
|
+
let sum = 0;
|
|
6618
|
+
for (let j = 0; j < needleLines.length; j++) {
|
|
6619
|
+
sum += prefixSimilarity(needleTrimmed[j], fileLines[i + j].trim());
|
|
6620
|
+
}
|
|
6621
|
+
const score = sum / needleLines.length;
|
|
6622
|
+
if (score > bestScore) {
|
|
6623
|
+
bestScore = score;
|
|
6624
|
+
bestStart = i;
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
if (bestStart === -1 || bestScore < 0.5) return void 0;
|
|
6628
|
+
const snippet = fileLines.slice(bestStart, bestStart + Math.min(3, needleLines.length)).map((l) => l.length > 120 ? `${l.slice(0, 120)}\u2026` : l).join("\n");
|
|
6629
|
+
return { line: bestStart + 1, snippet };
|
|
6630
|
+
}
|
|
6631
|
+
function prefixSimilarity(a, b) {
|
|
6632
|
+
if (a === b) return 1;
|
|
6633
|
+
const max = Math.max(a.length, b.length);
|
|
6634
|
+
if (max === 0) return 1;
|
|
6635
|
+
let p = 0;
|
|
6636
|
+
const lim = Math.min(a.length, b.length);
|
|
6637
|
+
while (p < lim && a.charCodeAt(p) === b.charCodeAt(p)) p++;
|
|
6638
|
+
return p / max;
|
|
6639
|
+
}
|
|
6640
|
+
var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
6641
|
+
var MAX_CHECK_CHARS = 15e5;
|
|
6642
|
+
var MAX_ERRORS = 5;
|
|
6643
|
+
var tsLoad = null;
|
|
6644
|
+
function loadTypescript() {
|
|
6645
|
+
tsLoad ??= import('typescript').then(
|
|
6646
|
+
(m) => m.default ?? m,
|
|
6647
|
+
() => null
|
|
6648
|
+
);
|
|
6649
|
+
return tsLoad;
|
|
6650
|
+
}
|
|
6651
|
+
async function checkSyntax(filePath, content, previousContent) {
|
|
6652
|
+
const errors = await parseErrors(filePath, content);
|
|
6653
|
+
if (errors === void 0) return void 0;
|
|
6654
|
+
if (errors.length === 0) return { errors, preExisting: false };
|
|
6655
|
+
let preExisting = false;
|
|
6656
|
+
if (previousContent !== void 0) {
|
|
6657
|
+
const prevErrors = await parseErrors(filePath, previousContent);
|
|
6658
|
+
preExisting = prevErrors !== void 0 && prevErrors.length > 0;
|
|
6659
|
+
}
|
|
6660
|
+
return { errors, preExisting };
|
|
6661
|
+
}
|
|
6662
|
+
function isJsoncFile(filePath) {
|
|
6663
|
+
const base = path12.basename(filePath).toLowerCase();
|
|
6664
|
+
if (base.endsWith(".jsonc")) return true;
|
|
6665
|
+
if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
|
|
6666
|
+
const dir = path12.basename(path12.dirname(filePath)).toLowerCase();
|
|
6667
|
+
return dir === ".vscode";
|
|
6668
|
+
}
|
|
6669
|
+
async function parseErrors(filePath, content) {
|
|
6670
|
+
if (content.length > MAX_CHECK_CHARS) return void 0;
|
|
6671
|
+
const ext = path12.extname(filePath).toLowerCase();
|
|
6672
|
+
if (ext === ".json" || ext === ".jsonc") {
|
|
6673
|
+
try {
|
|
6674
|
+
JSON.parse(content);
|
|
6675
|
+
return [];
|
|
6676
|
+
} catch (err) {
|
|
6677
|
+
if (isJsoncFile(filePath)) {
|
|
6678
|
+
const ts3 = await loadTypescript();
|
|
6679
|
+
if (ts3) {
|
|
6680
|
+
const jsonc = ts3.parseConfigFileTextToJson(filePath, content);
|
|
6681
|
+
if (!jsonc.error) return [];
|
|
6682
|
+
return [formatDiag(ts3, jsonc.error, content)];
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
return [`JSON parse error: ${err.message}`];
|
|
6686
|
+
}
|
|
6687
|
+
}
|
|
6688
|
+
if (!TS_LIKE.has(ext)) return void 0;
|
|
6689
|
+
const ts2 = await loadTypescript();
|
|
6690
|
+
if (!ts2) return void 0;
|
|
6691
|
+
const scriptKind = ext === ".tsx" ? ts2.ScriptKind.TSX : ext === ".ts" || ext === ".mts" || ext === ".cts" ? ts2.ScriptKind.TS : (
|
|
6692
|
+
// Plain JS may legitimately contain JSX; the JSX grammar is a
|
|
6693
|
+
// superset for untyped code, so parsing .js/.jsx as JSX avoids
|
|
6694
|
+
// false positives on React files.
|
|
6695
|
+
ts2.ScriptKind.JSX
|
|
6696
|
+
);
|
|
6697
|
+
const sourceFile = ts2.createSourceFile(
|
|
6698
|
+
path12.basename(filePath),
|
|
6699
|
+
content,
|
|
6700
|
+
ts2.ScriptTarget.Latest,
|
|
6701
|
+
/* setParentNodes */
|
|
6702
|
+
false,
|
|
6703
|
+
scriptKind
|
|
6704
|
+
);
|
|
6705
|
+
const diags = sourceFile.parseDiagnostics ?? [];
|
|
6706
|
+
return diags.slice(0, MAX_ERRORS).map((d) => formatDiag(ts2, d, content, sourceFile));
|
|
6707
|
+
}
|
|
6708
|
+
function formatDiag(ts2, diag, content, sourceFile) {
|
|
6709
|
+
const message = ts2.flattenDiagnosticMessageText(diag.messageText, " ");
|
|
6710
|
+
if (diag.start === void 0) return message;
|
|
6711
|
+
let line;
|
|
6712
|
+
if (sourceFile) {
|
|
6713
|
+
line = sourceFile.getLineAndCharacterOfPosition(diag.start).line + 1;
|
|
6714
|
+
} else {
|
|
6715
|
+
line = 1;
|
|
6716
|
+
for (let i = 0; i < diag.start && i < content.length; i++) {
|
|
6717
|
+
if (content.charCodeAt(i) === 10) line++;
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
return `line ${line}: ${message}`;
|
|
6721
|
+
}
|
|
6722
|
+
|
|
6723
|
+
// src/edit.ts
|
|
6393
6724
|
var editTool = {
|
|
6394
6725
|
name: "edit",
|
|
6395
6726
|
category: "Filesystem",
|
|
6396
6727
|
description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. Fails safely if the `old_string` appears more than once unless `replace_all` is set.",
|
|
6397
|
-
usageHint: "RECOMMENDED WORKFLOW:\n1. Prefer calling `read` on the target file first when planning an edit.\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nIf no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.",
|
|
6728
|
+
usageHint: "RECOMMENDED WORKFLOW:\n1. Prefer calling `read` on the target file first when planning an edit.\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nIf no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.\nIf `old_string` differs from the file only in whitespace (trailing spaces, indentation), a lower-confidence fallback match is applied and reported in `matched_by`/`note` \u2014 always verify the diff when this happens.\nAfter the edit, TS/JS/JSON files are syntax-checked; if `syntax_errors` is present in the output, fix those errors immediately with a follow-up edit.",
|
|
6729
|
+
selection: {
|
|
6730
|
+
doNotUseWhen: "creating a new file, replacing the whole file, or applying an existing unified diff.",
|
|
6731
|
+
useInstead: ["write", "patch"]
|
|
6732
|
+
},
|
|
6398
6733
|
permission: "confirm",
|
|
6399
6734
|
mutating: true,
|
|
6400
6735
|
capabilities: ["fs.write"],
|
|
@@ -6410,7 +6745,7 @@ var editTool = {
|
|
|
6410
6745
|
},
|
|
6411
6746
|
required: ["path", "old_string", "new_string"]
|
|
6412
6747
|
},
|
|
6413
|
-
async execute(input, ctx) {
|
|
6748
|
+
async execute(input, ctx, opts) {
|
|
6414
6749
|
if (!input?.path) {
|
|
6415
6750
|
throw new ToolValidationError({ message: "edit: path is required", field: "path" });
|
|
6416
6751
|
}
|
|
@@ -6433,7 +6768,7 @@ var editTool = {
|
|
|
6433
6768
|
});
|
|
6434
6769
|
}
|
|
6435
6770
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
6436
|
-
const
|
|
6771
|
+
const stat13 = await fs2.stat(absPath).catch((err) => {
|
|
6437
6772
|
if (err.code === "ENOENT") {
|
|
6438
6773
|
throw new ToolValidationError({
|
|
6439
6774
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
@@ -6443,7 +6778,7 @@ var editTool = {
|
|
|
6443
6778
|
}
|
|
6444
6779
|
throw err;
|
|
6445
6780
|
});
|
|
6446
|
-
if (!
|
|
6781
|
+
if (!stat13.isFile()) {
|
|
6447
6782
|
throw new ToolValidationError({
|
|
6448
6783
|
message: `edit: "${input.path}" is not a regular file`,
|
|
6449
6784
|
field: "path"
|
|
@@ -6453,15 +6788,27 @@ var editTool = {
|
|
|
6453
6788
|
const original = await fs2.readFile(absPath, "utf8");
|
|
6454
6789
|
const updated = await fs2.stat(absPath);
|
|
6455
6790
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
6456
|
-
const
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6791
|
+
const originalHash = sha256hex(original);
|
|
6792
|
+
const lastReadHash = ctx.lastReadHash?.(absPath);
|
|
6793
|
+
if (lastReadHash !== void 0) {
|
|
6794
|
+
if (lastReadHash !== originalHash) {
|
|
6795
|
+
throw new ToolValidationError({
|
|
6796
|
+
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
6797
|
+
field: "path",
|
|
6798
|
+
context: { reason: "external_modification" }
|
|
6799
|
+
});
|
|
6800
|
+
}
|
|
6801
|
+
} else {
|
|
6802
|
+
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
6803
|
+
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
6804
|
+
throw new ToolValidationError({
|
|
6805
|
+
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
6806
|
+
field: "path",
|
|
6807
|
+
context: { reason: "external_modification" }
|
|
6808
|
+
});
|
|
6809
|
+
}
|
|
6463
6810
|
}
|
|
6464
|
-
if (autoRead && updated.mtimeMs >
|
|
6811
|
+
if (autoRead && updated.mtimeMs > stat13.mtimeMs + mtimeTolerance) {
|
|
6465
6812
|
throw new ToolValidationError({
|
|
6466
6813
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
6467
6814
|
field: "path",
|
|
@@ -6474,42 +6821,78 @@ var editTool = {
|
|
|
6474
6821
|
const oldLf = normalizeToLf(input.old_string);
|
|
6475
6822
|
const newLf = normalizeToLf(input.new_string);
|
|
6476
6823
|
if (oldLf === newLf) {
|
|
6477
|
-
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs);
|
|
6824
|
+
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
|
|
6478
6825
|
return {
|
|
6479
6826
|
path: absPath,
|
|
6480
6827
|
replacements: 0,
|
|
6481
6828
|
diff: "(no-op: old and new are identical)",
|
|
6829
|
+
matched_by: "exact",
|
|
6482
6830
|
note: autoReadNote
|
|
6483
6831
|
};
|
|
6484
6832
|
}
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
while (idx !== -1) {
|
|
6489
|
-
matches.push(idx);
|
|
6490
|
-
count++;
|
|
6491
|
-
idx = fileLf.indexOf(oldLf, idx + 1);
|
|
6492
|
-
}
|
|
6493
|
-
if (count === 0) {
|
|
6494
|
-
const hint = findSimilarity(fileLf, oldLf);
|
|
6833
|
+
const ladder = findLadderMatches(fileLf, oldLf);
|
|
6834
|
+
if (!ladder) {
|
|
6835
|
+
const hint = nearestMatchHint(fileLf, oldLf);
|
|
6495
6836
|
throw new ToolValidationError({
|
|
6496
|
-
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}
|
|
6837
|
+
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
|
|
6838
|
+
${hint.snippet}
|
|
6839
|
+
Compare this against your old_string and retry with the file's actual text.` : ""}`,
|
|
6497
6840
|
field: "old_string"
|
|
6498
6841
|
});
|
|
6499
6842
|
}
|
|
6843
|
+
const { tier, matches } = ladder;
|
|
6844
|
+
const count = matches.length;
|
|
6845
|
+
if (ladder.ambiguous) {
|
|
6846
|
+
const lines = matches.map((m) => m.startLine);
|
|
6847
|
+
throw new ToolValidationError({
|
|
6848
|
+
message: `edit: old_string only matched fuzzily and ${count} candidate blocks scored too close to distinguish (lines: ${lines.join(", ")}) in "${input.path}". Re-read the file and use the exact text of the intended block.`,
|
|
6849
|
+
field: "old_string",
|
|
6850
|
+
context: { occurrences: count, matchTier: tier }
|
|
6851
|
+
});
|
|
6852
|
+
}
|
|
6853
|
+
if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
|
|
6854
|
+
throw new ToolValidationError({
|
|
6855
|
+
message: `edit: old_string only matched via ${TIER_LABEL[tier]} in "${input.path}", but replace_all requires an exact (or trailing-whitespace) match. Re-read the file and use its exact text.`,
|
|
6856
|
+
field: "old_string",
|
|
6857
|
+
context: { matchTier: tier }
|
|
6858
|
+
});
|
|
6859
|
+
}
|
|
6500
6860
|
if (count > 1 && !input.replace_all) {
|
|
6501
|
-
const lines =
|
|
6861
|
+
const lines = matches.map((m) => m.startLine);
|
|
6502
6862
|
throw new ToolValidationError({
|
|
6503
|
-
message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true.`,
|
|
6863
|
+
message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")})${tier === "exact" ? "" : ` via ${TIER_LABEL[tier]}`}. Add more context to make it unique, or set replace_all: true.`,
|
|
6504
6864
|
field: "old_string",
|
|
6505
|
-
context: { occurrences: count }
|
|
6865
|
+
context: { occurrences: count, matchTier: tier }
|
|
6506
6866
|
});
|
|
6507
6867
|
}
|
|
6508
|
-
|
|
6868
|
+
let newFileLf;
|
|
6869
|
+
let tierNote;
|
|
6870
|
+
if (tier === "exact") {
|
|
6871
|
+
newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
6872
|
+
} else {
|
|
6873
|
+
let replacement = newLf;
|
|
6874
|
+
let indentSuffix = "";
|
|
6875
|
+
if (tier === "whitespace-normalized" || tier === "fuzzy") {
|
|
6876
|
+
const first = matches[0];
|
|
6877
|
+
const matchedText = fileLf.slice(first.start, first.end);
|
|
6878
|
+
const adjusted = adjustIndent(newLf, firstLineIndent(oldLf), firstLineIndent(matchedText));
|
|
6879
|
+
replacement = adjusted.text;
|
|
6880
|
+
if (adjusted.adjusted) indentSuffix = "; replacement re-indented to match the file";
|
|
6881
|
+
}
|
|
6882
|
+
newFileLf = fileLf;
|
|
6883
|
+
const applied = input.replace_all ? matches : matches.slice(0, 1);
|
|
6884
|
+
for (let i = applied.length - 1; i >= 0; i--) {
|
|
6885
|
+
const m = applied[i];
|
|
6886
|
+
newFileLf = newFileLf.slice(0, m.start) + replacement + newFileLf.slice(m.end);
|
|
6887
|
+
}
|
|
6888
|
+
const scoreSuffix = ladder.score !== void 0 ? `, similarity ${(ladder.score * 100).toFixed(1)}%` : "";
|
|
6889
|
+
tierNote = `old_string did not match exactly; applied via ${TIER_LABEL[tier]} at line ${matches[0].startLine} (confidence: ${TIER_CONFIDENCE[tier]}${scoreSuffix}${indentSuffix}). Review the diff to confirm the intended target was edited.`;
|
|
6890
|
+
}
|
|
6509
6891
|
const newFile = toStyle(newFileLf, style);
|
|
6892
|
+
opts?.signal?.throwIfAborted();
|
|
6510
6893
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
6511
6894
|
const written = await fs2.stat(absPath);
|
|
6512
|
-
ctx.recordRead(absPath, written.mtimeMs, "write");
|
|
6895
|
+
ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
|
|
6513
6896
|
ctx.session.recordFileChange({
|
|
6514
6897
|
path: absPath,
|
|
6515
6898
|
action: "modified",
|
|
@@ -6520,38 +6903,22 @@ var editTool = {
|
|
|
6520
6903
|
fromFile: input.path,
|
|
6521
6904
|
toFile: input.path
|
|
6522
6905
|
});
|
|
6906
|
+
const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
|
|
6907
|
+
let syntaxNote;
|
|
6908
|
+
if (syntax && syntax.errors.length > 0) {
|
|
6909
|
+
syntaxNote = syntax.preExisting ? `Syntax check: the file still has parse errors (they pre-date this edit) \u2014 see syntax_errors.` : `Syntax check: this edit introduced ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`;
|
|
6910
|
+
}
|
|
6911
|
+
const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
|
|
6523
6912
|
return {
|
|
6524
6913
|
path: absPath,
|
|
6525
6914
|
replacements: input.replace_all ? count : 1,
|
|
6526
6915
|
diff,
|
|
6527
|
-
|
|
6916
|
+
matched_by: tier,
|
|
6917
|
+
syntax_errors: syntax && syntax.errors.length > 0 ? syntax.errors : void 0,
|
|
6918
|
+
note: notes.length > 0 ? notes.join("\n") : void 0
|
|
6528
6919
|
};
|
|
6529
6920
|
}
|
|
6530
6921
|
};
|
|
6531
|
-
function lineNumbersFor(text, indices) {
|
|
6532
|
-
const out = [];
|
|
6533
|
-
let pos = 0;
|
|
6534
|
-
let line = 1;
|
|
6535
|
-
for (const target of indices) {
|
|
6536
|
-
while (pos < target) {
|
|
6537
|
-
if (text.charCodeAt(pos) === 10) line++;
|
|
6538
|
-
pos++;
|
|
6539
|
-
}
|
|
6540
|
-
out.push(line);
|
|
6541
|
-
}
|
|
6542
|
-
return out;
|
|
6543
|
-
}
|
|
6544
|
-
function findSimilarity(haystack, needle) {
|
|
6545
|
-
if (needle.length < 20) return void 0;
|
|
6546
|
-
const probe = needle.slice(0, Math.min(40, needle.length));
|
|
6547
|
-
const idx = haystack.indexOf(probe);
|
|
6548
|
-
if (idx === -1) return void 0;
|
|
6549
|
-
let line = 1;
|
|
6550
|
-
for (let i = 0; i < idx; i++) {
|
|
6551
|
-
if (haystack.charCodeAt(i) === 10) line++;
|
|
6552
|
-
}
|
|
6553
|
-
return line;
|
|
6554
|
-
}
|
|
6555
6922
|
var isWin3 = process.platform === "win32";
|
|
6556
6923
|
var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
6557
6924
|
// JS / TS toolchain
|
|
@@ -7283,6 +7650,10 @@ var execTool = {
|
|
|
7283
7650
|
category: "Shell",
|
|
7284
7651
|
description: "Execute a **whitelisted, restricted set of commands** with strict argument validation. This is the **preferred and safer** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It prevents arbitrary command injection and limits what the model can do.",
|
|
7285
7652
|
usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be in the allowlist. Defaults cover JS (node/npm/pnpm/yarn/bun/deno/tsc/vitest/eslint/biome), Go (`go build`/`go test`), Rust (cargo), Python (python/pip), Ruby (gem/bundle), JVM (java/mvn/gradle), .NET (dotnet), native (make/cmake), and git. Users can extend it via `tools.exec.allow` in config.\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- If a command is not allowlisted, the error explains how to add it; for one-off arbitrary commands, fall back to `bash` (with strong justification).\nThis tool significantly reduces the risk compared to full shell access.",
|
|
7653
|
+
selection: {
|
|
7654
|
+
doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
|
|
7655
|
+
useInstead: ["bash"]
|
|
7656
|
+
},
|
|
7286
7657
|
permission: "confirm",
|
|
7287
7658
|
mutating: true,
|
|
7288
7659
|
riskTier: "standard",
|
|
@@ -7946,13 +8317,13 @@ var formatTool = {
|
|
|
7946
8317
|
}
|
|
7947
8318
|
};
|
|
7948
8319
|
async function detectFixer(cwd) {
|
|
7949
|
-
const { stat:
|
|
8320
|
+
const { stat: stat13 } = await import('node:fs/promises');
|
|
7950
8321
|
try {
|
|
7951
|
-
await
|
|
8322
|
+
await stat13(`${cwd}/biome.json`);
|
|
7952
8323
|
return "biome";
|
|
7953
8324
|
} catch {
|
|
7954
8325
|
try {
|
|
7955
|
-
await
|
|
8326
|
+
await stat13(`${cwd}/.prettierrc`);
|
|
7956
8327
|
return "prettier";
|
|
7957
8328
|
} catch {
|
|
7958
8329
|
return "biome";
|
|
@@ -8113,8 +8484,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
8113
8484
|
let dir = cwd;
|
|
8114
8485
|
for (let i = 0; i < 20; i++) {
|
|
8115
8486
|
try {
|
|
8116
|
-
const
|
|
8117
|
-
if (
|
|
8487
|
+
const stat13 = statSync(`${dir}/.git`);
|
|
8488
|
+
if (stat13.isDirectory() || stat13.isFile()) return dir;
|
|
8118
8489
|
} catch {
|
|
8119
8490
|
}
|
|
8120
8491
|
if (dir === root) break;
|
|
@@ -8261,6 +8632,10 @@ var globTool = {
|
|
|
8261
8632
|
category: "Filesystem",
|
|
8262
8633
|
description: "Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.",
|
|
8263
8634
|
usageHint: "RECOMMENDED FOR SCOPING SEARCHES:\n\n- Use early to get a list of files you actually care about.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
|
|
8635
|
+
selection: {
|
|
8636
|
+
doNotUseWhen: "you need to search inside file contents.",
|
|
8637
|
+
useInstead: ["grep"]
|
|
8638
|
+
},
|
|
8264
8639
|
permission: "auto",
|
|
8265
8640
|
mutating: false,
|
|
8266
8641
|
capabilities: ["fs.read"],
|
|
@@ -8285,8 +8660,9 @@ var globTool = {
|
|
|
8285
8660
|
},
|
|
8286
8661
|
required: ["pattern"]
|
|
8287
8662
|
},
|
|
8288
|
-
async execute(input, ctx) {
|
|
8663
|
+
async execute(input, ctx, opts) {
|
|
8289
8664
|
if (!input?.pattern) throw new Error("glob: pattern is required");
|
|
8665
|
+
const signal = opts?.signal;
|
|
8290
8666
|
const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
|
|
8291
8667
|
const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
|
|
8292
8668
|
const ignored = await readGitignore(base);
|
|
@@ -8310,6 +8686,10 @@ var globTool = {
|
|
|
8310
8686
|
}
|
|
8311
8687
|
};
|
|
8312
8688
|
const walk2 = async (dir, relPrefix) => {
|
|
8689
|
+
if (signal?.aborted) {
|
|
8690
|
+
truncated = true;
|
|
8691
|
+
return;
|
|
8692
|
+
}
|
|
8313
8693
|
if (results.length >= limit) {
|
|
8314
8694
|
truncated = true;
|
|
8315
8695
|
return;
|
|
@@ -8327,7 +8707,7 @@ var globTool = {
|
|
|
8327
8707
|
if (DEFAULT_IGNORE2.includes(name)) continue;
|
|
8328
8708
|
if (ignored.includes(name)) continue;
|
|
8329
8709
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
8330
|
-
const full =
|
|
8710
|
+
const full = path12.join(dir, name);
|
|
8331
8711
|
if (e.isDirectory()) {
|
|
8332
8712
|
subdirs.push({ full, rel });
|
|
8333
8713
|
} else if (e.isFile()) {
|
|
@@ -8371,7 +8751,7 @@ var globTool = {
|
|
|
8371
8751
|
};
|
|
8372
8752
|
async function readGitignore(dir) {
|
|
8373
8753
|
try {
|
|
8374
|
-
const raw = await fs2.readFile(
|
|
8754
|
+
const raw = await fs2.readFile(path12.join(dir, ".gitignore"), "utf8");
|
|
8375
8755
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
8376
8756
|
} catch {
|
|
8377
8757
|
return [];
|
|
@@ -8433,6 +8813,10 @@ var grepTool = {
|
|
|
8433
8813
|
category: "Search",
|
|
8434
8814
|
description: "Search across files using a regular expression. This is one of the primary code search tools. Prefers ripgrep for speed and features when available.",
|
|
8435
8815
|
usageHint: 'POWERFUL CODE SEARCH TOOL:\n\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
|
|
8816
|
+
selection: {
|
|
8817
|
+
doNotUseWhen: "you only need to locate files by name or path pattern.",
|
|
8818
|
+
useInstead: ["glob"]
|
|
8819
|
+
},
|
|
8436
8820
|
permission: "auto",
|
|
8437
8821
|
mutating: false,
|
|
8438
8822
|
capabilities: ["fs.read"],
|
|
@@ -8672,8 +9056,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
8672
9056
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
8673
9057
|
if (globRe) globRe.lastIndex = 0;
|
|
8674
9058
|
try {
|
|
8675
|
-
const
|
|
8676
|
-
if (!
|
|
9059
|
+
const stat13 = await fs2.stat(full);
|
|
9060
|
+
if (!stat13.isFile() || stat13.size > maxBytes || stopped || signal.aborted) return;
|
|
8677
9061
|
const file = await fs2.open(full, "r");
|
|
8678
9062
|
try {
|
|
8679
9063
|
let bytesReadTotal = 0;
|
|
@@ -8763,7 +9147,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
8763
9147
|
if (stopped) return;
|
|
8764
9148
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
8765
9149
|
if (e.isSymbolicLink()) continue;
|
|
8766
|
-
const full =
|
|
9150
|
+
const full = path12.join(dir, e.name);
|
|
8767
9151
|
if (e.isDirectory()) {
|
|
8768
9152
|
subdirs.push(full);
|
|
8769
9153
|
} else if (e.isFile()) {
|
|
@@ -9310,56 +9694,56 @@ function jmespathSearch(data, query) {
|
|
|
9310
9694
|
}
|
|
9311
9695
|
function validateJsonSchema(data, schema) {
|
|
9312
9696
|
const errors = [];
|
|
9313
|
-
function check(value, s,
|
|
9697
|
+
function check(value, s, path24) {
|
|
9314
9698
|
if (s["type"]) {
|
|
9315
9699
|
const expectedType = s["type"];
|
|
9316
9700
|
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
9317
9701
|
if (expectedType === "integer") {
|
|
9318
|
-
if (!Number.isInteger(value)) errors.push(`${
|
|
9702
|
+
if (!Number.isInteger(value)) errors.push(`${path24}: expected integer, got ${actualType}`);
|
|
9319
9703
|
} else if (expectedType !== actualType) {
|
|
9320
|
-
errors.push(`${
|
|
9704
|
+
errors.push(`${path24}: expected ${expectedType}, got ${actualType}`);
|
|
9321
9705
|
}
|
|
9322
9706
|
}
|
|
9323
9707
|
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
9324
9708
|
try {
|
|
9325
9709
|
new URL(value);
|
|
9326
9710
|
} catch {
|
|
9327
|
-
errors.push(`${
|
|
9711
|
+
errors.push(`${path24}: not a valid URI`);
|
|
9328
9712
|
}
|
|
9329
9713
|
}
|
|
9330
9714
|
if (typeof value === "string" && s["pattern"]) {
|
|
9331
9715
|
const re = new RegExp(s["pattern"]);
|
|
9332
|
-
if (!re.test(value)) errors.push(`${
|
|
9716
|
+
if (!re.test(value)) errors.push(`${path24}: does not match pattern ${s["pattern"]}`);
|
|
9333
9717
|
}
|
|
9334
9718
|
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
9335
|
-
errors.push(`${
|
|
9719
|
+
errors.push(`${path24}: string too short (min ${s["minLength"]})`);
|
|
9336
9720
|
}
|
|
9337
9721
|
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
9338
|
-
errors.push(`${
|
|
9722
|
+
errors.push(`${path24}: string too long (max ${s["maxLength"]})`);
|
|
9339
9723
|
}
|
|
9340
9724
|
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
9341
|
-
errors.push(`${
|
|
9725
|
+
errors.push(`${path24}: below minimum ${s["minimum"]}`);
|
|
9342
9726
|
}
|
|
9343
9727
|
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
9344
|
-
errors.push(`${
|
|
9728
|
+
errors.push(`${path24}: above maximum ${s["maximum"]}`);
|
|
9345
9729
|
}
|
|
9346
9730
|
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
9347
9731
|
for (let i = 0; i < value.length; i++) {
|
|
9348
|
-
check(value[i], s["items"], `${
|
|
9732
|
+
check(value[i], s["items"], `${path24}[${i}]`);
|
|
9349
9733
|
}
|
|
9350
9734
|
}
|
|
9351
9735
|
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
9352
9736
|
const props = s["properties"];
|
|
9353
9737
|
for (const [k, propSchema] of Object.entries(props)) {
|
|
9354
|
-
check(value[k], propSchema, `${
|
|
9738
|
+
check(value[k], propSchema, `${path24}.${k}`);
|
|
9355
9739
|
}
|
|
9356
9740
|
}
|
|
9357
9741
|
}
|
|
9358
9742
|
check(data, schema, "$");
|
|
9359
9743
|
return { valid: errors.length === 0, errors };
|
|
9360
9744
|
}
|
|
9361
|
-
function simpleQuery(data,
|
|
9362
|
-
const parts =
|
|
9745
|
+
function simpleQuery(data, path24) {
|
|
9746
|
+
const parts = path24.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
9363
9747
|
let current = data;
|
|
9364
9748
|
for (const part of parts) {
|
|
9365
9749
|
if (current === null || current === void 0) return void 0;
|
|
@@ -10284,11 +10668,11 @@ var lintTool = {
|
|
|
10284
10668
|
}
|
|
10285
10669
|
};
|
|
10286
10670
|
async function detectLinter(cwd) {
|
|
10287
|
-
const { stat:
|
|
10671
|
+
const { stat: stat13 } = await import('node:fs/promises');
|
|
10288
10672
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
10289
10673
|
for (const f of checks) {
|
|
10290
10674
|
try {
|
|
10291
|
-
await
|
|
10675
|
+
await stat13(`${cwd}/${f}`);
|
|
10292
10676
|
if (f.includes("biome")) return "biome";
|
|
10293
10677
|
if (f.includes("eslint")) return "eslint";
|
|
10294
10678
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -10430,7 +10814,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
10430
10814
|
}
|
|
10431
10815
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
10432
10816
|
var MAX_TAIL_LINES = 1e5;
|
|
10433
|
-
async function fileLogs(
|
|
10817
|
+
async function fileLogs(path24, lines, filterRe, stream) {
|
|
10434
10818
|
const { createInterface } = await import('node:readline');
|
|
10435
10819
|
const { createReadStream } = await import('node:fs');
|
|
10436
10820
|
const entries = [];
|
|
@@ -10439,7 +10823,7 @@ async function fileLogs(path23, lines, filterRe, stream) {
|
|
|
10439
10823
|
let writeIdx = 0;
|
|
10440
10824
|
let totalLines = 0;
|
|
10441
10825
|
const rl = createInterface({
|
|
10442
|
-
input: createReadStream(
|
|
10826
|
+
input: createReadStream(path24),
|
|
10443
10827
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
10444
10828
|
});
|
|
10445
10829
|
for await (const line of rl) {
|
|
@@ -10460,7 +10844,7 @@ async function fileLogs(path23, lines, filterRe, stream) {
|
|
|
10460
10844
|
if (parsed) entries.push(parsed);
|
|
10461
10845
|
}
|
|
10462
10846
|
return {
|
|
10463
|
-
source:
|
|
10847
|
+
source: path24,
|
|
10464
10848
|
entries,
|
|
10465
10849
|
total: entries.length,
|
|
10466
10850
|
truncated: totalLines > effLines,
|
|
@@ -10627,6 +11011,10 @@ var patchTool = {
|
|
|
10627
11011
|
category: "Filesystem",
|
|
10628
11012
|
description: "Apply a unified diff (patch) to the project. This is the correct tool when you have a diff that needs to be applied precisely, including handling of rejects.",
|
|
10629
11013
|
usageHint: "Best used when you already have a diff (from generation, external source, or previous step).\n- Use `dry_run: true` to see what would happen without modifying files.\n- On failure it creates .rej and .orig files for manual review.\nOften cleaner than many small `edit` operations for larger changes.",
|
|
11014
|
+
selection: {
|
|
11015
|
+
doNotUseWhen: "you do not already have a unified diff or only need one precise replacement.",
|
|
11016
|
+
useInstead: ["edit"]
|
|
11017
|
+
},
|
|
10630
11018
|
permission: "confirm",
|
|
10631
11019
|
mutating: true,
|
|
10632
11020
|
capabilities: ["fs.write"],
|
|
@@ -10648,12 +11036,13 @@ var patchTool = {
|
|
|
10648
11036
|
const strip = Math.max(1, input.strip ?? 1);
|
|
10649
11037
|
const dryRun = input.dry_run ?? false;
|
|
10650
11038
|
const targets = extractDiffTargets(input.patch);
|
|
11039
|
+
const resolvedTargets = [];
|
|
10651
11040
|
for (const t of targets) {
|
|
10652
11041
|
const stripped = stripPathComponents(t, strip);
|
|
10653
11042
|
if (!stripped) continue;
|
|
10654
|
-
const candidate =
|
|
10655
|
-
const rel =
|
|
10656
|
-
if (rel.startsWith("..") ||
|
|
11043
|
+
const candidate = path12.resolve(dir, stripped);
|
|
11044
|
+
const rel = path12.relative(ctx.projectRoot, candidate);
|
|
11045
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) {
|
|
10657
11046
|
return {
|
|
10658
11047
|
applied: 0,
|
|
10659
11048
|
rejected: 1,
|
|
@@ -10662,12 +11051,19 @@ var patchTool = {
|
|
|
10662
11051
|
message: `patch refused: target "${t}" resolves outside project root`
|
|
10663
11052
|
};
|
|
10664
11053
|
}
|
|
11054
|
+
resolvedTargets.push(candidate);
|
|
10665
11055
|
}
|
|
10666
|
-
const
|
|
11056
|
+
const beforeContents = /* @__PURE__ */ new Map();
|
|
11057
|
+
if (!dryRun) {
|
|
11058
|
+
for (const target of resolvedTargets) {
|
|
11059
|
+
beforeContents.set(target, await readTextForTracking(target));
|
|
11060
|
+
}
|
|
11061
|
+
}
|
|
11062
|
+
const tmpDir = await fs2.mkdtemp(path12.join(os2.tmpdir(), ".wstack_patch_"));
|
|
10667
11063
|
try {
|
|
10668
11064
|
await fs2.chmod(tmpDir, 448).catch(() => {
|
|
10669
11065
|
});
|
|
10670
|
-
const patchFile =
|
|
11066
|
+
const patchFile = path12.join(tmpDir, "in.diff");
|
|
10671
11067
|
await fs2.writeFile(patchFile, input.patch, { mode: 384 });
|
|
10672
11068
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
10673
11069
|
const result = await runPatch(args, dir, opts.signal);
|
|
@@ -10681,6 +11077,21 @@ var patchTool = {
|
|
|
10681
11077
|
};
|
|
10682
11078
|
}
|
|
10683
11079
|
const patched = extractPatchedFiles(result.stdout);
|
|
11080
|
+
if (!dryRun) {
|
|
11081
|
+
for (const target of resolvedTargets) {
|
|
11082
|
+
const before = beforeContents.get(target) ?? null;
|
|
11083
|
+
const after = await readTextForTracking(target);
|
|
11084
|
+
if (after === null || after === before) continue;
|
|
11085
|
+
const stat13 = await fs2.stat(target).catch(() => null);
|
|
11086
|
+
if (stat13) ctx.recordRead?.(target, stat13.mtimeMs, "write", sha256hex(after));
|
|
11087
|
+
ctx.session?.recordFileChange?.({
|
|
11088
|
+
path: target,
|
|
11089
|
+
action: before === null ? "created" : "modified",
|
|
11090
|
+
before,
|
|
11091
|
+
after
|
|
11092
|
+
});
|
|
11093
|
+
}
|
|
11094
|
+
}
|
|
10684
11095
|
return {
|
|
10685
11096
|
applied: patched.length,
|
|
10686
11097
|
rejected: 0,
|
|
@@ -10694,6 +11105,18 @@ var patchTool = {
|
|
|
10694
11105
|
}
|
|
10695
11106
|
}
|
|
10696
11107
|
};
|
|
11108
|
+
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
11109
|
+
async function readTextForTracking(absPath) {
|
|
11110
|
+
try {
|
|
11111
|
+
const stat13 = await fs2.stat(absPath);
|
|
11112
|
+
if (!stat13.isFile() || stat13.size > MAX_TRACKING_BYTES) return null;
|
|
11113
|
+
const buf = await fs2.readFile(absPath);
|
|
11114
|
+
if (buf.includes(0)) return null;
|
|
11115
|
+
return buf.toString("utf8");
|
|
11116
|
+
} catch {
|
|
11117
|
+
return null;
|
|
11118
|
+
}
|
|
11119
|
+
}
|
|
10697
11120
|
function extractDiffTargets(patch) {
|
|
10698
11121
|
const out = [];
|
|
10699
11122
|
const re = /^\+\+\+\s+([^\t\r\n]+)/gm;
|
|
@@ -11001,6 +11424,10 @@ var readTool = {
|
|
|
11001
11424
|
category: "Filesystem",
|
|
11002
11425
|
description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits.",
|
|
11003
11426
|
usageHint: "FOUNDATIONAL TOOL \u2014 call this before almost any edit operation.\n\nBest practices:\n- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\n- Use `offset` + `limit` for very large files instead of reading everything at once.\n- Default limit is generous (2000 lines) but can be increased.\n- The output format is designed to be directly usable as context for `edit` operations.",
|
|
11427
|
+
selection: {
|
|
11428
|
+
doNotUseWhen: "you need to search many files for matching content.",
|
|
11429
|
+
useInstead: ["grep"]
|
|
11430
|
+
},
|
|
11004
11431
|
permission: "auto",
|
|
11005
11432
|
mutating: false,
|
|
11006
11433
|
capabilities: ["fs.read"],
|
|
@@ -11038,9 +11465,9 @@ var readTool = {
|
|
|
11038
11465
|
});
|
|
11039
11466
|
}
|
|
11040
11467
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
11041
|
-
let
|
|
11468
|
+
let stat13;
|
|
11042
11469
|
try {
|
|
11043
|
-
|
|
11470
|
+
stat13 = await fs2.stat(absPath);
|
|
11044
11471
|
} catch (err) {
|
|
11045
11472
|
const code = err.code;
|
|
11046
11473
|
if (code === "ENOENT") {
|
|
@@ -11059,7 +11486,7 @@ var readTool = {
|
|
|
11059
11486
|
cause: err
|
|
11060
11487
|
});
|
|
11061
11488
|
}
|
|
11062
|
-
if (!
|
|
11489
|
+
if (!stat13.isFile()) {
|
|
11063
11490
|
throw new FsError({
|
|
11064
11491
|
message: `read: "${input.path}" is not a regular file`,
|
|
11065
11492
|
code: "FS_READ_FAILED",
|
|
@@ -11067,22 +11494,22 @@ var readTool = {
|
|
|
11067
11494
|
context: { reason: "not-a-regular-file" }
|
|
11068
11495
|
});
|
|
11069
11496
|
}
|
|
11070
|
-
if (
|
|
11497
|
+
if (stat13.size > MAX_BYTES2) {
|
|
11071
11498
|
throw new FsError({
|
|
11072
|
-
message: `read: file too large (${
|
|
11499
|
+
message: `read: file too large (${stat13.size} bytes, limit ${MAX_BYTES2})`,
|
|
11073
11500
|
code: "FS_READ_FAILED",
|
|
11074
11501
|
path: absPath,
|
|
11075
|
-
context: { size:
|
|
11502
|
+
context: { size: stat13.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
11076
11503
|
});
|
|
11077
11504
|
}
|
|
11078
11505
|
const offset = Math.max(1, input.offset ?? 1);
|
|
11079
11506
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
11080
11507
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
11081
11508
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
11082
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
11083
|
-
ctx.recordRead(absPath,
|
|
11509
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat13.mtimeMs, offset, requestedEnd)) {
|
|
11510
|
+
ctx.recordRead(absPath, stat13.mtimeMs);
|
|
11084
11511
|
return {
|
|
11085
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
11512
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat13.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
11086
11513
|
total_lines: prior.totalLines,
|
|
11087
11514
|
encoding: "utf8",
|
|
11088
11515
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -11095,13 +11522,14 @@ var readTool = {
|
|
|
11095
11522
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
11096
11523
|
}
|
|
11097
11524
|
const text = buf.toString("utf8");
|
|
11525
|
+
const contentHash = sha256hex(text);
|
|
11098
11526
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
11099
11527
|
const total = allLines.length;
|
|
11100
11528
|
if (input.mode === "summary") {
|
|
11101
|
-
ctx.recordRead(absPath,
|
|
11102
|
-
rememberReadRange(ctx, absPath,
|
|
11529
|
+
ctx.recordRead(absPath, stat13.mtimeMs, "user", contentHash);
|
|
11530
|
+
rememberReadRange(ctx, absPath, stat13.mtimeMs, total, 1, Math.min(total, 200));
|
|
11103
11531
|
return {
|
|
11104
|
-
text: summarizeFile(input.path,
|
|
11532
|
+
text: summarizeFile(input.path, stat13.size, allLines),
|
|
11105
11533
|
total_lines: total,
|
|
11106
11534
|
encoding: "utf8",
|
|
11107
11535
|
truncated: total > 200,
|
|
@@ -11109,13 +11537,13 @@ var readTool = {
|
|
|
11109
11537
|
};
|
|
11110
11538
|
}
|
|
11111
11539
|
if (limit === 0) {
|
|
11112
|
-
ctx.recordRead(absPath,
|
|
11113
|
-
rememberReadRange(ctx, absPath,
|
|
11540
|
+
ctx.recordRead(absPath, stat13.mtimeMs, "user", contentHash);
|
|
11541
|
+
rememberReadRange(ctx, absPath, stat13.mtimeMs, total, 1, 0);
|
|
11114
11542
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
11115
11543
|
}
|
|
11116
11544
|
if (offset > total) {
|
|
11117
|
-
ctx.recordRead(absPath,
|
|
11118
|
-
rememberReadRange(ctx, absPath,
|
|
11545
|
+
ctx.recordRead(absPath, stat13.mtimeMs, "user", contentHash);
|
|
11546
|
+
rememberReadRange(ctx, absPath, stat13.mtimeMs, total, total + 1, total + 1);
|
|
11119
11547
|
return {
|
|
11120
11548
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
11121
11549
|
total_lines: total,
|
|
@@ -11127,8 +11555,8 @@ var readTool = {
|
|
|
11127
11555
|
const truncated = offset - 1 + slice.length < total;
|
|
11128
11556
|
const width = String(offset + slice.length - 1).length;
|
|
11129
11557
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
11130
|
-
ctx.recordRead(absPath,
|
|
11131
|
-
rememberReadRange(ctx, absPath,
|
|
11558
|
+
ctx.recordRead(absPath, stat13.mtimeMs, "user", contentHash);
|
|
11559
|
+
rememberReadRange(ctx, absPath, stat13.mtimeMs, total, offset, offset + slice.length - 1);
|
|
11132
11560
|
return {
|
|
11133
11561
|
text: numbered,
|
|
11134
11562
|
total_lines: total,
|
|
@@ -11270,10 +11698,10 @@ var replaceTool = {
|
|
|
11270
11698
|
} catch {
|
|
11271
11699
|
continue;
|
|
11272
11700
|
}
|
|
11273
|
-
const rel =
|
|
11274
|
-
if (rel.startsWith("..") ||
|
|
11275
|
-
const
|
|
11276
|
-
if (!
|
|
11701
|
+
const rel = path12.relative(realRoot, realPath);
|
|
11702
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) continue;
|
|
11703
|
+
const stat13 = await fs2.stat(realPath).catch(() => null);
|
|
11704
|
+
if (!stat13?.isFile()) continue;
|
|
11277
11705
|
let content;
|
|
11278
11706
|
try {
|
|
11279
11707
|
const buf = await fs2.readFile(realPath);
|
|
@@ -11298,7 +11726,17 @@ var replaceTool = {
|
|
|
11298
11726
|
totalReplacements += count;
|
|
11299
11727
|
if (!dryRun) {
|
|
11300
11728
|
const newContent = toStyle(newContentLf, style);
|
|
11301
|
-
await atomicWrite(realPath, newContent, { mode:
|
|
11729
|
+
await atomicWrite(realPath, newContent, { mode: stat13.mode & 511 });
|
|
11730
|
+
const written = await fs2.stat(realPath).catch(() => null);
|
|
11731
|
+
if (written) {
|
|
11732
|
+
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
11733
|
+
}
|
|
11734
|
+
ctx.session?.recordFileChange?.({
|
|
11735
|
+
path: realPath,
|
|
11736
|
+
action: "modified",
|
|
11737
|
+
before: content,
|
|
11738
|
+
after: newContent
|
|
11739
|
+
});
|
|
11302
11740
|
}
|
|
11303
11741
|
const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
|
|
11304
11742
|
fromFile: absPath,
|
|
@@ -11328,8 +11766,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
11328
11766
|
const resolved = [];
|
|
11329
11767
|
for (const p of parts) {
|
|
11330
11768
|
const absPath = safeResolve(p, ctx);
|
|
11331
|
-
const
|
|
11332
|
-
if (
|
|
11769
|
+
const stat13 = await fs2.stat(absPath).catch(() => null);
|
|
11770
|
+
if (stat13?.isFile()) {
|
|
11333
11771
|
resolved.push(absPath);
|
|
11334
11772
|
}
|
|
11335
11773
|
}
|
|
@@ -11390,10 +11828,10 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
11390
11828
|
}
|
|
11391
11829
|
for (const e of entries) {
|
|
11392
11830
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
11393
|
-
const full =
|
|
11831
|
+
const full = path12.join(dir, e.name);
|
|
11394
11832
|
try {
|
|
11395
|
-
const
|
|
11396
|
-
if (
|
|
11833
|
+
const stat13 = await fs2.lstat(full);
|
|
11834
|
+
if (stat13.isSymbolicLink()) continue;
|
|
11397
11835
|
} catch {
|
|
11398
11836
|
continue;
|
|
11399
11837
|
}
|
|
@@ -11561,16 +11999,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
11561
11999
|
let filesCreated = 0;
|
|
11562
12000
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
11563
12001
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
11564
|
-
const joinedPath =
|
|
11565
|
-
const root =
|
|
11566
|
-
const target =
|
|
11567
|
-
const rel =
|
|
11568
|
-
if (rel.startsWith("..") ||
|
|
12002
|
+
const joinedPath = path12.join(cwd, resolvedPath);
|
|
12003
|
+
const root = path12.resolve(ctx.projectRoot);
|
|
12004
|
+
const target = path12.resolve(joinedPath);
|
|
12005
|
+
const rel = path12.relative(root, target);
|
|
12006
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) {
|
|
11569
12007
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
11570
12008
|
}
|
|
11571
12009
|
const fullPath = target;
|
|
11572
12010
|
if (!dryRun) {
|
|
11573
|
-
await fs2.mkdir(
|
|
12011
|
+
await fs2.mkdir(path12.dirname(fullPath), { recursive: true });
|
|
11574
12012
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
11575
12013
|
}
|
|
11576
12014
|
files.push(resolvedPath);
|
|
@@ -12470,11 +12908,11 @@ var testTool = {
|
|
|
12470
12908
|
}
|
|
12471
12909
|
};
|
|
12472
12910
|
async function detectRunner(cwd) {
|
|
12473
|
-
const { stat:
|
|
12911
|
+
const { stat: stat13 } = await import('node:fs/promises');
|
|
12474
12912
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
12475
12913
|
for (const f of candidates) {
|
|
12476
12914
|
try {
|
|
12477
|
-
await
|
|
12915
|
+
await stat13(path12.join(cwd, f));
|
|
12478
12916
|
if (f.includes("vitest")) return "vitest";
|
|
12479
12917
|
if (f.includes("jest")) return "jest";
|
|
12480
12918
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -13102,7 +13540,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
13102
13540
|
opts.lines.push(opts.prefix + branch + displayName);
|
|
13103
13541
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
13104
13542
|
const childPrefix = opts.prefix + connector;
|
|
13105
|
-
await walkDir(
|
|
13543
|
+
await walkDir(path12.join(dir, entry.name), depth + 1, {
|
|
13106
13544
|
...opts,
|
|
13107
13545
|
prefix: childPrefix,
|
|
13108
13546
|
isLast
|
|
@@ -13188,12 +13626,12 @@ var typecheckTool = {
|
|
|
13188
13626
|
}
|
|
13189
13627
|
};
|
|
13190
13628
|
async function findTsConfig(cwd) {
|
|
13191
|
-
const { stat:
|
|
13629
|
+
const { stat: stat13 } = await import('node:fs/promises');
|
|
13192
13630
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
13193
13631
|
for (const f of candidates) {
|
|
13194
13632
|
try {
|
|
13195
|
-
const s = await
|
|
13196
|
-
if (s.isFile()) return
|
|
13633
|
+
const s = await stat13(path12.join(cwd, f));
|
|
13634
|
+
if (s.isFile()) return path12.join(cwd, f);
|
|
13197
13635
|
} catch {
|
|
13198
13636
|
}
|
|
13199
13637
|
}
|
|
@@ -13204,6 +13642,10 @@ var writeTool = {
|
|
|
13204
13642
|
category: "Filesystem",
|
|
13205
13643
|
description: "Write or completely overwrite a file on disk. This is a high-privilege operation. For modifying existing files, you should almost always prefer the `edit` tool instead, because `edit` is safer and works on the last-read version of the file.",
|
|
13206
13644
|
usageHint: "RULES FOR CORRECT USAGE:\n- Use `write` primarily for **new files** or when you want to replace the entire content.\n- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n- You MUST have called `read` on the file earlier in the conversation before using `write` on an existing path (the system enforces this for safety).\n- The path is resolved relative to the project root and protected against escaping the workspace.",
|
|
13645
|
+
selection: {
|
|
13646
|
+
doNotUseWhen: "making a precise change to part of an existing file.",
|
|
13647
|
+
useInstead: ["edit"]
|
|
13648
|
+
},
|
|
13207
13649
|
permission: "confirm",
|
|
13208
13650
|
mutating: true,
|
|
13209
13651
|
timeoutMs: 5e3,
|
|
@@ -13223,10 +13665,10 @@ var writeTool = {
|
|
|
13223
13665
|
},
|
|
13224
13666
|
required: ["path", "content"]
|
|
13225
13667
|
},
|
|
13226
|
-
async execute(input, ctx) {
|
|
13227
|
-
return writeFile7(input, ctx);
|
|
13668
|
+
async execute(input, ctx, opts) {
|
|
13669
|
+
return writeFile7(input, ctx, opts?.signal);
|
|
13228
13670
|
},
|
|
13229
|
-
async *executeStream(input, ctx) {
|
|
13671
|
+
async *executeStream(input, ctx, opts) {
|
|
13230
13672
|
const prepared = await prepareWrite(input, ctx);
|
|
13231
13673
|
if (!prepared.existed) {
|
|
13232
13674
|
for (const line of input.content.split("\n")) {
|
|
@@ -13234,11 +13676,11 @@ var writeTool = {
|
|
|
13234
13676
|
`, data: { livePreview: true } };
|
|
13235
13677
|
}
|
|
13236
13678
|
}
|
|
13237
|
-
yield { type: "final", output: await finishWrite(input, ctx, prepared) };
|
|
13679
|
+
yield { type: "final", output: await finishWrite(input, ctx, prepared, opts?.signal) };
|
|
13238
13680
|
}
|
|
13239
13681
|
};
|
|
13240
|
-
async function writeFile7(input, ctx) {
|
|
13241
|
-
return finishWrite(input, ctx, await prepareWrite(input, ctx));
|
|
13682
|
+
async function writeFile7(input, ctx, signal) {
|
|
13683
|
+
return finishWrite(input, ctx, await prepareWrite(input, ctx), signal);
|
|
13242
13684
|
}
|
|
13243
13685
|
async function prepareWrite(input, ctx) {
|
|
13244
13686
|
if (!input?.path) {
|
|
@@ -13257,12 +13699,12 @@ async function prepareWrite(input, ctx) {
|
|
|
13257
13699
|
let existed = false;
|
|
13258
13700
|
let prev = "";
|
|
13259
13701
|
try {
|
|
13260
|
-
const
|
|
13261
|
-
existed =
|
|
13702
|
+
const stat13 = await fs2.stat(absPath);
|
|
13703
|
+
existed = stat13.isFile();
|
|
13262
13704
|
if (existed) {
|
|
13263
13705
|
if (!ctx.hasRead(absPath)) {
|
|
13264
13706
|
prev = await fs2.readFile(absPath, "utf8");
|
|
13265
|
-
ctx.recordRead(absPath,
|
|
13707
|
+
ctx.recordRead(absPath, stat13.mtimeMs, "write", sha256hex(prev));
|
|
13266
13708
|
} else {
|
|
13267
13709
|
prev = await fs2.readFile(absPath, "utf8");
|
|
13268
13710
|
}
|
|
@@ -13274,23 +13716,32 @@ async function prepareWrite(input, ctx) {
|
|
|
13274
13716
|
}
|
|
13275
13717
|
return { absPath, existed, prev };
|
|
13276
13718
|
}
|
|
13277
|
-
async function finishWrite(input, ctx, prepared) {
|
|
13719
|
+
async function finishWrite(input, ctx, prepared, signal) {
|
|
13720
|
+
signal?.throwIfAborted();
|
|
13278
13721
|
await atomicWrite(prepared.absPath, input.content);
|
|
13279
13722
|
const diff = prepared.existed ? unifiedDiff(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
13280
13723
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
13281
|
-
const
|
|
13282
|
-
ctx.recordRead(prepared.absPath,
|
|
13724
|
+
const stat13 = await fs2.stat(prepared.absPath);
|
|
13725
|
+
ctx.recordRead(prepared.absPath, stat13.mtimeMs, "write", sha256hex(input.content));
|
|
13283
13726
|
ctx.session.recordFileChange({
|
|
13284
13727
|
path: prepared.absPath,
|
|
13285
13728
|
action: prepared.existed ? "modified" : "created",
|
|
13286
13729
|
before: prepared.existed ? prepared.prev : null,
|
|
13287
13730
|
after: input.content
|
|
13288
13731
|
});
|
|
13732
|
+
const syntax = await checkSyntax(
|
|
13733
|
+
prepared.absPath,
|
|
13734
|
+
input.content,
|
|
13735
|
+
prepared.existed ? prepared.prev : void 0
|
|
13736
|
+
).catch(() => void 0);
|
|
13737
|
+
const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
|
|
13289
13738
|
return {
|
|
13290
13739
|
path: prepared.absPath,
|
|
13291
13740
|
bytes_written: Buffer.byteLength(input.content, "utf8"),
|
|
13292
13741
|
created: !prepared.existed,
|
|
13293
|
-
diff
|
|
13742
|
+
diff,
|
|
13743
|
+
syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
|
|
13744
|
+
note: hasSyntaxErrors ? syntax.preExisting ? "Syntax check: the file still has parse errors (they pre-date this write) \u2014 see syntax_errors." : `Syntax check: the written content has ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.` : void 0
|
|
13294
13745
|
};
|
|
13295
13746
|
}
|
|
13296
13747
|
|
|
@@ -13699,6 +14150,7 @@ var ProcessGuardian = class {
|
|
|
13699
14150
|
heartbeatTimer = null;
|
|
13700
14151
|
isRunning = false;
|
|
13701
14152
|
instanceId;
|
|
14153
|
+
sigtermCount = 0;
|
|
13702
14154
|
constructor(config = {}) {
|
|
13703
14155
|
this.registry = getPersistentProcessRegistry();
|
|
13704
14156
|
this.config = {
|
|
@@ -13706,7 +14158,8 @@ var ProcessGuardian = class {
|
|
|
13706
14158
|
autoResurrect: config.autoResurrect ?? false,
|
|
13707
14159
|
// Disabled by default
|
|
13708
14160
|
maxResurrectionAttempts: config.maxResurrectionAttempts ?? 3,
|
|
13709
|
-
protectedPatterns: config.protectedPatterns ?? ["node", "wrongstack"]
|
|
14161
|
+
protectedPatterns: config.protectedPatterns ?? ["node", "wrongstack"],
|
|
14162
|
+
sigtermThreshold: config.sigtermThreshold ?? 3
|
|
13710
14163
|
};
|
|
13711
14164
|
this.instanceId = this.registry.getInstanceId();
|
|
13712
14165
|
}
|
|
@@ -13849,15 +14302,43 @@ var ProcessGuardian = class {
|
|
|
13849
14302
|
this.stop();
|
|
13850
14303
|
process.exit(1);
|
|
13851
14304
|
});
|
|
13852
|
-
process.on("SIGTERM", (
|
|
14305
|
+
process.on("SIGTERM", () => {
|
|
14306
|
+
this.sigtermCount++;
|
|
14307
|
+
const threshold = this.config.sigtermThreshold;
|
|
14308
|
+
if (threshold === Infinity) {
|
|
14309
|
+
console.log(JSON.stringify({
|
|
14310
|
+
level: "warn",
|
|
14311
|
+
event: "process_guardian.sigterm_ignored",
|
|
14312
|
+
pid: process.pid,
|
|
14313
|
+
instanceId: this.instanceId,
|
|
14314
|
+
sigtermCount: this.sigtermCount,
|
|
14315
|
+
message: "SIGTERM received but sigtermThreshold is Infinity \u2014 ignoring (use graceful shutdown instead)"
|
|
14316
|
+
}));
|
|
14317
|
+
return;
|
|
14318
|
+
}
|
|
14319
|
+
if (this.sigtermCount < threshold) {
|
|
14320
|
+
console.log(JSON.stringify({
|
|
14321
|
+
level: "warn",
|
|
14322
|
+
event: "process_guardian.sigterm_deferred",
|
|
14323
|
+
pid: process.pid,
|
|
14324
|
+
instanceId: this.instanceId,
|
|
14325
|
+
sigtermCount: this.sigtermCount,
|
|
14326
|
+
threshold,
|
|
14327
|
+
message: `SIGTERM received (${this.sigtermCount}/${threshold}) \u2014 ignoring until threshold is reached`
|
|
14328
|
+
}));
|
|
14329
|
+
return;
|
|
14330
|
+
}
|
|
13853
14331
|
console.log(JSON.stringify({
|
|
13854
14332
|
level: "warn",
|
|
13855
|
-
event: "process_guardian.
|
|
13856
|
-
origin,
|
|
14333
|
+
event: "process_guardian.sigterm_exiting",
|
|
13857
14334
|
pid: process.pid,
|
|
13858
14335
|
instanceId: this.instanceId,
|
|
13859
|
-
|
|
14336
|
+
sigtermCount: this.sigtermCount,
|
|
14337
|
+
threshold,
|
|
14338
|
+
message: `SIGTERM threshold (${threshold}) reached \u2014 shutting down gracefully`
|
|
13860
14339
|
}));
|
|
14340
|
+
this.stop();
|
|
14341
|
+
process.exit(0);
|
|
13861
14342
|
});
|
|
13862
14343
|
process.on("SIGHUP", () => {
|
|
13863
14344
|
console.log(JSON.stringify({
|
|
@@ -14265,7 +14746,7 @@ function makeSkillTool(skillLoader) {
|
|
|
14265
14746
|
field: "name"
|
|
14266
14747
|
});
|
|
14267
14748
|
}
|
|
14268
|
-
const dir =
|
|
14749
|
+
const dir = path12.dirname(manifest.path);
|
|
14269
14750
|
let loadedResource;
|
|
14270
14751
|
if (input.resource?.trim()) {
|
|
14271
14752
|
loadedResource = await loadResource(dir, input.resource.trim());
|
|
@@ -14315,15 +14796,15 @@ ${listing}`;
|
|
|
14315
14796
|
}
|
|
14316
14797
|
async function loadResource(skillDir, rel) {
|
|
14317
14798
|
const norm = rel.replace(/\\/g, "/");
|
|
14318
|
-
if (
|
|
14799
|
+
if (path12.isAbsolute(rel) || norm.split("/").some((seg) => seg === "..")) {
|
|
14319
14800
|
throw new ToolValidationError({
|
|
14320
14801
|
message: `skill: invalid resource path "${rel}"`,
|
|
14321
14802
|
field: "resource"
|
|
14322
14803
|
});
|
|
14323
14804
|
}
|
|
14324
|
-
const absPath =
|
|
14325
|
-
const root =
|
|
14326
|
-
if (absPath !== root && !absPath.startsWith(root +
|
|
14805
|
+
const absPath = path12.resolve(skillDir, rel);
|
|
14806
|
+
const root = path12.resolve(skillDir);
|
|
14807
|
+
if (absPath !== root && !absPath.startsWith(root + path12.sep)) {
|
|
14327
14808
|
throw new ToolValidationError({
|
|
14328
14809
|
message: `skill: resource "${rel}" escapes the skill directory`,
|
|
14329
14810
|
field: "resource"
|
|
@@ -14364,7 +14845,7 @@ async function walk(root, dir, out) {
|
|
|
14364
14845
|
}
|
|
14365
14846
|
for (const e of entries) {
|
|
14366
14847
|
if (out.length >= MAX_LISTED_RESOURCES) return;
|
|
14367
|
-
const fullPath =
|
|
14848
|
+
const fullPath = path12.join(dir, e.name);
|
|
14368
14849
|
let isDir = e.isDirectory();
|
|
14369
14850
|
if (e.isSymbolicLink()) {
|
|
14370
14851
|
try {
|
|
@@ -14379,9 +14860,9 @@ async function walk(root, dir, out) {
|
|
|
14379
14860
|
} else if (e.isFile()) {
|
|
14380
14861
|
if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
|
|
14381
14862
|
try {
|
|
14382
|
-
const
|
|
14383
|
-
const rel =
|
|
14384
|
-
out.push({ path: rel, bytes:
|
|
14863
|
+
const stat13 = await fs2.stat(fullPath);
|
|
14864
|
+
const rel = path12.relative(root, fullPath).split(path12.sep).join("/");
|
|
14865
|
+
out.push({ path: rel, bytes: stat13.size });
|
|
14385
14866
|
} catch {
|
|
14386
14867
|
}
|
|
14387
14868
|
}
|