@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/builtin.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { spawn, execFileSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, ToolValidationError, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, ToolError, FetchError, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, deepMerge,
|
|
3
|
+
import { buildChildEnv, getDesignKitLoader, isDesignStack, loadActiveKit, applyTokenOverrides, setActiveKit, recordKitChoice, recordOverrides, setDesignOverrides, materializeTokens, runDesignVerify, ToolValidationError, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, ToolError, FetchError, isPrivateIPv4, isPrivateIPv6, assessCommitSafety, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, deepMerge, deserializeTaskGraph, serializeTaskGraph, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, FsError, toErrorMessage as toErrorMessage$2, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import { statSync, mkdirSync, createWriteStream } from 'node:fs';
|
|
6
6
|
import * as fs2 from 'node:fs/promises';
|
|
7
|
-
import * as
|
|
7
|
+
import * as path12 from 'node:path';
|
|
8
8
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
9
9
|
import * as os2 from 'node:os';
|
|
10
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
10
11
|
import { toErrorMessage as toErrorMessage$3 } from '@wrongstack/core/utils';
|
|
11
12
|
import { createRequire } from 'node:module';
|
|
12
13
|
import { fileURLToPath } from 'node:url';
|
|
@@ -17,14 +18,14 @@ import * as dns from 'node:dns/promises';
|
|
|
17
18
|
import * as net from 'node:net';
|
|
18
19
|
import { Agent, fetch } from 'undici';
|
|
19
20
|
import TurndownService from 'turndown';
|
|
20
|
-
import {
|
|
21
|
+
import { addLinkToTask, addNoteToTask, updateCheckOnTask, addCheckToTask, updateGoalMetricOnTask, addGoalMetricToTask, addDependency, getKanbanQueueHealth, listKanbanEvents, recoverStaleTaskAssignments, heartbeatTaskAssignment, updateTaskAssignment, assignTask, releaseTaskClaim, claimReadyTask, getTaskChain, setTaskChain, removeTask, moveTask, updateTask, getTask, transferTaskToBoard, copyTaskToBoard, mergeTasks, splitTask, addTask, removeColumn, updateColumn, addColumn, getKanbanOrchestrationSnapshot, listReadyTasks, searchKanban, syncBoardFromTaskGraph, exportBoardToTaskGraph, exportBoardAsMarkdown, generateBoardFromDescription, createBoard, parseLinesIntoTasks, getBoard, removeBoard, duplicateBoard, updateBoard, listBoards } from '@wrongstack/kanban';
|
|
21
22
|
|
|
22
23
|
// src/_spawn-stream.ts
|
|
23
24
|
var SPOOL_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
24
25
|
var SPOOL_WRITE_HWM_BYTES = 4 * 1024 * 1024;
|
|
25
26
|
var sweepStarted = false;
|
|
26
27
|
function toolOutputDir() {
|
|
27
|
-
return
|
|
28
|
+
return path12.join(wstackGlobalRoot(), "tool-output");
|
|
28
29
|
}
|
|
29
30
|
function sweepOldSpoolFiles(dir) {
|
|
30
31
|
if (sweepStarted) return;
|
|
@@ -34,7 +35,7 @@ function sweepOldSpoolFiles(dir) {
|
|
|
34
35
|
const now = Date.now();
|
|
35
36
|
for (const name of await fs2.readdir(dir)) {
|
|
36
37
|
if (!name.endsWith(".log")) continue;
|
|
37
|
-
const p =
|
|
38
|
+
const p = path12.join(dir, name);
|
|
38
39
|
try {
|
|
39
40
|
const st = await fs2.stat(p);
|
|
40
41
|
if (now - st.mtimeMs > SPOOL_RETENTION_MS) await fs2.unlink(p);
|
|
@@ -69,7 +70,7 @@ function createOutputSpool(opts) {
|
|
|
69
70
|
sweepOldSpoolFiles(dir);
|
|
70
71
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
71
72
|
const rand = Math.random().toString(36).slice(2, 6);
|
|
72
|
-
filePath =
|
|
73
|
+
filePath = path12.join(dir, `${stamp}-${safeTool}-${rand}.log`);
|
|
73
74
|
stream = createWriteStream(filePath, { flags: "w", encoding: "utf8" });
|
|
74
75
|
stream.on("error", () => {
|
|
75
76
|
failed = true;
|
|
@@ -702,13 +703,13 @@ function getProcessRegistry() {
|
|
|
702
703
|
}
|
|
703
704
|
function resolveWin32Command(cmd) {
|
|
704
705
|
if (process.platform !== "win32") return cmd;
|
|
705
|
-
if (cmd.includes("/") || cmd.includes("\\") ||
|
|
706
|
+
if (cmd.includes("/") || cmd.includes("\\") || path12.extname(cmd.replace(/\//g, "\\"))) {
|
|
706
707
|
return cmd;
|
|
707
708
|
}
|
|
708
709
|
const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
|
|
709
|
-
const pathDirs = (process.env["PATH"] ?? "").split(
|
|
710
|
+
const pathDirs = (process.env["PATH"] ?? "").split(path12.delimiter);
|
|
710
711
|
for (const dir of pathDirs) {
|
|
711
|
-
const base =
|
|
712
|
+
const base = path12.join(dir, cmd);
|
|
712
713
|
for (const ext of pathext) {
|
|
713
714
|
const full = `${base}${ext}`;
|
|
714
715
|
try {
|
|
@@ -922,37 +923,40 @@ async function* spawnStream(opts) {
|
|
|
922
923
|
}
|
|
923
924
|
}
|
|
924
925
|
}
|
|
926
|
+
function sha256hex(content) {
|
|
927
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
928
|
+
}
|
|
925
929
|
async function detectPackageManager(cwd) {
|
|
926
|
-
const { stat:
|
|
930
|
+
const { stat: stat12 } = await import('node:fs/promises');
|
|
927
931
|
try {
|
|
928
|
-
await
|
|
932
|
+
await stat12(`${cwd}/pnpm-lock.yaml`);
|
|
929
933
|
return "pnpm";
|
|
930
934
|
} catch {
|
|
931
935
|
}
|
|
932
936
|
try {
|
|
933
|
-
await
|
|
937
|
+
await stat12(`${cwd}/yarn.lock`);
|
|
934
938
|
return "yarn";
|
|
935
939
|
} catch {
|
|
936
940
|
}
|
|
937
941
|
return "npm";
|
|
938
942
|
}
|
|
939
943
|
function resolvePath(input, ctx) {
|
|
940
|
-
return
|
|
944
|
+
return path12.isAbsolute(input) ? path12.normalize(input) : path12.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
941
945
|
}
|
|
942
946
|
function allowedRoots(ctx) {
|
|
943
|
-
return [
|
|
947
|
+
return [path12.resolve(ctx.projectRoot), path12.resolve(Core.wstackGlobalRoot())];
|
|
944
948
|
}
|
|
945
949
|
function isInsideAny(target, roots) {
|
|
946
950
|
return roots.some((root) => {
|
|
947
|
-
const rel =
|
|
948
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
951
|
+
const rel = path12.relative(root, target);
|
|
952
|
+
return rel === "" || !rel.startsWith("..") && !path12.isAbsolute(rel);
|
|
949
953
|
});
|
|
950
954
|
}
|
|
951
955
|
function ensureInsideRoot(absPath, ctx) {
|
|
952
|
-
const target =
|
|
956
|
+
const target = path12.resolve(absPath);
|
|
953
957
|
if (ctx.allowOutsideProjectRoot) return target;
|
|
954
958
|
if (isInsideAny(target, allowedRoots(ctx))) return target;
|
|
955
|
-
throw new Error(`Path "${absPath}" is outside project root "${
|
|
959
|
+
throw new Error(`Path "${absPath}" is outside project root "${path12.resolve(ctx.projectRoot)}"`);
|
|
956
960
|
}
|
|
957
961
|
function safeResolve(input, ctx) {
|
|
958
962
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
@@ -960,7 +964,7 @@ function safeResolve(input, ctx) {
|
|
|
960
964
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
961
965
|
if (ctx.allowOutsideProjectRoot) return;
|
|
962
966
|
const realRoots = await Promise.all(
|
|
963
|
-
allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() =>
|
|
967
|
+
allowedRoots(ctx).map((r) => fs2.realpath(r).catch(() => path12.resolve(r)))
|
|
964
968
|
);
|
|
965
969
|
let probe = absPath;
|
|
966
970
|
for (; ; ) {
|
|
@@ -969,7 +973,7 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
969
973
|
real = await fs2.realpath(probe);
|
|
970
974
|
} catch (err) {
|
|
971
975
|
if (err.code === "ENOENT") {
|
|
972
|
-
const parent =
|
|
976
|
+
const parent = path12.dirname(probe);
|
|
973
977
|
if (parent === probe) return;
|
|
974
978
|
probe = parent;
|
|
975
979
|
continue;
|
|
@@ -1282,15 +1286,15 @@ var PersistentProcessRegistry = class {
|
|
|
1282
1286
|
constructor(baseRegistry) {
|
|
1283
1287
|
this.instanceId = generateInstanceId();
|
|
1284
1288
|
const homeDir = os2.homedir();
|
|
1285
|
-
this.registryPath =
|
|
1286
|
-
this.lockPath =
|
|
1289
|
+
this.registryPath = path12.join(homeDir, REGISTRY_FILE);
|
|
1290
|
+
this.lockPath = path12.join(homeDir, LOCKFILE);
|
|
1287
1291
|
this.baseRegistry = baseRegistry ?? getProcessRegistry();
|
|
1288
1292
|
this.ensureDirectory().catch((err) => {
|
|
1289
1293
|
emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
|
|
1290
1294
|
});
|
|
1291
1295
|
}
|
|
1292
1296
|
async ensureDirectory() {
|
|
1293
|
-
const dir =
|
|
1297
|
+
const dir = path12.dirname(this.registryPath);
|
|
1294
1298
|
try {
|
|
1295
1299
|
await fs2.mkdir(dir, { recursive: true });
|
|
1296
1300
|
} catch (err) {
|
|
@@ -1870,6 +1874,10 @@ var bashTool = {
|
|
|
1870
1874
|
category: "Shell",
|
|
1871
1875
|
description: "Execute an arbitrary command in the user's default shell (bash/zsh/pwsh/cmd). stdout and stderr are merged into one stream. This is the most powerful and dangerous tool \u2014 it gives the model full access to the developer's machine. Prefer specialized tools whenever possible.",
|
|
1872
1876
|
usageHint: "SECURITY WARNING: This tool runs with the full privileges of the current user.\n\nBest practices for the model:\n- Strongly prefer `exec` for known safe commands (node, npm, pnpm, tsc, git, etc.).\n- Use bash only when you genuinely need shell features (pipes, redirection, complex one-liners).\n- Prefer single focused commands over huge `&&` chains.\n- Use `background: true` only for long-running processes (dev servers, watchers).\n- The working directory is the project root.\n- Output may be truncated in the middle for very large results.",
|
|
1877
|
+
selection: {
|
|
1878
|
+
doNotUseWhen: "the command is allowlisted and does not require pipes, redirection, or shell expansion.",
|
|
1879
|
+
useInstead: ["exec"]
|
|
1880
|
+
},
|
|
1873
1881
|
permission: "confirm",
|
|
1874
1882
|
mutating: true,
|
|
1875
1883
|
riskTier: "destructive",
|
|
@@ -2675,6 +2683,24 @@ var IndexStore = class {
|
|
|
2675
2683
|
* When false, ranked search falls back to the LIKE + in-process BM25 path.
|
|
2676
2684
|
*/
|
|
2677
2685
|
ftsAvailable = false;
|
|
2686
|
+
/**
|
|
2687
|
+
* Cache of prepared statements keyed by their SQL text. `DatabaseSync`
|
|
2688
|
+
* compiles SQL on every `.prepare()` call; for the fixed-SQL methods
|
|
2689
|
+
* (upsertFile, getFileMeta, deleteFile, insertRefs, …) that runs thousands
|
|
2690
|
+
* of times during a full reindex. `StatementSync` objects are reusable
|
|
2691
|
+
* across calls on the same connection, so we compile each distinct SQL once
|
|
2692
|
+
* and reuse it. Cleared in {@link close} when the connection is torn down.
|
|
2693
|
+
*/
|
|
2694
|
+
stmtCache = /* @__PURE__ */ new Map();
|
|
2695
|
+
/** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
|
|
2696
|
+
stmt(sql) {
|
|
2697
|
+
let s = this.stmtCache.get(sql);
|
|
2698
|
+
if (s === void 0) {
|
|
2699
|
+
s = this.db.prepare(sql);
|
|
2700
|
+
this.stmtCache.set(sql, s);
|
|
2701
|
+
}
|
|
2702
|
+
return s;
|
|
2703
|
+
}
|
|
2678
2704
|
/**
|
|
2679
2705
|
* Execute a SQLite write operation with automatic retry on lock conflicts.
|
|
2680
2706
|
*
|
|
@@ -2713,7 +2739,7 @@ var IndexStore = class {
|
|
|
2713
2739
|
this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
|
|
2714
2740
|
fs.mkdirSync(this.indexDir, { recursive: true });
|
|
2715
2741
|
const Database = loadDatabaseSync();
|
|
2716
|
-
this.db = new Database(
|
|
2742
|
+
this.db = new Database(path12.join(this.indexDir, DB_FILE));
|
|
2717
2743
|
try {
|
|
2718
2744
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
2719
2745
|
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -2844,9 +2870,9 @@ var IndexStore = class {
|
|
|
2844
2870
|
deleteSymbolsForFile(file) {
|
|
2845
2871
|
this.runWithRetry(() => {
|
|
2846
2872
|
if (this.ftsAvailable) {
|
|
2847
|
-
this.
|
|
2873
|
+
this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
|
|
2848
2874
|
}
|
|
2849
|
-
this.
|
|
2875
|
+
this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
|
|
2850
2876
|
});
|
|
2851
2877
|
}
|
|
2852
2878
|
/**
|
|
@@ -2858,13 +2884,13 @@ var IndexStore = class {
|
|
|
2858
2884
|
this.runWithRetry(() => {
|
|
2859
2885
|
this.deleteRefsForFile(file);
|
|
2860
2886
|
this.deleteSymbolsForFile(file);
|
|
2861
|
-
this.
|
|
2887
|
+
this.stmt("DELETE FROM files WHERE file = ?").run(file);
|
|
2862
2888
|
});
|
|
2863
2889
|
}
|
|
2864
2890
|
// ─── File metadata ──────────────────────────────────────────────────────────
|
|
2865
2891
|
upsertFile(meta) {
|
|
2866
2892
|
this.runWithRetry(() => {
|
|
2867
|
-
this.
|
|
2893
|
+
this.stmt(
|
|
2868
2894
|
`INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
|
|
2869
2895
|
VALUES (?, ?, ?, ?, ?)
|
|
2870
2896
|
ON CONFLICT(file) DO UPDATE SET
|
|
@@ -2876,7 +2902,7 @@ var IndexStore = class {
|
|
|
2876
2902
|
});
|
|
2877
2903
|
}
|
|
2878
2904
|
getFileMeta(file) {
|
|
2879
|
-
const rows = this.
|
|
2905
|
+
const rows = this.stmt(
|
|
2880
2906
|
"SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files WHERE file = ?"
|
|
2881
2907
|
).all(file);
|
|
2882
2908
|
if (!rows.length) return null;
|
|
@@ -3099,7 +3125,7 @@ var IndexStore = class {
|
|
|
3099
3125
|
*/
|
|
3100
3126
|
insertRefs(fromId, refs) {
|
|
3101
3127
|
this.runWithRetry(() => {
|
|
3102
|
-
this.
|
|
3128
|
+
this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
|
|
3103
3129
|
if (refs.length === 0) return;
|
|
3104
3130
|
const stmt = this.db.prepare(
|
|
3105
3131
|
`INSERT INTO refs(from_id, to_name, to_id, call_type, line)
|
|
@@ -3292,7 +3318,7 @@ var IndexStore = class {
|
|
|
3292
3318
|
}));
|
|
3293
3319
|
}
|
|
3294
3320
|
sizeBytes() {
|
|
3295
|
-
const dbPath =
|
|
3321
|
+
const dbPath = path12.join(this.indexDir, DB_FILE);
|
|
3296
3322
|
try {
|
|
3297
3323
|
return fs.statSync(dbPath).size;
|
|
3298
3324
|
} catch {
|
|
@@ -3300,6 +3326,7 @@ var IndexStore = class {
|
|
|
3300
3326
|
}
|
|
3301
3327
|
}
|
|
3302
3328
|
close() {
|
|
3329
|
+
this.stmtCache.clear();
|
|
3303
3330
|
try {
|
|
3304
3331
|
this.db.close();
|
|
3305
3332
|
} catch {
|
|
@@ -3790,9 +3817,9 @@ func formatType(t ast.Expr) string {
|
|
|
3790
3817
|
}
|
|
3791
3818
|
`;
|
|
3792
3819
|
async function syncGoParse(filePath, content, lang) {
|
|
3793
|
-
const tmpDir = await fs2.mkdtemp(
|
|
3820
|
+
const tmpDir = await fs2.mkdtemp(path12.join(os2.tmpdir(), "ws-go-parse-"));
|
|
3794
3821
|
try {
|
|
3795
|
-
const scriptPath =
|
|
3822
|
+
const scriptPath = path12.join(tmpDir, "parse.go");
|
|
3796
3823
|
await fs2.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
|
|
3797
3824
|
const proc = spawn("go", ["run", scriptPath], {
|
|
3798
3825
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -4054,9 +4081,9 @@ var _cachedScriptPath = null;
|
|
|
4054
4081
|
async function syncPyParse(filePath, content, lang) {
|
|
4055
4082
|
try {
|
|
4056
4083
|
if (!_cachedScriptPath) {
|
|
4057
|
-
const tmpDir =
|
|
4084
|
+
const tmpDir = path12.join(os2.tmpdir(), "ws-py-parse");
|
|
4058
4085
|
await fs2.mkdir(tmpDir, { recursive: true });
|
|
4059
|
-
_cachedScriptPath =
|
|
4086
|
+
_cachedScriptPath = path12.join(tmpDir, "parse.py");
|
|
4060
4087
|
await fs2.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
|
|
4061
4088
|
}
|
|
4062
4089
|
const proc = spawn("python", [_cachedScriptPath, filePath], {
|
|
@@ -4114,7 +4141,7 @@ async function parseSymbols4(opts) {
|
|
|
4114
4141
|
function checkNativeParser() {
|
|
4115
4142
|
try {
|
|
4116
4143
|
execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
|
|
4117
|
-
const toolsDir =
|
|
4144
|
+
const toolsDir = path12.join(process.cwd(), "tools");
|
|
4118
4145
|
try {
|
|
4119
4146
|
execFileSync(
|
|
4120
4147
|
"cargo",
|
|
@@ -4124,7 +4151,7 @@ function checkNativeParser() {
|
|
|
4124
4151
|
"--format-version",
|
|
4125
4152
|
"1",
|
|
4126
4153
|
"--manifest-path",
|
|
4127
|
-
|
|
4154
|
+
path12.join(toolsDir, "Cargo.toml")
|
|
4128
4155
|
],
|
|
4129
4156
|
{ stdio: "pipe", windowsHide: true }
|
|
4130
4157
|
);
|
|
@@ -4138,13 +4165,13 @@ function checkNativeParser() {
|
|
|
4138
4165
|
}
|
|
4139
4166
|
async function tryNativeParse(file, content) {
|
|
4140
4167
|
try {
|
|
4141
|
-
const toolsDir =
|
|
4142
|
-
const crateDir =
|
|
4143
|
-
const tmpFile =
|
|
4168
|
+
const toolsDir = path12.join(process.cwd(), "tools");
|
|
4169
|
+
const crateDir = path12.join(toolsDir, "syn-parser");
|
|
4170
|
+
const tmpFile = path12.join(crateDir, "src", "input.rs");
|
|
4144
4171
|
await fs2.writeFile(tmpFile, content, "utf8");
|
|
4145
4172
|
const proc = spawn(
|
|
4146
4173
|
"cargo",
|
|
4147
|
-
["run", "--manifest-path",
|
|
4174
|
+
["run", "--manifest-path", path12.join(toolsDir, "Cargo.toml")],
|
|
4148
4175
|
{
|
|
4149
4176
|
cwd: process.cwd(),
|
|
4150
4177
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -4194,16 +4221,16 @@ function regexParse(opts) {
|
|
|
4194
4221
|
const { file, content, lang } = opts;
|
|
4195
4222
|
const symbols = [];
|
|
4196
4223
|
const lines = content.split("\n");
|
|
4197
|
-
const
|
|
4224
|
+
const lineOffsets2 = [0];
|
|
4198
4225
|
for (let i = 0; i < lines.length; i++) {
|
|
4199
|
-
|
|
4226
|
+
lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
|
|
4200
4227
|
}
|
|
4201
4228
|
function lineFromOffset(offset) {
|
|
4202
4229
|
let lo = 0;
|
|
4203
|
-
let hi =
|
|
4230
|
+
let hi = lineOffsets2.length - 1;
|
|
4204
4231
|
while (lo < hi) {
|
|
4205
4232
|
const mid = lo + hi + 1 >>> 1;
|
|
4206
|
-
if (expectDefined(
|
|
4233
|
+
if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
|
|
4207
4234
|
else hi = mid - 1;
|
|
4208
4235
|
}
|
|
4209
4236
|
return lo + 1;
|
|
@@ -4218,7 +4245,7 @@ function regexParse(opts) {
|
|
|
4218
4245
|
const name = expectDefined(match[1]);
|
|
4219
4246
|
const offset = match.index ?? 0;
|
|
4220
4247
|
const line = lineFromOffset(offset);
|
|
4221
|
-
const col = offset - (
|
|
4248
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4222
4249
|
const lineIdx = line - 1;
|
|
4223
4250
|
const signature = extractDeclaration(lineIdx);
|
|
4224
4251
|
symbols.push({
|
|
@@ -4256,22 +4283,22 @@ function parseSymbols5(opts) {
|
|
|
4256
4283
|
function regexParse2(opts) {
|
|
4257
4284
|
const { file, content, lang } = opts;
|
|
4258
4285
|
const symbols = [];
|
|
4259
|
-
const
|
|
4260
|
-
const isPackageJson =
|
|
4261
|
-
const isTsconfig =
|
|
4286
|
+
const basename4 = path12.basename(file).toLowerCase();
|
|
4287
|
+
const isPackageJson = basename4 === "package.json";
|
|
4288
|
+
const isTsconfig = basename4 === "tsconfig.json" || basename4 === "tsconfig.build.json";
|
|
4262
4289
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
4263
4290
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
4264
4291
|
const lines = content.split("\n");
|
|
4265
|
-
const
|
|
4292
|
+
const lineOffsets2 = [0];
|
|
4266
4293
|
for (let i = 0; i < lines.length; i++) {
|
|
4267
|
-
|
|
4294
|
+
lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
|
|
4268
4295
|
}
|
|
4269
4296
|
function lineFromOffset(offset) {
|
|
4270
4297
|
let lo = 0;
|
|
4271
|
-
let hi =
|
|
4298
|
+
let hi = lineOffsets2.length - 1;
|
|
4272
4299
|
while (lo < hi) {
|
|
4273
4300
|
const mid = lo + hi + 1 >>> 1;
|
|
4274
|
-
if (expectDefined(
|
|
4301
|
+
if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
|
|
4275
4302
|
else hi = mid - 1;
|
|
4276
4303
|
}
|
|
4277
4304
|
return lo + 1;
|
|
@@ -4282,11 +4309,11 @@ function regexParse2(opts) {
|
|
|
4282
4309
|
const line = lineFromOffset(offset);
|
|
4283
4310
|
symbols.push(
|
|
4284
4311
|
makeSymbol({
|
|
4285
|
-
name:
|
|
4312
|
+
name: path12.basename(file),
|
|
4286
4313
|
kind: "object",
|
|
4287
4314
|
line,
|
|
4288
4315
|
col: 0,
|
|
4289
|
-
signature: `"${
|
|
4316
|
+
signature: `"${path12.basename(file)}" = { ... }`,
|
|
4290
4317
|
file,
|
|
4291
4318
|
lang
|
|
4292
4319
|
})
|
|
@@ -4297,7 +4324,7 @@ function regexParse2(opts) {
|
|
|
4297
4324
|
const key = expectDefined(match[1]);
|
|
4298
4325
|
const offset = match.index ?? 0;
|
|
4299
4326
|
const line = lineFromOffset(offset);
|
|
4300
|
-
const col = offset - (
|
|
4327
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4301
4328
|
let kind = "property";
|
|
4302
4329
|
let signature = `"${key}": ..."`;
|
|
4303
4330
|
if (isPackageJson) {
|
|
@@ -4332,10 +4359,10 @@ function regexParse2(opts) {
|
|
|
4332
4359
|
})
|
|
4333
4360
|
);
|
|
4334
4361
|
if (isPackageJson && key === "scripts") {
|
|
4335
|
-
extractPackageScripts(content, symbols, file, lang,
|
|
4362
|
+
extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset);
|
|
4336
4363
|
}
|
|
4337
4364
|
if (isTsconfig && key === "compilerOptions") {
|
|
4338
|
-
extractCompilerOptions(content, symbols, file, lang,
|
|
4365
|
+
extractCompilerOptions(content, symbols, file, lang, lineOffsets2, line, lineFromOffset);
|
|
4339
4366
|
}
|
|
4340
4367
|
}
|
|
4341
4368
|
const defsRegex = /"\$defs"\s*:|"\$defs"\s*:/g;
|
|
@@ -4348,7 +4375,7 @@ function regexParse2(opts) {
|
|
|
4348
4375
|
name: "$defs",
|
|
4349
4376
|
kind: "property",
|
|
4350
4377
|
line,
|
|
4351
|
-
col: offset - (
|
|
4378
|
+
col: offset - (lineOffsets2[line - 1] ?? 0),
|
|
4352
4379
|
signature: '"$defs": { ... }',
|
|
4353
4380
|
file,
|
|
4354
4381
|
lang
|
|
@@ -4372,7 +4399,7 @@ function regexParse2(opts) {
|
|
|
4372
4399
|
name: key,
|
|
4373
4400
|
kind: "property",
|
|
4374
4401
|
line,
|
|
4375
|
-
col: offset - (
|
|
4402
|
+
col: offset - (lineOffsets2[line - 1] ?? 0),
|
|
4376
4403
|
signature: `"${key}": { ... }`,
|
|
4377
4404
|
file,
|
|
4378
4405
|
lang
|
|
@@ -4382,7 +4409,7 @@ function regexParse2(opts) {
|
|
|
4382
4409
|
}
|
|
4383
4410
|
return { file, lang, symbols, mtimeMs: Date.now() };
|
|
4384
4411
|
}
|
|
4385
|
-
function extractPackageScripts(content, symbols, file, lang,
|
|
4412
|
+
function extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset) {
|
|
4386
4413
|
const scriptsBlockRegex = /"scripts"\s*:\s*\{([^}]+)\}/g;
|
|
4387
4414
|
for (let match = scriptsBlockRegex.exec(content); match !== null; match = scriptsBlockRegex.exec(content)) {
|
|
4388
4415
|
const blockContent = expectDefined(match[0]);
|
|
@@ -4397,7 +4424,7 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFr
|
|
|
4397
4424
|
name: key,
|
|
4398
4425
|
kind: "function",
|
|
4399
4426
|
line,
|
|
4400
|
-
col: keyOffset - (
|
|
4427
|
+
col: keyOffset - (lineOffsets2[line - 1] ?? 0),
|
|
4401
4428
|
signature: `"${key}": "..."`,
|
|
4402
4429
|
file,
|
|
4403
4430
|
lang
|
|
@@ -4406,7 +4433,7 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFr
|
|
|
4406
4433
|
}
|
|
4407
4434
|
}
|
|
4408
4435
|
}
|
|
4409
|
-
function extractCompilerOptions(content, symbols, file, lang,
|
|
4436
|
+
function extractCompilerOptions(content, symbols, file, lang, lineOffsets2, parentLine, lineFromOffset) {
|
|
4410
4437
|
const optsBlockRegex = /"compilerOptions"\s*:\s*\{([^}]+)\}/g;
|
|
4411
4438
|
for (let match = optsBlockRegex.exec(content); match !== null; match = optsBlockRegex.exec(content)) {
|
|
4412
4439
|
const blockContent = expectDefined(match[0]);
|
|
@@ -4422,7 +4449,7 @@ function extractCompilerOptions(content, symbols, file, lang, lineOffsets, paren
|
|
|
4422
4449
|
name: key,
|
|
4423
4450
|
kind: "property",
|
|
4424
4451
|
line,
|
|
4425
|
-
col: keyOffset - (
|
|
4452
|
+
col: keyOffset - (lineOffsets2[line - 1] ?? 0),
|
|
4426
4453
|
signature: `"${key}": ...`,
|
|
4427
4454
|
file,
|
|
4428
4455
|
lang
|
|
@@ -4458,16 +4485,16 @@ function regexParse3(opts) {
|
|
|
4458
4485
|
const { file, content, lang } = opts;
|
|
4459
4486
|
const symbols = [];
|
|
4460
4487
|
const lines = content.split("\n");
|
|
4461
|
-
const
|
|
4488
|
+
const lineOffsets2 = [0];
|
|
4462
4489
|
for (let i = 0; i < lines.length; i++) {
|
|
4463
|
-
|
|
4490
|
+
lineOffsets2.push((lineOffsets2[i] ?? 0) + (lines[i]?.length ?? 0) + 1);
|
|
4464
4491
|
}
|
|
4465
4492
|
function lineFromOffset(offset) {
|
|
4466
4493
|
let lo = 0;
|
|
4467
|
-
let hi =
|
|
4494
|
+
let hi = lineOffsets2.length - 1;
|
|
4468
4495
|
while (lo < hi) {
|
|
4469
4496
|
const mid = lo + hi + 1 >>> 1;
|
|
4470
|
-
if (expectDefined(
|
|
4497
|
+
if (expectDefined(lineOffsets2[mid]) <= offset) lo = mid;
|
|
4471
4498
|
else hi = mid - 1;
|
|
4472
4499
|
}
|
|
4473
4500
|
return lo + 1;
|
|
@@ -4477,7 +4504,7 @@ function regexParse3(opts) {
|
|
|
4477
4504
|
const name = expectDefined(match[1]);
|
|
4478
4505
|
const offset = match.index ?? 0;
|
|
4479
4506
|
const line = lineFromOffset(offset);
|
|
4480
|
-
const col = offset - (
|
|
4507
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4481
4508
|
symbols.push(
|
|
4482
4509
|
makeSymbol2({
|
|
4483
4510
|
name,
|
|
@@ -4495,7 +4522,7 @@ function regexParse3(opts) {
|
|
|
4495
4522
|
const name = expectDefined(match[1]);
|
|
4496
4523
|
const offset = match.index ?? 0;
|
|
4497
4524
|
const line = lineFromOffset(offset);
|
|
4498
|
-
const col = offset - (
|
|
4525
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4499
4526
|
symbols.push(
|
|
4500
4527
|
makeSymbol2({
|
|
4501
4528
|
name,
|
|
@@ -4515,7 +4542,7 @@ function regexParse3(opts) {
|
|
|
4515
4542
|
if (!key) continue;
|
|
4516
4543
|
const offset = match.index ?? 0;
|
|
4517
4544
|
const line = lineFromOffset(offset);
|
|
4518
|
-
const col = offset - (
|
|
4545
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4519
4546
|
const lineContent = lines[line - 1] ?? "";
|
|
4520
4547
|
if (/^[|&>]/.test(lineContent.trim())) continue;
|
|
4521
4548
|
if (key === "---" || key === "...") continue;
|
|
@@ -4530,7 +4557,7 @@ function regexParse3(opts) {
|
|
|
4530
4557
|
const key = expectDefined(match[2]);
|
|
4531
4558
|
const offset = match.index ?? 0;
|
|
4532
4559
|
const line = lineFromOffset(offset);
|
|
4533
|
-
const col = offset - (
|
|
4560
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4534
4561
|
const value = extractValue(content, offset + match[0]?.length);
|
|
4535
4562
|
const kind = isScalar(value) ? "literal" : "property";
|
|
4536
4563
|
symbols.push(
|
|
@@ -4550,7 +4577,7 @@ function regexParse3(opts) {
|
|
|
4550
4577
|
const key = expectDefined(match[2]);
|
|
4551
4578
|
const offset = match.index ?? 0;
|
|
4552
4579
|
const line = lineFromOffset(offset);
|
|
4553
|
-
const col = offset - (
|
|
4580
|
+
const col = offset - (lineOffsets2[line - 1] ?? 0);
|
|
4554
4581
|
symbols.push(
|
|
4555
4582
|
makeSymbol2({
|
|
4556
4583
|
name: key,
|
|
@@ -4636,7 +4663,7 @@ function compileGitignore(lines) {
|
|
|
4636
4663
|
async function loadGitignoreMatcher(projectRoot) {
|
|
4637
4664
|
let lines = [];
|
|
4638
4665
|
try {
|
|
4639
|
-
const raw = await fs2.readFile(
|
|
4666
|
+
const raw = await fs2.readFile(path12.join(projectRoot, ".gitignore"), "utf8");
|
|
4640
4667
|
lines = raw.split("\n");
|
|
4641
4668
|
} catch {
|
|
4642
4669
|
}
|
|
@@ -4701,14 +4728,14 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
|
|
|
4701
4728
|
dirCount++;
|
|
4702
4729
|
for (const e of entries) {
|
|
4703
4730
|
if (ignoreSet.has(e.name)) continue;
|
|
4704
|
-
const full =
|
|
4705
|
-
const rel =
|
|
4731
|
+
const full = path12.join(dir, e.name);
|
|
4732
|
+
const rel = path12.relative(projectRoot, full).replace(/\\/g, "/");
|
|
4706
4733
|
if (e.isDirectory()) {
|
|
4707
4734
|
if (isGitIgnored(rel, true)) continue;
|
|
4708
4735
|
await walk(full);
|
|
4709
4736
|
} else if (e.isFile()) {
|
|
4710
4737
|
if (isGitIgnored(rel, false)) continue;
|
|
4711
|
-
const ext =
|
|
4738
|
+
const ext = path12.extname(e.name);
|
|
4712
4739
|
for (const { ext: extName, pat } of globs) {
|
|
4713
4740
|
if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
|
|
4714
4741
|
results.push(full);
|
|
@@ -4764,7 +4791,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4764
4791
|
let files;
|
|
4765
4792
|
let discoveredFiles = null;
|
|
4766
4793
|
if (opts.files && opts.files.length > 0) {
|
|
4767
|
-
files = opts.files.map((f) =>
|
|
4794
|
+
files = opts.files.map((f) => path12.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path12.relative(projectRoot, f).replace(/\\/g, "/"), false));
|
|
4768
4795
|
} else {
|
|
4769
4796
|
files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
|
|
4770
4797
|
discoveredFiles = new Set(files);
|
|
@@ -4792,34 +4819,34 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4792
4819
|
const statOpts = signal ? { signal } : {};
|
|
4793
4820
|
const statReadParse = await Promise.allSettled(
|
|
4794
4821
|
batchFiles.map(async (file) => {
|
|
4795
|
-
let
|
|
4822
|
+
let stat12;
|
|
4796
4823
|
try {
|
|
4797
|
-
|
|
4824
|
+
stat12 = await fs2.stat(file, statOpts);
|
|
4798
4825
|
} catch (e) {
|
|
4799
4826
|
if (isAbortError(e)) throw e;
|
|
4800
4827
|
return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
|
|
4801
4828
|
}
|
|
4802
|
-
if (!
|
|
4829
|
+
if (!stat12.isFile()) return { file, stat: stat12, lang: "", parsed: null };
|
|
4803
4830
|
const lang = detectLang(file);
|
|
4804
|
-
if (!lang) return { file, stat:
|
|
4831
|
+
if (!lang) return { file, stat: stat12, lang: "", parsed: null };
|
|
4805
4832
|
const meta = existingMeta.get(file);
|
|
4806
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
4807
|
-
return { file, stat:
|
|
4833
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat12.mtimeMs)) {
|
|
4834
|
+
return { file, stat: stat12, lang, parsed: null, skippedMeta: meta };
|
|
4808
4835
|
}
|
|
4809
4836
|
let content;
|
|
4810
4837
|
try {
|
|
4811
4838
|
content = await fs2.readFile(file, { encoding: "utf8", signal });
|
|
4812
4839
|
} catch (e) {
|
|
4813
4840
|
if (isAbortError(e)) throw e;
|
|
4814
|
-
return { file, stat:
|
|
4841
|
+
return { file, stat: stat12, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
|
|
4815
4842
|
}
|
|
4816
4843
|
let parsed;
|
|
4817
4844
|
try {
|
|
4818
4845
|
parsed = await parseFile(file, content, lang);
|
|
4819
4846
|
} catch (e) {
|
|
4820
|
-
return { file, stat:
|
|
4847
|
+
return { file, stat: stat12, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
|
|
4821
4848
|
}
|
|
4822
|
-
return { file, stat:
|
|
4849
|
+
return { file, stat: stat12, lang, parsed, content };
|
|
4823
4850
|
})
|
|
4824
4851
|
);
|
|
4825
4852
|
const batchEntries = [];
|
|
@@ -4839,7 +4866,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4839
4866
|
if (result.error.includes("error:")) errors.push(result.error);
|
|
4840
4867
|
continue;
|
|
4841
4868
|
}
|
|
4842
|
-
const { stat:
|
|
4869
|
+
const { stat: stat12, lang, parsed } = result;
|
|
4843
4870
|
if (result.skippedMeta) {
|
|
4844
4871
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
4845
4872
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -4851,7 +4878,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4851
4878
|
store.upsertFile({
|
|
4852
4879
|
file,
|
|
4853
4880
|
lang,
|
|
4854
|
-
mtimeMs: Math.floor(
|
|
4881
|
+
mtimeMs: Math.floor(stat12.mtimeMs),
|
|
4855
4882
|
symbolCount: 0,
|
|
4856
4883
|
lastIndexed: Date.now()
|
|
4857
4884
|
});
|
|
@@ -4863,7 +4890,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4863
4890
|
store.upsertFile({
|
|
4864
4891
|
file,
|
|
4865
4892
|
lang,
|
|
4866
|
-
mtimeMs: Math.floor(
|
|
4893
|
+
mtimeMs: Math.floor(stat12.mtimeMs),
|
|
4867
4894
|
symbolCount: 0,
|
|
4868
4895
|
lastIndexed: Date.now()
|
|
4869
4896
|
});
|
|
@@ -4879,7 +4906,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
4879
4906
|
lang,
|
|
4880
4907
|
symbols: parsed.symbols,
|
|
4881
4908
|
refs,
|
|
4882
|
-
mtimeMs: Math.floor(
|
|
4909
|
+
mtimeMs: Math.floor(stat12.mtimeMs),
|
|
4883
4910
|
symbolCount: parsed.symbols.length
|
|
4884
4911
|
});
|
|
4885
4912
|
deleteForFiles.push(file);
|
|
@@ -5028,7 +5055,7 @@ function statsService(args) {
|
|
|
5028
5055
|
}
|
|
5029
5056
|
|
|
5030
5057
|
// src/codebase-index/background-indexer.ts
|
|
5031
|
-
var DEFAULT_FULL_INDEX_TIMEOUT_MS =
|
|
5058
|
+
var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
|
|
5032
5059
|
var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
|
|
5033
5060
|
var _ready = false;
|
|
5034
5061
|
var _indexing = false;
|
|
@@ -5485,6 +5512,25 @@ var codebaseStatsTool = {
|
|
|
5485
5512
|
};
|
|
5486
5513
|
}
|
|
5487
5514
|
};
|
|
5515
|
+
async function resolveReal(p) {
|
|
5516
|
+
const resolved = path12.resolve(p);
|
|
5517
|
+
let probe = resolved;
|
|
5518
|
+
const missing = [];
|
|
5519
|
+
for (; ; ) {
|
|
5520
|
+
try {
|
|
5521
|
+
return path12.resolve(await fs2.realpath(probe), ...missing);
|
|
5522
|
+
} catch (err) {
|
|
5523
|
+
if (err.code === "ENOENT") {
|
|
5524
|
+
const parent = path12.dirname(probe);
|
|
5525
|
+
if (parent === probe) return resolved;
|
|
5526
|
+
missing.unshift(path12.basename(probe));
|
|
5527
|
+
probe = parent;
|
|
5528
|
+
continue;
|
|
5529
|
+
}
|
|
5530
|
+
return resolved;
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5488
5534
|
function normalizeOverrides(set) {
|
|
5489
5535
|
const out = {};
|
|
5490
5536
|
if (set && typeof set === "object") {
|
|
@@ -5642,10 +5688,12 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
|
|
|
5642
5688
|
kitId: active.kit,
|
|
5643
5689
|
outPath: input.out
|
|
5644
5690
|
});
|
|
5645
|
-
const root =
|
|
5646
|
-
const
|
|
5647
|
-
const
|
|
5648
|
-
|
|
5691
|
+
const root = await resolveReal(ctx.projectRoot);
|
|
5692
|
+
const absResolved = path12.isAbsolute(result.path) ? path12.resolve(result.path) : path12.resolve(path12.join(ctx.projectRoot, result.path));
|
|
5693
|
+
const absParent = await resolveReal(path12.dirname(absResolved));
|
|
5694
|
+
const abs = path12.join(absParent, path12.basename(absResolved));
|
|
5695
|
+
const rel = path12.relative(root, abs);
|
|
5696
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) {
|
|
5649
5697
|
throw new Error(
|
|
5650
5698
|
`design: materialize path "${result.path}" would escape the project root`
|
|
5651
5699
|
);
|
|
@@ -5669,7 +5717,7 @@ ${result.content}
|
|
|
5669
5717
|
\`\`\``
|
|
5670
5718
|
};
|
|
5671
5719
|
}
|
|
5672
|
-
await fs2.mkdir(
|
|
5720
|
+
await fs2.mkdir(path12.dirname(abs), { recursive: true });
|
|
5673
5721
|
await fs2.writeFile(abs, result.content);
|
|
5674
5722
|
return {
|
|
5675
5723
|
action,
|
|
@@ -5797,11 +5845,11 @@ function findGitDir(cwd) {
|
|
|
5797
5845
|
let dir = cwd;
|
|
5798
5846
|
for (let i = 0; i < 20; i++) {
|
|
5799
5847
|
try {
|
|
5800
|
-
const
|
|
5801
|
-
if (
|
|
5848
|
+
const stat12 = statSync(path12.join(dir, ".git"));
|
|
5849
|
+
if (stat12.isDirectory()) return dir;
|
|
5802
5850
|
} catch {
|
|
5803
5851
|
}
|
|
5804
|
-
const parent =
|
|
5852
|
+
const parent = path12.dirname(dir);
|
|
5805
5853
|
if (parent === dir) break;
|
|
5806
5854
|
dir = parent;
|
|
5807
5855
|
}
|
|
@@ -5842,8 +5890,8 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
5842
5890
|
const results = [];
|
|
5843
5891
|
for (const file of files) {
|
|
5844
5892
|
const absPath = safeResolve(file, ctx);
|
|
5845
|
-
const
|
|
5846
|
-
if (!
|
|
5893
|
+
const stat12 = await fs2.stat(absPath).catch(() => null);
|
|
5894
|
+
if (!stat12?.isFile()) continue;
|
|
5847
5895
|
const content = await fs2.readFile(absPath, "utf8");
|
|
5848
5896
|
const lines = content.split(/\r?\n/);
|
|
5849
5897
|
results.push(formatWithLineNumbers(file, lines));
|
|
@@ -5940,8 +5988,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
5940
5988
|
for (const f of files) {
|
|
5941
5989
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
5942
5990
|
try {
|
|
5943
|
-
const
|
|
5944
|
-
if (
|
|
5991
|
+
const stat12 = await fs2.stat(absPath);
|
|
5992
|
+
if (stat12.isFile()) resolved.push(absPath);
|
|
5945
5993
|
} catch {
|
|
5946
5994
|
}
|
|
5947
5995
|
}
|
|
@@ -6007,11 +6055,303 @@ function processFile(content, absPath, _style, _overwrite, target) {
|
|
|
6007
6055
|
}
|
|
6008
6056
|
return results;
|
|
6009
6057
|
}
|
|
6058
|
+
|
|
6059
|
+
// src/_edit-match.ts
|
|
6060
|
+
var TIER_LABEL = {
|
|
6061
|
+
exact: "exact match",
|
|
6062
|
+
"trailing-whitespace": "whitespace-normalized match (trailing whitespace ignored)",
|
|
6063
|
+
"whitespace-normalized": "whitespace-normalized match (indentation ignored, replacement re-indented)",
|
|
6064
|
+
fuzzy: "fuzzy match (block anchors exact, interior \u226590% similar)"
|
|
6065
|
+
};
|
|
6066
|
+
var TIER_CONFIDENCE = {
|
|
6067
|
+
exact: "exact",
|
|
6068
|
+
"trailing-whitespace": "high",
|
|
6069
|
+
"whitespace-normalized": "medium",
|
|
6070
|
+
fuzzy: "low"
|
|
6071
|
+
};
|
|
6072
|
+
var MIN_NORMALIZED_NEEDLE_CHARS = 16;
|
|
6073
|
+
var FUZZY_MIN_SIMILARITY = 0.9;
|
|
6074
|
+
var FUZZY_AMBIGUITY_MARGIN = 0.05;
|
|
6075
|
+
var FUZZY_MAX_INTERIOR_CHARS = 2e3;
|
|
6076
|
+
function findLadderMatches(fileLf, oldLf) {
|
|
6077
|
+
const exact = [];
|
|
6078
|
+
let idx = fileLf.indexOf(oldLf);
|
|
6079
|
+
while (idx !== -1) {
|
|
6080
|
+
exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });
|
|
6081
|
+
idx = fileLf.indexOf(oldLf, idx + 1);
|
|
6082
|
+
}
|
|
6083
|
+
if (exact.length > 0) return { tier: "exact", matches: exact };
|
|
6084
|
+
const fileLines = fileLf.split("\n");
|
|
6085
|
+
const needleLines = oldLf.split("\n");
|
|
6086
|
+
if (needleLines.length > fileLines.length) return void 0;
|
|
6087
|
+
const offsets = lineOffsets(fileLines);
|
|
6088
|
+
const trailing = windowScan(
|
|
6089
|
+
fileLines,
|
|
6090
|
+
needleLines,
|
|
6091
|
+
offsets,
|
|
6092
|
+
(a, b) => a.trimEnd() === b.trimEnd()
|
|
6093
|
+
);
|
|
6094
|
+
if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
|
|
6095
|
+
const normalizedLen = needleLines.reduce((n, l) => n + l.trim().length, 0);
|
|
6096
|
+
if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
|
|
6097
|
+
const normalized = windowScan(fileLines, needleLines, offsets, (a, b) => a.trim() === b.trim());
|
|
6098
|
+
if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
|
|
6099
|
+
return fuzzyScan(fileLines, needleLines, offsets);
|
|
6100
|
+
}
|
|
6101
|
+
function lineAt(text, pos) {
|
|
6102
|
+
let line = 1;
|
|
6103
|
+
for (let i = 0; i < pos; i++) {
|
|
6104
|
+
if (text.charCodeAt(i) === 10) line++;
|
|
6105
|
+
}
|
|
6106
|
+
return line;
|
|
6107
|
+
}
|
|
6108
|
+
function lineOffsets(lines) {
|
|
6109
|
+
const out = new Array(lines.length);
|
|
6110
|
+
let pos = 0;
|
|
6111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6112
|
+
out[i] = pos;
|
|
6113
|
+
pos += lines[i].length + 1;
|
|
6114
|
+
}
|
|
6115
|
+
return out;
|
|
6116
|
+
}
|
|
6117
|
+
function windowToMatch(fileLines, offsets, start, windowLen) {
|
|
6118
|
+
const lastLine = start + windowLen - 1;
|
|
6119
|
+
return {
|
|
6120
|
+
start: offsets[start],
|
|
6121
|
+
end: offsets[lastLine] + fileLines[lastLine].length,
|
|
6122
|
+
startLine: start + 1
|
|
6123
|
+
};
|
|
6124
|
+
}
|
|
6125
|
+
function windowScan(fileLines, needleLines, offsets, eq) {
|
|
6126
|
+
const n = needleLines.length;
|
|
6127
|
+
const out = [];
|
|
6128
|
+
for (let i = 0; i + n <= fileLines.length; i++) {
|
|
6129
|
+
let all = true;
|
|
6130
|
+
for (let j = 0; j < n; j++) {
|
|
6131
|
+
if (!eq(fileLines[i + j], needleLines[j])) {
|
|
6132
|
+
all = false;
|
|
6133
|
+
break;
|
|
6134
|
+
}
|
|
6135
|
+
}
|
|
6136
|
+
if (all) {
|
|
6137
|
+
out.push(windowToMatch(fileLines, offsets, i, n));
|
|
6138
|
+
i += n - 1;
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
return out;
|
|
6142
|
+
}
|
|
6143
|
+
function fuzzyScan(fileLines, needleLines, offsets) {
|
|
6144
|
+
const n = needleLines.length;
|
|
6145
|
+
if (n < 3) return void 0;
|
|
6146
|
+
const firstNeedle = needleLines[0].trim();
|
|
6147
|
+
const lastNeedle = needleLines[n - 1].trim();
|
|
6148
|
+
const needleInterior = needleLines.slice(1, -1).map((l) => l.trim()).join("\n");
|
|
6149
|
+
if (needleInterior.length > FUZZY_MAX_INTERIOR_CHARS) return void 0;
|
|
6150
|
+
const candidates = [];
|
|
6151
|
+
for (let i = 0; i + n <= fileLines.length; i++) {
|
|
6152
|
+
if (fileLines[i].trim() !== firstNeedle) continue;
|
|
6153
|
+
if (fileLines[i + n - 1].trim() !== lastNeedle) continue;
|
|
6154
|
+
const windowInterior = fileLines.slice(i + 1, i + n - 1).map((l) => l.trim()).join("\n");
|
|
6155
|
+
if (windowInterior.length > FUZZY_MAX_INTERIOR_CHARS) continue;
|
|
6156
|
+
const score = similarity(needleInterior, windowInterior);
|
|
6157
|
+
if (score >= FUZZY_MIN_SIMILARITY) {
|
|
6158
|
+
candidates.push({ match: windowToMatch(fileLines, offsets, i, n), score });
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
if (candidates.length === 0) return void 0;
|
|
6162
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
6163
|
+
const best = candidates[0];
|
|
6164
|
+
const runnerUp = candidates[1];
|
|
6165
|
+
if (runnerUp && best.score - runnerUp.score < FUZZY_AMBIGUITY_MARGIN) {
|
|
6166
|
+
return {
|
|
6167
|
+
tier: "fuzzy",
|
|
6168
|
+
matches: candidates.map((c) => c.match),
|
|
6169
|
+
score: best.score,
|
|
6170
|
+
ambiguous: true
|
|
6171
|
+
};
|
|
6172
|
+
}
|
|
6173
|
+
return { tier: "fuzzy", matches: [best.match], score: best.score };
|
|
6174
|
+
}
|
|
6175
|
+
function similarity(a, b) {
|
|
6176
|
+
if (a === b) return 1;
|
|
6177
|
+
const max = Math.max(a.length, b.length);
|
|
6178
|
+
if (max === 0) return 1;
|
|
6179
|
+
if (Math.abs(a.length - b.length) / max > 1 - FUZZY_MIN_SIMILARITY + 0.05) {
|
|
6180
|
+
return 1 - Math.abs(a.length - b.length) / max;
|
|
6181
|
+
}
|
|
6182
|
+
return 1 - levenshtein(a, b) / max;
|
|
6183
|
+
}
|
|
6184
|
+
function levenshtein(a, b) {
|
|
6185
|
+
if (a.length === 0) return b.length;
|
|
6186
|
+
if (b.length === 0) return a.length;
|
|
6187
|
+
let prev = new Array(b.length + 1);
|
|
6188
|
+
let cur = new Array(b.length + 1);
|
|
6189
|
+
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
6190
|
+
for (let i = 1; i <= a.length; i++) {
|
|
6191
|
+
cur[0] = i;
|
|
6192
|
+
const ca = a.charCodeAt(i - 1);
|
|
6193
|
+
for (let j = 1; j <= b.length; j++) {
|
|
6194
|
+
const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
|
|
6195
|
+
cur[j] = Math.min(
|
|
6196
|
+
prev[j] + 1,
|
|
6197
|
+
cur[j - 1] + 1,
|
|
6198
|
+
prev[j - 1] + cost
|
|
6199
|
+
);
|
|
6200
|
+
}
|
|
6201
|
+
[prev, cur] = [cur, prev];
|
|
6202
|
+
}
|
|
6203
|
+
return prev[b.length];
|
|
6204
|
+
}
|
|
6205
|
+
function firstLineIndent(text) {
|
|
6206
|
+
for (const line of text.split("\n")) {
|
|
6207
|
+
if (line.trim() === "") continue;
|
|
6208
|
+
return /^[ \t]*/.exec(line)[0];
|
|
6209
|
+
}
|
|
6210
|
+
return "";
|
|
6211
|
+
}
|
|
6212
|
+
function adjustIndent(newLf, fromIndent, toIndent) {
|
|
6213
|
+
if (fromIndent === toIndent) return { text: newLf, adjusted: false };
|
|
6214
|
+
const lines = newLf.split("\n");
|
|
6215
|
+
if (toIndent.startsWith(fromIndent)) {
|
|
6216
|
+
const extra = toIndent.slice(fromIndent.length);
|
|
6217
|
+
return {
|
|
6218
|
+
text: lines.map((l) => l.trim() === "" ? l : extra + l).join("\n"),
|
|
6219
|
+
adjusted: true
|
|
6220
|
+
};
|
|
6221
|
+
}
|
|
6222
|
+
if (fromIndent.startsWith(toIndent)) {
|
|
6223
|
+
const remove = fromIndent.slice(toIndent.length);
|
|
6224
|
+
return {
|
|
6225
|
+
text: lines.map((l) => l.startsWith(remove) ? l.slice(remove.length) : l).join("\n"),
|
|
6226
|
+
adjusted: true
|
|
6227
|
+
};
|
|
6228
|
+
}
|
|
6229
|
+
return { text: newLf, adjusted: false };
|
|
6230
|
+
}
|
|
6231
|
+
function nearestMatchHint(fileLf, oldLf) {
|
|
6232
|
+
const fileLines = fileLf.split("\n");
|
|
6233
|
+
const needleLines = oldLf.split("\n");
|
|
6234
|
+
if (needleLines.length > fileLines.length) return void 0;
|
|
6235
|
+
const needleTrimmed = needleLines.map((l) => l.trim());
|
|
6236
|
+
let bestScore = 0;
|
|
6237
|
+
let bestStart = -1;
|
|
6238
|
+
for (let i = 0; i + needleLines.length <= fileLines.length; i++) {
|
|
6239
|
+
let sum = 0;
|
|
6240
|
+
for (let j = 0; j < needleLines.length; j++) {
|
|
6241
|
+
sum += prefixSimilarity(needleTrimmed[j], fileLines[i + j].trim());
|
|
6242
|
+
}
|
|
6243
|
+
const score = sum / needleLines.length;
|
|
6244
|
+
if (score > bestScore) {
|
|
6245
|
+
bestScore = score;
|
|
6246
|
+
bestStart = i;
|
|
6247
|
+
}
|
|
6248
|
+
}
|
|
6249
|
+
if (bestStart === -1 || bestScore < 0.5) return void 0;
|
|
6250
|
+
const snippet = fileLines.slice(bestStart, bestStart + Math.min(3, needleLines.length)).map((l) => l.length > 120 ? `${l.slice(0, 120)}\u2026` : l).join("\n");
|
|
6251
|
+
return { line: bestStart + 1, snippet };
|
|
6252
|
+
}
|
|
6253
|
+
function prefixSimilarity(a, b) {
|
|
6254
|
+
if (a === b) return 1;
|
|
6255
|
+
const max = Math.max(a.length, b.length);
|
|
6256
|
+
if (max === 0) return 1;
|
|
6257
|
+
let p = 0;
|
|
6258
|
+
const lim = Math.min(a.length, b.length);
|
|
6259
|
+
while (p < lim && a.charCodeAt(p) === b.charCodeAt(p)) p++;
|
|
6260
|
+
return p / max;
|
|
6261
|
+
}
|
|
6262
|
+
var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
6263
|
+
var MAX_CHECK_CHARS = 15e5;
|
|
6264
|
+
var MAX_ERRORS = 5;
|
|
6265
|
+
var tsLoad = null;
|
|
6266
|
+
function loadTypescript() {
|
|
6267
|
+
tsLoad ??= import('typescript').then(
|
|
6268
|
+
(m) => m.default ?? m,
|
|
6269
|
+
() => null
|
|
6270
|
+
);
|
|
6271
|
+
return tsLoad;
|
|
6272
|
+
}
|
|
6273
|
+
async function checkSyntax(filePath, content, previousContent) {
|
|
6274
|
+
const errors = await parseErrors(filePath, content);
|
|
6275
|
+
if (errors === void 0) return void 0;
|
|
6276
|
+
if (errors.length === 0) return { errors, preExisting: false };
|
|
6277
|
+
let preExisting = false;
|
|
6278
|
+
if (previousContent !== void 0) {
|
|
6279
|
+
const prevErrors = await parseErrors(filePath, previousContent);
|
|
6280
|
+
preExisting = prevErrors !== void 0 && prevErrors.length > 0;
|
|
6281
|
+
}
|
|
6282
|
+
return { errors, preExisting };
|
|
6283
|
+
}
|
|
6284
|
+
function isJsoncFile(filePath) {
|
|
6285
|
+
const base = path12.basename(filePath).toLowerCase();
|
|
6286
|
+
if (base.endsWith(".jsonc")) return true;
|
|
6287
|
+
if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
|
|
6288
|
+
const dir = path12.basename(path12.dirname(filePath)).toLowerCase();
|
|
6289
|
+
return dir === ".vscode";
|
|
6290
|
+
}
|
|
6291
|
+
async function parseErrors(filePath, content) {
|
|
6292
|
+
if (content.length > MAX_CHECK_CHARS) return void 0;
|
|
6293
|
+
const ext = path12.extname(filePath).toLowerCase();
|
|
6294
|
+
if (ext === ".json" || ext === ".jsonc") {
|
|
6295
|
+
try {
|
|
6296
|
+
JSON.parse(content);
|
|
6297
|
+
return [];
|
|
6298
|
+
} catch (err) {
|
|
6299
|
+
if (isJsoncFile(filePath)) {
|
|
6300
|
+
const ts3 = await loadTypescript();
|
|
6301
|
+
if (ts3) {
|
|
6302
|
+
const jsonc = ts3.parseConfigFileTextToJson(filePath, content);
|
|
6303
|
+
if (!jsonc.error) return [];
|
|
6304
|
+
return [formatDiag(ts3, jsonc.error, content)];
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
return [`JSON parse error: ${err.message}`];
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
if (!TS_LIKE.has(ext)) return void 0;
|
|
6311
|
+
const ts2 = await loadTypescript();
|
|
6312
|
+
if (!ts2) return void 0;
|
|
6313
|
+
const scriptKind = ext === ".tsx" ? ts2.ScriptKind.TSX : ext === ".ts" || ext === ".mts" || ext === ".cts" ? ts2.ScriptKind.TS : (
|
|
6314
|
+
// Plain JS may legitimately contain JSX; the JSX grammar is a
|
|
6315
|
+
// superset for untyped code, so parsing .js/.jsx as JSX avoids
|
|
6316
|
+
// false positives on React files.
|
|
6317
|
+
ts2.ScriptKind.JSX
|
|
6318
|
+
);
|
|
6319
|
+
const sourceFile = ts2.createSourceFile(
|
|
6320
|
+
path12.basename(filePath),
|
|
6321
|
+
content,
|
|
6322
|
+
ts2.ScriptTarget.Latest,
|
|
6323
|
+
/* setParentNodes */
|
|
6324
|
+
false,
|
|
6325
|
+
scriptKind
|
|
6326
|
+
);
|
|
6327
|
+
const diags = sourceFile.parseDiagnostics ?? [];
|
|
6328
|
+
return diags.slice(0, MAX_ERRORS).map((d) => formatDiag(ts2, d, content, sourceFile));
|
|
6329
|
+
}
|
|
6330
|
+
function formatDiag(ts2, diag, content, sourceFile) {
|
|
6331
|
+
const message = ts2.flattenDiagnosticMessageText(diag.messageText, " ");
|
|
6332
|
+
if (diag.start === void 0) return message;
|
|
6333
|
+
let line;
|
|
6334
|
+
if (sourceFile) {
|
|
6335
|
+
line = sourceFile.getLineAndCharacterOfPosition(diag.start).line + 1;
|
|
6336
|
+
} else {
|
|
6337
|
+
line = 1;
|
|
6338
|
+
for (let i = 0; i < diag.start && i < content.length; i++) {
|
|
6339
|
+
if (content.charCodeAt(i) === 10) line++;
|
|
6340
|
+
}
|
|
6341
|
+
}
|
|
6342
|
+
return `line ${line}: ${message}`;
|
|
6343
|
+
}
|
|
6344
|
+
|
|
6345
|
+
// src/edit.ts
|
|
6010
6346
|
var editTool = {
|
|
6011
6347
|
name: "edit",
|
|
6012
6348
|
category: "Filesystem",
|
|
6013
6349
|
description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. Fails safely if the `old_string` appears more than once unless `replace_all` is set.",
|
|
6014
|
-
usageHint: "RECOMMENDED WORKFLOW:\n1. Prefer calling `read` on the target file first when planning an edit.\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nIf no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.",
|
|
6350
|
+
usageHint: "RECOMMENDED WORKFLOW:\n1. Prefer calling `read` on the target file first when planning an edit.\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nIf no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.\nIf `old_string` differs from the file only in whitespace (trailing spaces, indentation), a lower-confidence fallback match is applied and reported in `matched_by`/`note` \u2014 always verify the diff when this happens.\nAfter the edit, TS/JS/JSON files are syntax-checked; if `syntax_errors` is present in the output, fix those errors immediately with a follow-up edit.",
|
|
6351
|
+
selection: {
|
|
6352
|
+
doNotUseWhen: "creating a new file, replacing the whole file, or applying an existing unified diff.",
|
|
6353
|
+
useInstead: ["write", "patch"]
|
|
6354
|
+
},
|
|
6015
6355
|
permission: "confirm",
|
|
6016
6356
|
mutating: true,
|
|
6017
6357
|
capabilities: ["fs.write"],
|
|
@@ -6027,7 +6367,7 @@ var editTool = {
|
|
|
6027
6367
|
},
|
|
6028
6368
|
required: ["path", "old_string", "new_string"]
|
|
6029
6369
|
},
|
|
6030
|
-
async execute(input, ctx) {
|
|
6370
|
+
async execute(input, ctx, opts) {
|
|
6031
6371
|
if (!input?.path) {
|
|
6032
6372
|
throw new ToolValidationError({ message: "edit: path is required", field: "path" });
|
|
6033
6373
|
}
|
|
@@ -6050,7 +6390,7 @@ var editTool = {
|
|
|
6050
6390
|
});
|
|
6051
6391
|
}
|
|
6052
6392
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
6053
|
-
const
|
|
6393
|
+
const stat12 = await fs2.stat(absPath).catch((err) => {
|
|
6054
6394
|
if (err.code === "ENOENT") {
|
|
6055
6395
|
throw new ToolValidationError({
|
|
6056
6396
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
@@ -6060,7 +6400,7 @@ var editTool = {
|
|
|
6060
6400
|
}
|
|
6061
6401
|
throw err;
|
|
6062
6402
|
});
|
|
6063
|
-
if (!
|
|
6403
|
+
if (!stat12.isFile()) {
|
|
6064
6404
|
throw new ToolValidationError({
|
|
6065
6405
|
message: `edit: "${input.path}" is not a regular file`,
|
|
6066
6406
|
field: "path"
|
|
@@ -6070,15 +6410,27 @@ var editTool = {
|
|
|
6070
6410
|
const original = await fs2.readFile(absPath, "utf8");
|
|
6071
6411
|
const updated = await fs2.stat(absPath);
|
|
6072
6412
|
const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
|
|
6073
|
-
const
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6413
|
+
const originalHash = sha256hex(original);
|
|
6414
|
+
const lastReadHash = ctx.lastReadHash?.(absPath);
|
|
6415
|
+
if (lastReadHash !== void 0) {
|
|
6416
|
+
if (lastReadHash !== originalHash) {
|
|
6417
|
+
throw new ToolValidationError({
|
|
6418
|
+
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
6419
|
+
field: "path",
|
|
6420
|
+
context: { reason: "external_modification" }
|
|
6421
|
+
});
|
|
6422
|
+
}
|
|
6423
|
+
} else {
|
|
6424
|
+
const lastReadMtime = ctx.lastReadMtime(absPath);
|
|
6425
|
+
if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
|
|
6426
|
+
throw new ToolValidationError({
|
|
6427
|
+
message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
|
|
6428
|
+
field: "path",
|
|
6429
|
+
context: { reason: "external_modification" }
|
|
6430
|
+
});
|
|
6431
|
+
}
|
|
6080
6432
|
}
|
|
6081
|
-
if (autoRead && updated.mtimeMs >
|
|
6433
|
+
if (autoRead && updated.mtimeMs > stat12.mtimeMs + mtimeTolerance) {
|
|
6082
6434
|
throw new ToolValidationError({
|
|
6083
6435
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
6084
6436
|
field: "path",
|
|
@@ -6091,42 +6443,78 @@ var editTool = {
|
|
|
6091
6443
|
const oldLf = normalizeToLf(input.old_string);
|
|
6092
6444
|
const newLf = normalizeToLf(input.new_string);
|
|
6093
6445
|
if (oldLf === newLf) {
|
|
6094
|
-
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs);
|
|
6446
|
+
if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
|
|
6095
6447
|
return {
|
|
6096
6448
|
path: absPath,
|
|
6097
6449
|
replacements: 0,
|
|
6098
6450
|
diff: "(no-op: old and new are identical)",
|
|
6451
|
+
matched_by: "exact",
|
|
6099
6452
|
note: autoReadNote
|
|
6100
6453
|
};
|
|
6101
6454
|
}
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
while (idx !== -1) {
|
|
6106
|
-
matches.push(idx);
|
|
6107
|
-
count++;
|
|
6108
|
-
idx = fileLf.indexOf(oldLf, idx + 1);
|
|
6109
|
-
}
|
|
6110
|
-
if (count === 0) {
|
|
6111
|
-
const hint = findSimilarity(fileLf, oldLf);
|
|
6455
|
+
const ladder = findLadderMatches(fileLf, oldLf);
|
|
6456
|
+
if (!ladder) {
|
|
6457
|
+
const hint = nearestMatchHint(fileLf, oldLf);
|
|
6112
6458
|
throw new ToolValidationError({
|
|
6113
|
-
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint}
|
|
6459
|
+
message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
|
|
6460
|
+
${hint.snippet}
|
|
6461
|
+
Compare this against your old_string and retry with the file's actual text.` : ""}`,
|
|
6114
6462
|
field: "old_string"
|
|
6115
6463
|
});
|
|
6116
6464
|
}
|
|
6465
|
+
const { tier, matches } = ladder;
|
|
6466
|
+
const count = matches.length;
|
|
6467
|
+
if (ladder.ambiguous) {
|
|
6468
|
+
const lines = matches.map((m) => m.startLine);
|
|
6469
|
+
throw new ToolValidationError({
|
|
6470
|
+
message: `edit: old_string only matched fuzzily and ${count} candidate blocks scored too close to distinguish (lines: ${lines.join(", ")}) in "${input.path}". Re-read the file and use the exact text of the intended block.`,
|
|
6471
|
+
field: "old_string",
|
|
6472
|
+
context: { occurrences: count, matchTier: tier }
|
|
6473
|
+
});
|
|
6474
|
+
}
|
|
6475
|
+
if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
|
|
6476
|
+
throw new ToolValidationError({
|
|
6477
|
+
message: `edit: old_string only matched via ${TIER_LABEL[tier]} in "${input.path}", but replace_all requires an exact (or trailing-whitespace) match. Re-read the file and use its exact text.`,
|
|
6478
|
+
field: "old_string",
|
|
6479
|
+
context: { matchTier: tier }
|
|
6480
|
+
});
|
|
6481
|
+
}
|
|
6117
6482
|
if (count > 1 && !input.replace_all) {
|
|
6118
|
-
const lines =
|
|
6483
|
+
const lines = matches.map((m) => m.startLine);
|
|
6119
6484
|
throw new ToolValidationError({
|
|
6120
|
-
message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")}). Add more context to make it unique, or set replace_all: true.`,
|
|
6485
|
+
message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")})${tier === "exact" ? "" : ` via ${TIER_LABEL[tier]}`}. Add more context to make it unique, or set replace_all: true.`,
|
|
6121
6486
|
field: "old_string",
|
|
6122
|
-
context: { occurrences: count }
|
|
6487
|
+
context: { occurrences: count, matchTier: tier }
|
|
6123
6488
|
});
|
|
6124
6489
|
}
|
|
6125
|
-
|
|
6490
|
+
let newFileLf;
|
|
6491
|
+
let tierNote;
|
|
6492
|
+
if (tier === "exact") {
|
|
6493
|
+
newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
|
|
6494
|
+
} else {
|
|
6495
|
+
let replacement = newLf;
|
|
6496
|
+
let indentSuffix = "";
|
|
6497
|
+
if (tier === "whitespace-normalized" || tier === "fuzzy") {
|
|
6498
|
+
const first = matches[0];
|
|
6499
|
+
const matchedText = fileLf.slice(first.start, first.end);
|
|
6500
|
+
const adjusted = adjustIndent(newLf, firstLineIndent(oldLf), firstLineIndent(matchedText));
|
|
6501
|
+
replacement = adjusted.text;
|
|
6502
|
+
if (adjusted.adjusted) indentSuffix = "; replacement re-indented to match the file";
|
|
6503
|
+
}
|
|
6504
|
+
newFileLf = fileLf;
|
|
6505
|
+
const applied = input.replace_all ? matches : matches.slice(0, 1);
|
|
6506
|
+
for (let i = applied.length - 1; i >= 0; i--) {
|
|
6507
|
+
const m = applied[i];
|
|
6508
|
+
newFileLf = newFileLf.slice(0, m.start) + replacement + newFileLf.slice(m.end);
|
|
6509
|
+
}
|
|
6510
|
+
const scoreSuffix = ladder.score !== void 0 ? `, similarity ${(ladder.score * 100).toFixed(1)}%` : "";
|
|
6511
|
+
tierNote = `old_string did not match exactly; applied via ${TIER_LABEL[tier]} at line ${matches[0].startLine} (confidence: ${TIER_CONFIDENCE[tier]}${scoreSuffix}${indentSuffix}). Review the diff to confirm the intended target was edited.`;
|
|
6512
|
+
}
|
|
6126
6513
|
const newFile = toStyle(newFileLf, style);
|
|
6514
|
+
opts?.signal?.throwIfAborted();
|
|
6127
6515
|
await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
|
|
6128
6516
|
const written = await fs2.stat(absPath);
|
|
6129
|
-
ctx.recordRead(absPath, written.mtimeMs, "write");
|
|
6517
|
+
ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
|
|
6130
6518
|
ctx.session.recordFileChange({
|
|
6131
6519
|
path: absPath,
|
|
6132
6520
|
action: "modified",
|
|
@@ -6137,38 +6525,22 @@ var editTool = {
|
|
|
6137
6525
|
fromFile: input.path,
|
|
6138
6526
|
toFile: input.path
|
|
6139
6527
|
});
|
|
6528
|
+
const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
|
|
6529
|
+
let syntaxNote;
|
|
6530
|
+
if (syntax && syntax.errors.length > 0) {
|
|
6531
|
+
syntaxNote = syntax.preExisting ? `Syntax check: the file still has parse errors (they pre-date this edit) \u2014 see syntax_errors.` : `Syntax check: this edit introduced ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`;
|
|
6532
|
+
}
|
|
6533
|
+
const notes = [autoReadNote, tierNote, syntaxNote].filter(Boolean);
|
|
6140
6534
|
return {
|
|
6141
6535
|
path: absPath,
|
|
6142
6536
|
replacements: input.replace_all ? count : 1,
|
|
6143
6537
|
diff,
|
|
6144
|
-
|
|
6538
|
+
matched_by: tier,
|
|
6539
|
+
syntax_errors: syntax && syntax.errors.length > 0 ? syntax.errors : void 0,
|
|
6540
|
+
note: notes.length > 0 ? notes.join("\n") : void 0
|
|
6145
6541
|
};
|
|
6146
6542
|
}
|
|
6147
6543
|
};
|
|
6148
|
-
function lineNumbersFor(text, indices) {
|
|
6149
|
-
const out = [];
|
|
6150
|
-
let pos = 0;
|
|
6151
|
-
let line = 1;
|
|
6152
|
-
for (const target of indices) {
|
|
6153
|
-
while (pos < target) {
|
|
6154
|
-
if (text.charCodeAt(pos) === 10) line++;
|
|
6155
|
-
pos++;
|
|
6156
|
-
}
|
|
6157
|
-
out.push(line);
|
|
6158
|
-
}
|
|
6159
|
-
return out;
|
|
6160
|
-
}
|
|
6161
|
-
function findSimilarity(haystack, needle) {
|
|
6162
|
-
if (needle.length < 20) return void 0;
|
|
6163
|
-
const probe = needle.slice(0, Math.min(40, needle.length));
|
|
6164
|
-
const idx = haystack.indexOf(probe);
|
|
6165
|
-
if (idx === -1) return void 0;
|
|
6166
|
-
let line = 1;
|
|
6167
|
-
for (let i = 0; i < idx; i++) {
|
|
6168
|
-
if (haystack.charCodeAt(i) === 10) line++;
|
|
6169
|
-
}
|
|
6170
|
-
return line;
|
|
6171
|
-
}
|
|
6172
6544
|
|
|
6173
6545
|
// src/_danger-detect.ts
|
|
6174
6546
|
var argHas = (args, value) => args.includes(value);
|
|
@@ -6381,23 +6753,18 @@ var RULES = [
|
|
|
6381
6753
|
},
|
|
6382
6754
|
reason: "inline script evaluation (-c / -e / --eval)"
|
|
6383
6755
|
},
|
|
6384
|
-
// ----- pipe-to-shell (
|
|
6756
|
+
// ----- pipe-to-shell (destructive — download-and-run pattern) -----
|
|
6385
6757
|
// The classic `curl https://... | sh` download-and-run vector. Detected by
|
|
6386
|
-
// looking for a known fetcher
|
|
6387
|
-
//
|
|
6388
|
-
//
|
|
6758
|
+
// looking for a known fetcher piped into a shell or expression evaluator.
|
|
6759
|
+
// A shell command passed through `bash -c` / `pwsh -Command` arrives as one
|
|
6760
|
+
// argv string, so scan the reconstructed argv text rather than individual
|
|
6761
|
+
// tokens only.
|
|
6389
6762
|
{
|
|
6390
6763
|
id: "pipe-to-shell",
|
|
6391
|
-
level: "
|
|
6392
|
-
test: (
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
);
|
|
6396
|
-
const hasShellSink = args.some(
|
|
6397
|
-
(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")
|
|
6398
|
-
);
|
|
6399
|
-
return hasFetcher && hasShellSink;
|
|
6400
|
-
},
|
|
6764
|
+
level: "destructive",
|
|
6765
|
+
test: (cmd, args) => /\b(?:curl|wget|fetch|httpie|http|irm|iwr|Invoke-WebRequest|Invoke-RestMethod)\b[\s\S]{0,300}\|\s*(?:sudo\s+)?(?:sh|bash|zsh|fish|pwsh|powershell|iex|Invoke-Expression)\b/i.test(
|
|
6766
|
+
[cmd, ...args].join(" ")
|
|
6767
|
+
),
|
|
6401
6768
|
reason: "network fetch piped to a shell (download-and-run pattern)"
|
|
6402
6769
|
},
|
|
6403
6770
|
// ----- privilege escalation (caution) -----
|
|
@@ -7159,6 +7526,10 @@ var execTool = {
|
|
|
7159
7526
|
category: "Shell",
|
|
7160
7527
|
description: "Execute a **whitelisted, restricted set of commands** with strict argument validation. This is the **preferred and safer** alternative to the `bash` tool for running development tools (node, npm, pnpm, tsc, git, tests, linters, etc.). It prevents arbitrary command injection and limits what the model can do.",
|
|
7161
7528
|
usageHint: "PREFERRED SHELL TOOL for most cases.\n\nUse this instead of `bash` whenever possible.\n- `command` must be in the allowlist. Defaults cover JS (node/npm/pnpm/yarn/bun/deno/tsc/vitest/eslint/biome), Go (`go build`/`go test`), Rust (cargo), Python (python/pip), Ruby (gem/bundle), JVM (java/mvn/gradle), .NET (dotnet), native (make/cmake), and git. Users can extend it via `tools.exec.allow` in config.\n- Arguments are passed as a clean array (no shell interpretation).\n- `cwd` is validated to stay inside the project.\n- If a command is not allowlisted, the error explains how to add it; for one-off arbitrary commands, fall back to `bash` (with strong justification).\nThis tool significantly reduces the risk compared to full shell access.",
|
|
7529
|
+
selection: {
|
|
7530
|
+
doNotUseWhen: "the operation requires pipes, redirection, shell expansion, or a non-allowlisted command.",
|
|
7531
|
+
useInstead: ["bash"]
|
|
7532
|
+
},
|
|
7162
7533
|
permission: "confirm",
|
|
7163
7534
|
mutating: true,
|
|
7164
7535
|
riskTier: "standard",
|
|
@@ -7822,13 +8193,13 @@ var formatTool = {
|
|
|
7822
8193
|
}
|
|
7823
8194
|
};
|
|
7824
8195
|
async function detectFixer(cwd) {
|
|
7825
|
-
const { stat:
|
|
8196
|
+
const { stat: stat12 } = await import('node:fs/promises');
|
|
7826
8197
|
try {
|
|
7827
|
-
await
|
|
8198
|
+
await stat12(`${cwd}/biome.json`);
|
|
7828
8199
|
return "biome";
|
|
7829
8200
|
} catch {
|
|
7830
8201
|
try {
|
|
7831
|
-
await
|
|
8202
|
+
await stat12(`${cwd}/.prettierrc`);
|
|
7832
8203
|
return "prettier";
|
|
7833
8204
|
} catch {
|
|
7834
8205
|
return "biome";
|
|
@@ -7989,8 +8360,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
7989
8360
|
let dir = cwd;
|
|
7990
8361
|
for (let i = 0; i < 20; i++) {
|
|
7991
8362
|
try {
|
|
7992
|
-
const
|
|
7993
|
-
if (
|
|
8363
|
+
const stat12 = statSync(`${dir}/.git`);
|
|
8364
|
+
if (stat12.isDirectory() || stat12.isFile()) return dir;
|
|
7994
8365
|
} catch {
|
|
7995
8366
|
}
|
|
7996
8367
|
if (dir === root) break;
|
|
@@ -8137,6 +8508,10 @@ var globTool = {
|
|
|
8137
8508
|
category: "Filesystem",
|
|
8138
8509
|
description: "Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.",
|
|
8139
8510
|
usageHint: "RECOMMENDED FOR SCOPING SEARCHES:\n\n- Use early to get a list of files you actually care about.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
|
|
8511
|
+
selection: {
|
|
8512
|
+
doNotUseWhen: "you need to search inside file contents.",
|
|
8513
|
+
useInstead: ["grep"]
|
|
8514
|
+
},
|
|
8140
8515
|
permission: "auto",
|
|
8141
8516
|
mutating: false,
|
|
8142
8517
|
capabilities: ["fs.read"],
|
|
@@ -8161,8 +8536,9 @@ var globTool = {
|
|
|
8161
8536
|
},
|
|
8162
8537
|
required: ["pattern"]
|
|
8163
8538
|
},
|
|
8164
|
-
async execute(input, ctx) {
|
|
8539
|
+
async execute(input, ctx, opts) {
|
|
8165
8540
|
if (!input?.pattern) throw new Error("glob: pattern is required");
|
|
8541
|
+
const signal = opts?.signal;
|
|
8166
8542
|
const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
|
|
8167
8543
|
const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
|
|
8168
8544
|
const ignored = await readGitignore(base);
|
|
@@ -8186,6 +8562,10 @@ var globTool = {
|
|
|
8186
8562
|
}
|
|
8187
8563
|
};
|
|
8188
8564
|
const walk = async (dir, relPrefix) => {
|
|
8565
|
+
if (signal?.aborted) {
|
|
8566
|
+
truncated = true;
|
|
8567
|
+
return;
|
|
8568
|
+
}
|
|
8189
8569
|
if (results.length >= limit) {
|
|
8190
8570
|
truncated = true;
|
|
8191
8571
|
return;
|
|
@@ -8203,7 +8583,7 @@ var globTool = {
|
|
|
8203
8583
|
if (DEFAULT_IGNORE2.includes(name)) continue;
|
|
8204
8584
|
if (ignored.includes(name)) continue;
|
|
8205
8585
|
const rel = relPrefix ? `${relPrefix}/${name}` : name;
|
|
8206
|
-
const full =
|
|
8586
|
+
const full = path12.join(dir, name);
|
|
8207
8587
|
if (e.isDirectory()) {
|
|
8208
8588
|
subdirs.push({ full, rel });
|
|
8209
8589
|
} else if (e.isFile()) {
|
|
@@ -8247,7 +8627,7 @@ var globTool = {
|
|
|
8247
8627
|
};
|
|
8248
8628
|
async function readGitignore(dir) {
|
|
8249
8629
|
try {
|
|
8250
|
-
const raw = await fs2.readFile(
|
|
8630
|
+
const raw = await fs2.readFile(path12.join(dir, ".gitignore"), "utf8");
|
|
8251
8631
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
8252
8632
|
} catch {
|
|
8253
8633
|
return [];
|
|
@@ -8309,6 +8689,10 @@ var grepTool = {
|
|
|
8309
8689
|
category: "Search",
|
|
8310
8690
|
description: "Search across files using a regular expression. This is one of the primary code search tools. Prefers ripgrep for speed and features when available.",
|
|
8311
8691
|
usageHint: 'POWERFUL CODE SEARCH TOOL:\n\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
|
|
8692
|
+
selection: {
|
|
8693
|
+
doNotUseWhen: "you only need to locate files by name or path pattern.",
|
|
8694
|
+
useInstead: ["glob"]
|
|
8695
|
+
},
|
|
8312
8696
|
permission: "auto",
|
|
8313
8697
|
mutating: false,
|
|
8314
8698
|
capabilities: ["fs.read"],
|
|
@@ -8548,8 +8932,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
8548
8932
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
8549
8933
|
if (globRe) globRe.lastIndex = 0;
|
|
8550
8934
|
try {
|
|
8551
|
-
const
|
|
8552
|
-
if (!
|
|
8935
|
+
const stat12 = await fs2.stat(full);
|
|
8936
|
+
if (!stat12.isFile() || stat12.size > maxBytes || stopped || signal.aborted) return;
|
|
8553
8937
|
const file = await fs2.open(full, "r");
|
|
8554
8938
|
try {
|
|
8555
8939
|
let bytesReadTotal = 0;
|
|
@@ -8639,7 +9023,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
8639
9023
|
if (stopped) return;
|
|
8640
9024
|
if (DEFAULT_IGNORE3.includes(e.name)) continue;
|
|
8641
9025
|
if (e.isSymbolicLink()) continue;
|
|
8642
|
-
const full =
|
|
9026
|
+
const full = path12.join(dir, e.name);
|
|
8643
9027
|
if (e.isDirectory()) {
|
|
8644
9028
|
subdirs.push(full);
|
|
8645
9029
|
} else if (e.isFile()) {
|
|
@@ -9186,56 +9570,56 @@ function jmespathSearch(data, query) {
|
|
|
9186
9570
|
}
|
|
9187
9571
|
function validateJsonSchema(data, schema) {
|
|
9188
9572
|
const errors = [];
|
|
9189
|
-
function check(value, s,
|
|
9573
|
+
function check(value, s, path23) {
|
|
9190
9574
|
if (s["type"]) {
|
|
9191
9575
|
const expectedType = s["type"];
|
|
9192
9576
|
const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
9193
9577
|
if (expectedType === "integer") {
|
|
9194
|
-
if (!Number.isInteger(value)) errors.push(`${
|
|
9578
|
+
if (!Number.isInteger(value)) errors.push(`${path23}: expected integer, got ${actualType}`);
|
|
9195
9579
|
} else if (expectedType !== actualType) {
|
|
9196
|
-
errors.push(`${
|
|
9580
|
+
errors.push(`${path23}: expected ${expectedType}, got ${actualType}`);
|
|
9197
9581
|
}
|
|
9198
9582
|
}
|
|
9199
9583
|
if (typeof value === "string" && s["format"] === "uri" && value) {
|
|
9200
9584
|
try {
|
|
9201
9585
|
new URL(value);
|
|
9202
9586
|
} catch {
|
|
9203
|
-
errors.push(`${
|
|
9587
|
+
errors.push(`${path23}: not a valid URI`);
|
|
9204
9588
|
}
|
|
9205
9589
|
}
|
|
9206
9590
|
if (typeof value === "string" && s["pattern"]) {
|
|
9207
9591
|
const re = new RegExp(s["pattern"]);
|
|
9208
|
-
if (!re.test(value)) errors.push(`${
|
|
9592
|
+
if (!re.test(value)) errors.push(`${path23}: does not match pattern ${s["pattern"]}`);
|
|
9209
9593
|
}
|
|
9210
9594
|
if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
|
|
9211
|
-
errors.push(`${
|
|
9595
|
+
errors.push(`${path23}: string too short (min ${s["minLength"]})`);
|
|
9212
9596
|
}
|
|
9213
9597
|
if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
|
|
9214
|
-
errors.push(`${
|
|
9598
|
+
errors.push(`${path23}: string too long (max ${s["maxLength"]})`);
|
|
9215
9599
|
}
|
|
9216
9600
|
if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
|
|
9217
|
-
errors.push(`${
|
|
9601
|
+
errors.push(`${path23}: below minimum ${s["minimum"]}`);
|
|
9218
9602
|
}
|
|
9219
9603
|
if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
|
|
9220
|
-
errors.push(`${
|
|
9604
|
+
errors.push(`${path23}: above maximum ${s["maximum"]}`);
|
|
9221
9605
|
}
|
|
9222
9606
|
if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
|
|
9223
9607
|
for (let i = 0; i < value.length; i++) {
|
|
9224
|
-
check(value[i], s["items"], `${
|
|
9608
|
+
check(value[i], s["items"], `${path23}[${i}]`);
|
|
9225
9609
|
}
|
|
9226
9610
|
}
|
|
9227
9611
|
if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
|
|
9228
9612
|
const props = s["properties"];
|
|
9229
9613
|
for (const [k, propSchema] of Object.entries(props)) {
|
|
9230
|
-
check(value[k], propSchema, `${
|
|
9614
|
+
check(value[k], propSchema, `${path23}.${k}`);
|
|
9231
9615
|
}
|
|
9232
9616
|
}
|
|
9233
9617
|
}
|
|
9234
9618
|
check(data, schema, "$");
|
|
9235
9619
|
return { valid: errors.length === 0, errors };
|
|
9236
9620
|
}
|
|
9237
|
-
function simpleQuery(data,
|
|
9238
|
-
const parts =
|
|
9621
|
+
function simpleQuery(data, path23) {
|
|
9622
|
+
const parts = path23.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
9239
9623
|
let current = data;
|
|
9240
9624
|
for (const part of parts) {
|
|
9241
9625
|
if (current === null || current === void 0) return void 0;
|
|
@@ -10160,11 +10544,11 @@ var lintTool = {
|
|
|
10160
10544
|
}
|
|
10161
10545
|
};
|
|
10162
10546
|
async function detectLinter(cwd) {
|
|
10163
|
-
const { stat:
|
|
10547
|
+
const { stat: stat12 } = await import('node:fs/promises');
|
|
10164
10548
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
10165
10549
|
for (const f of checks) {
|
|
10166
10550
|
try {
|
|
10167
|
-
await
|
|
10551
|
+
await stat12(`${cwd}/${f}`);
|
|
10168
10552
|
if (f.includes("biome")) return "biome";
|
|
10169
10553
|
if (f.includes("eslint")) return "eslint";
|
|
10170
10554
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -10306,7 +10690,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
|
|
|
10306
10690
|
}
|
|
10307
10691
|
var DOCKER_LOGS_TIMEOUT_MS = 3e3;
|
|
10308
10692
|
var MAX_TAIL_LINES = 1e5;
|
|
10309
|
-
async function fileLogs(
|
|
10693
|
+
async function fileLogs(path23, lines, filterRe, stream) {
|
|
10310
10694
|
const { createInterface } = await import('node:readline');
|
|
10311
10695
|
const { createReadStream } = await import('node:fs');
|
|
10312
10696
|
const entries = [];
|
|
@@ -10315,7 +10699,7 @@ async function fileLogs(path22, lines, filterRe, stream) {
|
|
|
10315
10699
|
let writeIdx = 0;
|
|
10316
10700
|
let totalLines = 0;
|
|
10317
10701
|
const rl = createInterface({
|
|
10318
|
-
input: createReadStream(
|
|
10702
|
+
input: createReadStream(path23),
|
|
10319
10703
|
crlfDelay: Number.POSITIVE_INFINITY
|
|
10320
10704
|
});
|
|
10321
10705
|
for await (const line of rl) {
|
|
@@ -10336,7 +10720,7 @@ async function fileLogs(path22, lines, filterRe, stream) {
|
|
|
10336
10720
|
if (parsed) entries.push(parsed);
|
|
10337
10721
|
}
|
|
10338
10722
|
return {
|
|
10339
|
-
source:
|
|
10723
|
+
source: path23,
|
|
10340
10724
|
entries,
|
|
10341
10725
|
total: entries.length,
|
|
10342
10726
|
truncated: totalLines > effLines,
|
|
@@ -10503,6 +10887,10 @@ var patchTool = {
|
|
|
10503
10887
|
category: "Filesystem",
|
|
10504
10888
|
description: "Apply a unified diff (patch) to the project. This is the correct tool when you have a diff that needs to be applied precisely, including handling of rejects.",
|
|
10505
10889
|
usageHint: "Best used when you already have a diff (from generation, external source, or previous step).\n- Use `dry_run: true` to see what would happen without modifying files.\n- On failure it creates .rej and .orig files for manual review.\nOften cleaner than many small `edit` operations for larger changes.",
|
|
10890
|
+
selection: {
|
|
10891
|
+
doNotUseWhen: "you do not already have a unified diff or only need one precise replacement.",
|
|
10892
|
+
useInstead: ["edit"]
|
|
10893
|
+
},
|
|
10506
10894
|
permission: "confirm",
|
|
10507
10895
|
mutating: true,
|
|
10508
10896
|
capabilities: ["fs.write"],
|
|
@@ -10524,12 +10912,13 @@ var patchTool = {
|
|
|
10524
10912
|
const strip = Math.max(1, input.strip ?? 1);
|
|
10525
10913
|
const dryRun = input.dry_run ?? false;
|
|
10526
10914
|
const targets = extractDiffTargets(input.patch);
|
|
10915
|
+
const resolvedTargets = [];
|
|
10527
10916
|
for (const t of targets) {
|
|
10528
10917
|
const stripped = stripPathComponents(t, strip);
|
|
10529
10918
|
if (!stripped) continue;
|
|
10530
|
-
const candidate =
|
|
10531
|
-
const rel =
|
|
10532
|
-
if (rel.startsWith("..") ||
|
|
10919
|
+
const candidate = path12.resolve(dir, stripped);
|
|
10920
|
+
const rel = path12.relative(ctx.projectRoot, candidate);
|
|
10921
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) {
|
|
10533
10922
|
return {
|
|
10534
10923
|
applied: 0,
|
|
10535
10924
|
rejected: 1,
|
|
@@ -10538,12 +10927,19 @@ var patchTool = {
|
|
|
10538
10927
|
message: `patch refused: target "${t}" resolves outside project root`
|
|
10539
10928
|
};
|
|
10540
10929
|
}
|
|
10930
|
+
resolvedTargets.push(candidate);
|
|
10931
|
+
}
|
|
10932
|
+
const beforeContents = /* @__PURE__ */ new Map();
|
|
10933
|
+
if (!dryRun) {
|
|
10934
|
+
for (const target of resolvedTargets) {
|
|
10935
|
+
beforeContents.set(target, await readTextForTracking(target));
|
|
10936
|
+
}
|
|
10541
10937
|
}
|
|
10542
|
-
const tmpDir = await fs2.mkdtemp(
|
|
10938
|
+
const tmpDir = await fs2.mkdtemp(path12.join(os2.tmpdir(), ".wstack_patch_"));
|
|
10543
10939
|
try {
|
|
10544
10940
|
await fs2.chmod(tmpDir, 448).catch(() => {
|
|
10545
10941
|
});
|
|
10546
|
-
const patchFile =
|
|
10942
|
+
const patchFile = path12.join(tmpDir, "in.diff");
|
|
10547
10943
|
await fs2.writeFile(patchFile, input.patch, { mode: 384 });
|
|
10548
10944
|
const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
|
|
10549
10945
|
const result = await runPatch(args, dir, opts.signal);
|
|
@@ -10557,6 +10953,21 @@ var patchTool = {
|
|
|
10557
10953
|
};
|
|
10558
10954
|
}
|
|
10559
10955
|
const patched = extractPatchedFiles(result.stdout);
|
|
10956
|
+
if (!dryRun) {
|
|
10957
|
+
for (const target of resolvedTargets) {
|
|
10958
|
+
const before = beforeContents.get(target) ?? null;
|
|
10959
|
+
const after = await readTextForTracking(target);
|
|
10960
|
+
if (after === null || after === before) continue;
|
|
10961
|
+
const stat12 = await fs2.stat(target).catch(() => null);
|
|
10962
|
+
if (stat12) ctx.recordRead?.(target, stat12.mtimeMs, "write", sha256hex(after));
|
|
10963
|
+
ctx.session?.recordFileChange?.({
|
|
10964
|
+
path: target,
|
|
10965
|
+
action: before === null ? "created" : "modified",
|
|
10966
|
+
before,
|
|
10967
|
+
after
|
|
10968
|
+
});
|
|
10969
|
+
}
|
|
10970
|
+
}
|
|
10560
10971
|
return {
|
|
10561
10972
|
applied: patched.length,
|
|
10562
10973
|
rejected: 0,
|
|
@@ -10570,6 +10981,18 @@ var patchTool = {
|
|
|
10570
10981
|
}
|
|
10571
10982
|
}
|
|
10572
10983
|
};
|
|
10984
|
+
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
10985
|
+
async function readTextForTracking(absPath) {
|
|
10986
|
+
try {
|
|
10987
|
+
const stat12 = await fs2.stat(absPath);
|
|
10988
|
+
if (!stat12.isFile() || stat12.size > MAX_TRACKING_BYTES) return null;
|
|
10989
|
+
const buf = await fs2.readFile(absPath);
|
|
10990
|
+
if (buf.includes(0)) return null;
|
|
10991
|
+
return buf.toString("utf8");
|
|
10992
|
+
} catch {
|
|
10993
|
+
return null;
|
|
10994
|
+
}
|
|
10995
|
+
}
|
|
10573
10996
|
function extractDiffTargets(patch) {
|
|
10574
10997
|
const out = [];
|
|
10575
10998
|
const re = /^\+\+\+\s+([^\t\r\n]+)/gm;
|
|
@@ -10877,6 +11300,10 @@ var readTool = {
|
|
|
10877
11300
|
category: "Filesystem",
|
|
10878
11301
|
description: "Read the contents of a file with line numbers. This is the primary way to inspect source code, configuration, or any text file before making changes. Lines are returned 1-indexed with a ` N| ` prefix for easy reference in edits.",
|
|
10879
11302
|
usageHint: "FOUNDATIONAL TOOL \u2014 call this before almost any edit operation.\n\nBest practices:\n- Always read a file before using `edit`, `replace`, or `write` on it (the system often requires it for safety).\n- Use `offset` + `limit` for very large files instead of reading everything at once.\n- Default limit is generous (2000 lines) but can be increased.\n- The output format is designed to be directly usable as context for `edit` operations.",
|
|
11303
|
+
selection: {
|
|
11304
|
+
doNotUseWhen: "you need to search many files for matching content.",
|
|
11305
|
+
useInstead: ["grep"]
|
|
11306
|
+
},
|
|
10880
11307
|
permission: "auto",
|
|
10881
11308
|
mutating: false,
|
|
10882
11309
|
capabilities: ["fs.read"],
|
|
@@ -10914,9 +11341,9 @@ var readTool = {
|
|
|
10914
11341
|
});
|
|
10915
11342
|
}
|
|
10916
11343
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
10917
|
-
let
|
|
11344
|
+
let stat12;
|
|
10918
11345
|
try {
|
|
10919
|
-
|
|
11346
|
+
stat12 = await fs2.stat(absPath);
|
|
10920
11347
|
} catch (err) {
|
|
10921
11348
|
const code = err.code;
|
|
10922
11349
|
if (code === "ENOENT") {
|
|
@@ -10935,7 +11362,7 @@ var readTool = {
|
|
|
10935
11362
|
cause: err
|
|
10936
11363
|
});
|
|
10937
11364
|
}
|
|
10938
|
-
if (!
|
|
11365
|
+
if (!stat12.isFile()) {
|
|
10939
11366
|
throw new FsError({
|
|
10940
11367
|
message: `read: "${input.path}" is not a regular file`,
|
|
10941
11368
|
code: "FS_READ_FAILED",
|
|
@@ -10943,22 +11370,22 @@ var readTool = {
|
|
|
10943
11370
|
context: { reason: "not-a-regular-file" }
|
|
10944
11371
|
});
|
|
10945
11372
|
}
|
|
10946
|
-
if (
|
|
11373
|
+
if (stat12.size > MAX_BYTES2) {
|
|
10947
11374
|
throw new FsError({
|
|
10948
|
-
message: `read: file too large (${
|
|
11375
|
+
message: `read: file too large (${stat12.size} bytes, limit ${MAX_BYTES2})`,
|
|
10949
11376
|
code: "FS_READ_FAILED",
|
|
10950
11377
|
path: absPath,
|
|
10951
|
-
context: { size:
|
|
11378
|
+
context: { size: stat12.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
10952
11379
|
});
|
|
10953
11380
|
}
|
|
10954
11381
|
const offset = Math.max(1, input.offset ?? 1);
|
|
10955
11382
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
10956
11383
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
10957
11384
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
10958
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
10959
|
-
ctx.recordRead(absPath,
|
|
11385
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat12.mtimeMs, offset, requestedEnd)) {
|
|
11386
|
+
ctx.recordRead(absPath, stat12.mtimeMs);
|
|
10960
11387
|
return {
|
|
10961
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
11388
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat12.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
10962
11389
|
total_lines: prior.totalLines,
|
|
10963
11390
|
encoding: "utf8",
|
|
10964
11391
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -10971,13 +11398,14 @@ var readTool = {
|
|
|
10971
11398
|
throw new Error(`read: "${input.path}" appears to be binary`);
|
|
10972
11399
|
}
|
|
10973
11400
|
const text = buf.toString("utf8");
|
|
11401
|
+
const contentHash = sha256hex(text);
|
|
10974
11402
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
10975
11403
|
const total = allLines.length;
|
|
10976
11404
|
if (input.mode === "summary") {
|
|
10977
|
-
ctx.recordRead(absPath,
|
|
10978
|
-
rememberReadRange(ctx, absPath,
|
|
11405
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "user", contentHash);
|
|
11406
|
+
rememberReadRange(ctx, absPath, stat12.mtimeMs, total, 1, Math.min(total, 200));
|
|
10979
11407
|
return {
|
|
10980
|
-
text: summarizeFile(input.path,
|
|
11408
|
+
text: summarizeFile(input.path, stat12.size, allLines),
|
|
10981
11409
|
total_lines: total,
|
|
10982
11410
|
encoding: "utf8",
|
|
10983
11411
|
truncated: total > 200,
|
|
@@ -10985,13 +11413,13 @@ var readTool = {
|
|
|
10985
11413
|
};
|
|
10986
11414
|
}
|
|
10987
11415
|
if (limit === 0) {
|
|
10988
|
-
ctx.recordRead(absPath,
|
|
10989
|
-
rememberReadRange(ctx, absPath,
|
|
11416
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "user", contentHash);
|
|
11417
|
+
rememberReadRange(ctx, absPath, stat12.mtimeMs, total, 1, 0);
|
|
10990
11418
|
return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
|
|
10991
11419
|
}
|
|
10992
11420
|
if (offset > total) {
|
|
10993
|
-
ctx.recordRead(absPath,
|
|
10994
|
-
rememberReadRange(ctx, absPath,
|
|
11421
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "user", contentHash);
|
|
11422
|
+
rememberReadRange(ctx, absPath, stat12.mtimeMs, total, total + 1, total + 1);
|
|
10995
11423
|
return {
|
|
10996
11424
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
10997
11425
|
total_lines: total,
|
|
@@ -11003,8 +11431,8 @@ var readTool = {
|
|
|
11003
11431
|
const truncated = offset - 1 + slice.length < total;
|
|
11004
11432
|
const width = String(offset + slice.length - 1).length;
|
|
11005
11433
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
11006
|
-
ctx.recordRead(absPath,
|
|
11007
|
-
rememberReadRange(ctx, absPath,
|
|
11434
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "user", contentHash);
|
|
11435
|
+
rememberReadRange(ctx, absPath, stat12.mtimeMs, total, offset, offset + slice.length - 1);
|
|
11008
11436
|
return {
|
|
11009
11437
|
text: numbered,
|
|
11010
11438
|
total_lines: total,
|
|
@@ -11146,10 +11574,10 @@ var replaceTool = {
|
|
|
11146
11574
|
} catch {
|
|
11147
11575
|
continue;
|
|
11148
11576
|
}
|
|
11149
|
-
const rel =
|
|
11150
|
-
if (rel.startsWith("..") ||
|
|
11151
|
-
const
|
|
11152
|
-
if (!
|
|
11577
|
+
const rel = path12.relative(realRoot, realPath);
|
|
11578
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) continue;
|
|
11579
|
+
const stat12 = await fs2.stat(realPath).catch(() => null);
|
|
11580
|
+
if (!stat12?.isFile()) continue;
|
|
11153
11581
|
let content;
|
|
11154
11582
|
try {
|
|
11155
11583
|
const buf = await fs2.readFile(realPath);
|
|
@@ -11174,7 +11602,17 @@ var replaceTool = {
|
|
|
11174
11602
|
totalReplacements += count;
|
|
11175
11603
|
if (!dryRun) {
|
|
11176
11604
|
const newContent = toStyle(newContentLf, style);
|
|
11177
|
-
await atomicWrite(realPath, newContent, { mode:
|
|
11605
|
+
await atomicWrite(realPath, newContent, { mode: stat12.mode & 511 });
|
|
11606
|
+
const written = await fs2.stat(realPath).catch(() => null);
|
|
11607
|
+
if (written) {
|
|
11608
|
+
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
11609
|
+
}
|
|
11610
|
+
ctx.session?.recordFileChange?.({
|
|
11611
|
+
path: realPath,
|
|
11612
|
+
action: "modified",
|
|
11613
|
+
before: content,
|
|
11614
|
+
after: newContent
|
|
11615
|
+
});
|
|
11178
11616
|
}
|
|
11179
11617
|
const diff = dryRun || matches.length > 0 ? unifiedDiff(content, toStyle(newContentLf, style), {
|
|
11180
11618
|
fromFile: absPath,
|
|
@@ -11204,8 +11642,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
11204
11642
|
const resolved = [];
|
|
11205
11643
|
for (const p of parts) {
|
|
11206
11644
|
const absPath = safeResolve(p, ctx);
|
|
11207
|
-
const
|
|
11208
|
-
if (
|
|
11645
|
+
const stat12 = await fs2.stat(absPath).catch(() => null);
|
|
11646
|
+
if (stat12?.isFile()) {
|
|
11209
11647
|
resolved.push(absPath);
|
|
11210
11648
|
}
|
|
11211
11649
|
}
|
|
@@ -11266,10 +11704,10 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
11266
11704
|
}
|
|
11267
11705
|
for (const e of entries) {
|
|
11268
11706
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
11269
|
-
const full =
|
|
11707
|
+
const full = path12.join(dir, e.name);
|
|
11270
11708
|
try {
|
|
11271
|
-
const
|
|
11272
|
-
if (
|
|
11709
|
+
const stat12 = await fs2.lstat(full);
|
|
11710
|
+
if (stat12.isSymbolicLink()) continue;
|
|
11273
11711
|
} catch {
|
|
11274
11712
|
continue;
|
|
11275
11713
|
}
|
|
@@ -11437,16 +11875,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
11437
11875
|
let filesCreated = 0;
|
|
11438
11876
|
for (const [filePath, content] of Object.entries(templateFiles)) {
|
|
11439
11877
|
const resolvedPath = substituteVars(filePath, name, vars);
|
|
11440
|
-
const joinedPath =
|
|
11441
|
-
const root =
|
|
11442
|
-
const target =
|
|
11443
|
-
const rel =
|
|
11444
|
-
if (rel.startsWith("..") ||
|
|
11878
|
+
const joinedPath = path12.join(cwd, resolvedPath);
|
|
11879
|
+
const root = path12.resolve(ctx.projectRoot);
|
|
11880
|
+
const target = path12.resolve(joinedPath);
|
|
11881
|
+
const rel = path12.relative(root, target);
|
|
11882
|
+
if (rel.startsWith("..") || path12.isAbsolute(rel)) {
|
|
11445
11883
|
throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
|
|
11446
11884
|
}
|
|
11447
11885
|
const fullPath = target;
|
|
11448
11886
|
if (!dryRun) {
|
|
11449
|
-
await fs2.mkdir(
|
|
11887
|
+
await fs2.mkdir(path12.dirname(fullPath), { recursive: true });
|
|
11450
11888
|
await atomicWrite(fullPath, substituteVars(content, name, vars));
|
|
11451
11889
|
}
|
|
11452
11890
|
files.push(resolvedPath);
|
|
@@ -12346,11 +12784,11 @@ var testTool = {
|
|
|
12346
12784
|
}
|
|
12347
12785
|
};
|
|
12348
12786
|
async function detectRunner(cwd) {
|
|
12349
|
-
const { stat:
|
|
12787
|
+
const { stat: stat12 } = await import('node:fs/promises');
|
|
12350
12788
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
12351
12789
|
for (const f of candidates) {
|
|
12352
12790
|
try {
|
|
12353
|
-
await
|
|
12791
|
+
await stat12(path12.join(cwd, f));
|
|
12354
12792
|
if (f.includes("vitest")) return "vitest";
|
|
12355
12793
|
if (f.includes("jest")) return "jest";
|
|
12356
12794
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -12978,7 +13416,7 @@ async function walkDir(dir, depth, opts) {
|
|
|
12978
13416
|
opts.lines.push(opts.prefix + branch + displayName);
|
|
12979
13417
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
12980
13418
|
const childPrefix = opts.prefix + connector;
|
|
12981
|
-
await walkDir(
|
|
13419
|
+
await walkDir(path12.join(dir, entry.name), depth + 1, {
|
|
12982
13420
|
...opts,
|
|
12983
13421
|
prefix: childPrefix,
|
|
12984
13422
|
isLast
|
|
@@ -13064,12 +13502,12 @@ var typecheckTool = {
|
|
|
13064
13502
|
}
|
|
13065
13503
|
};
|
|
13066
13504
|
async function findTsConfig(cwd) {
|
|
13067
|
-
const { stat:
|
|
13505
|
+
const { stat: stat12 } = await import('node:fs/promises');
|
|
13068
13506
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
13069
13507
|
for (const f of candidates) {
|
|
13070
13508
|
try {
|
|
13071
|
-
const s = await
|
|
13072
|
-
if (s.isFile()) return
|
|
13509
|
+
const s = await stat12(path12.join(cwd, f));
|
|
13510
|
+
if (s.isFile()) return path12.join(cwd, f);
|
|
13073
13511
|
} catch {
|
|
13074
13512
|
}
|
|
13075
13513
|
}
|
|
@@ -13080,6 +13518,10 @@ var writeTool = {
|
|
|
13080
13518
|
category: "Filesystem",
|
|
13081
13519
|
description: "Write or completely overwrite a file on disk. This is a high-privilege operation. For modifying existing files, you should almost always prefer the `edit` tool instead, because `edit` is safer and works on the last-read version of the file.",
|
|
13082
13520
|
usageHint: "RULES FOR CORRECT USAGE:\n- Use `write` primarily for **new files** or when you want to replace the entire content.\n- For any existing file, strongly prefer `edit` (it requires a prior `read` in the same session and is more precise).\n- You MUST have called `read` on the file earlier in the conversation before using `write` on an existing path (the system enforces this for safety).\n- The path is resolved relative to the project root and protected against escaping the workspace.",
|
|
13521
|
+
selection: {
|
|
13522
|
+
doNotUseWhen: "making a precise change to part of an existing file.",
|
|
13523
|
+
useInstead: ["edit"]
|
|
13524
|
+
},
|
|
13083
13525
|
permission: "confirm",
|
|
13084
13526
|
mutating: true,
|
|
13085
13527
|
timeoutMs: 5e3,
|
|
@@ -13099,10 +13541,10 @@ var writeTool = {
|
|
|
13099
13541
|
},
|
|
13100
13542
|
required: ["path", "content"]
|
|
13101
13543
|
},
|
|
13102
|
-
async execute(input, ctx) {
|
|
13103
|
-
return writeFile7(input, ctx);
|
|
13544
|
+
async execute(input, ctx, opts) {
|
|
13545
|
+
return writeFile7(input, ctx, opts?.signal);
|
|
13104
13546
|
},
|
|
13105
|
-
async *executeStream(input, ctx) {
|
|
13547
|
+
async *executeStream(input, ctx, opts) {
|
|
13106
13548
|
const prepared = await prepareWrite(input, ctx);
|
|
13107
13549
|
if (!prepared.existed) {
|
|
13108
13550
|
for (const line of input.content.split("\n")) {
|
|
@@ -13110,11 +13552,11 @@ var writeTool = {
|
|
|
13110
13552
|
`, data: { livePreview: true } };
|
|
13111
13553
|
}
|
|
13112
13554
|
}
|
|
13113
|
-
yield { type: "final", output: await finishWrite(input, ctx, prepared) };
|
|
13555
|
+
yield { type: "final", output: await finishWrite(input, ctx, prepared, opts?.signal) };
|
|
13114
13556
|
}
|
|
13115
13557
|
};
|
|
13116
|
-
async function writeFile7(input, ctx) {
|
|
13117
|
-
return finishWrite(input, ctx, await prepareWrite(input, ctx));
|
|
13558
|
+
async function writeFile7(input, ctx, signal) {
|
|
13559
|
+
return finishWrite(input, ctx, await prepareWrite(input, ctx), signal);
|
|
13118
13560
|
}
|
|
13119
13561
|
async function prepareWrite(input, ctx) {
|
|
13120
13562
|
if (!input?.path) {
|
|
@@ -13133,12 +13575,12 @@ async function prepareWrite(input, ctx) {
|
|
|
13133
13575
|
let existed = false;
|
|
13134
13576
|
let prev = "";
|
|
13135
13577
|
try {
|
|
13136
|
-
const
|
|
13137
|
-
existed =
|
|
13578
|
+
const stat12 = await fs2.stat(absPath);
|
|
13579
|
+
existed = stat12.isFile();
|
|
13138
13580
|
if (existed) {
|
|
13139
13581
|
if (!ctx.hasRead(absPath)) {
|
|
13140
13582
|
prev = await fs2.readFile(absPath, "utf8");
|
|
13141
|
-
ctx.recordRead(absPath,
|
|
13583
|
+
ctx.recordRead(absPath, stat12.mtimeMs, "write", sha256hex(prev));
|
|
13142
13584
|
} else {
|
|
13143
13585
|
prev = await fs2.readFile(absPath, "utf8");
|
|
13144
13586
|
}
|
|
@@ -13150,23 +13592,32 @@ async function prepareWrite(input, ctx) {
|
|
|
13150
13592
|
}
|
|
13151
13593
|
return { absPath, existed, prev };
|
|
13152
13594
|
}
|
|
13153
|
-
async function finishWrite(input, ctx, prepared) {
|
|
13595
|
+
async function finishWrite(input, ctx, prepared, signal) {
|
|
13596
|
+
signal?.throwIfAborted();
|
|
13154
13597
|
await atomicWrite(prepared.absPath, input.content);
|
|
13155
13598
|
const diff = prepared.existed ? unifiedDiff(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
13156
13599
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
13157
|
-
const
|
|
13158
|
-
ctx.recordRead(prepared.absPath,
|
|
13600
|
+
const stat12 = await fs2.stat(prepared.absPath);
|
|
13601
|
+
ctx.recordRead(prepared.absPath, stat12.mtimeMs, "write", sha256hex(input.content));
|
|
13159
13602
|
ctx.session.recordFileChange({
|
|
13160
13603
|
path: prepared.absPath,
|
|
13161
13604
|
action: prepared.existed ? "modified" : "created",
|
|
13162
13605
|
before: prepared.existed ? prepared.prev : null,
|
|
13163
13606
|
after: input.content
|
|
13164
13607
|
});
|
|
13608
|
+
const syntax = await checkSyntax(
|
|
13609
|
+
prepared.absPath,
|
|
13610
|
+
input.content,
|
|
13611
|
+
prepared.existed ? prepared.prev : void 0
|
|
13612
|
+
).catch(() => void 0);
|
|
13613
|
+
const hasSyntaxErrors = syntax !== void 0 && syntax.errors.length > 0;
|
|
13165
13614
|
return {
|
|
13166
13615
|
path: prepared.absPath,
|
|
13167
13616
|
bytes_written: Buffer.byteLength(input.content, "utf8"),
|
|
13168
13617
|
created: !prepared.existed,
|
|
13169
|
-
diff
|
|
13618
|
+
diff,
|
|
13619
|
+
syntax_errors: hasSyntaxErrors ? syntax.errors : void 0,
|
|
13620
|
+
note: hasSyntaxErrors ? syntax.preExisting ? "Syntax check: the file still has parse errors (they pre-date this write) \u2014 see syntax_errors." : `Syntax check: the written content has ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.` : void 0
|
|
13170
13621
|
};
|
|
13171
13622
|
}
|
|
13172
13623
|
|