@theokit/sdk-tools 0.8.0 → 0.9.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/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CustomTool, ConfigurationError } from '@theokit/sdk';
2
+ import { FilesystemProvider } from '@theokit/sdk/filesystem';
2
3
 
3
4
  /**
4
5
  * `apply_patch` — built-in tool for coding agents.
@@ -504,6 +505,25 @@ interface QuestionTool {
504
505
  }
505
506
  declare function createQuestionTool(opts: QuestionToolOptions): QuestionTool;
506
507
 
508
+ /**
509
+ * `ReadTracker` — SE32 read-before-write safety.
510
+ *
511
+ * A per-run map of `path → mtimeMs` recorded whenever the read tool reads a
512
+ * file. The write tool (with `requireReadBeforeWrite` on) consults it before an
513
+ * overwrite: a file that was never read, or that changed on disk since it was
514
+ * read, is refused instead of silently clobbered. Scope one tracker per run /
515
+ * session — it is deliberately NOT a global singleton (no cross-run leak).
516
+ *
517
+ * @public
518
+ */
519
+ declare class ReadTracker {
520
+ private readonly seen;
521
+ /** Record the mtime observed when `path` was read. */
522
+ record(path: string, mtimeMs: number): void;
523
+ /** The mtime last recorded for `path`, or `undefined` if never read. */
524
+ expected(path: string): number | undefined;
525
+ }
526
+
507
527
  /**
508
528
  * `read_file` — built-in tool for coding agents.
509
529
  *
@@ -533,6 +553,12 @@ declare function createQuestionTool(opts: QuestionToolOptions): QuestionTool;
533
553
  interface CreateReadFileToolOptions {
534
554
  /** Absolute path to the project root. Every read is gated against this boundary. */
535
555
  projectRoot: string;
556
+ /**
557
+ * SE32 — optional read-before-write tracker. When provided, a successful read
558
+ * records the file's mtime so a paired `write_file` (with
559
+ * `requireReadBeforeWrite`) can refuse a blind or stale overwrite.
560
+ */
561
+ readTracker?: ReadTracker;
536
562
  }
537
563
  declare function createReadFileTool(opts: CreateReadFileToolOptions): CustomTool;
538
564
 
@@ -862,10 +888,32 @@ declare function createGenericHttpSearchAdapter(opts?: CreateGenericHttpSearchAd
862
888
  * 'binary_file' }` on refusal
863
889
  */
864
890
 
891
+ /** The run-scoped context a `defineTool` handler receives as its 2nd argument. */
892
+ type WriteToolContext = {
893
+ signal?: AbortSignal;
894
+ context?: unknown;
895
+ };
865
896
  interface CreateWriteFileToolOptions {
866
897
  /** Absolute path to the project root. Every write is gated against this boundary. */
867
898
  projectRoot: string;
899
+ /**
900
+ * SE31 — optional pluggable filesystem backend (`@theokit/sdk/filesystem`), or
901
+ * a per-request resolver `(ctx) => FilesystemBackend`. When provided, writes
902
+ * route through it (its own boundary + `readOnly` + per-request root) instead
903
+ * of the local project fs. Omitted ⇒ identical current behavior (local
904
+ * `projectRoot`). The `.env`/`.git` policy still applies (storage-independent).
905
+ */
906
+ filesystem?: FilesystemProvider<WriteToolContext>;
907
+ /**
908
+ * SE32 — when true (and a {@link ReadTracker} is supplied), refuse to
909
+ * overwrite an existing file that was not read first, or that changed on disk
910
+ * since it was read (`read_required` / `stale_file`). A NEW file writes
911
+ * freely. Default OFF (unchanged behavior).
912
+ */
913
+ requireReadBeforeWrite?: boolean;
914
+ /** SE32 — the per-run tracker populated by the paired `read_file` tool. */
915
+ readTracker?: ReadTracker;
868
916
  }
869
917
  declare function createWriteFileTool(opts: CreateWriteFileToolOptions): CustomTool;
870
918
 
871
- export { CatastrophicCommandError, type CommandPolicy, type CreateApplyPatchToolOptions, type CreateBraveWebSearchAdapterOptions, type CreateEditFileToolOptions, type CreateGenericHttpSearchAdapterOptions, type CreateGitDiffToolOptions, type CreateGlobToolOptions, type CreateListDirToolOptions, type CreateReadFileToolOptions, type CreateRunVitestToolOptions, type CreateSearchTextToolOptions, type CreateShellToolOptions, type CreateWebFetchToolOptions, type CreateWebSearchToolOptions, type CreateWriteFileToolOptions, DEFAULT_TOOL_GUIDANCE, type EnvContextOptions, type PlanModeTool, type PlanModeToolOptions, type PlanModeToolWithStore, type PlanNode, type QuestionTool, type QuestionToolOptions, RedirectBlockedError, type RepoMapOptions, type ResolveAndScreenOptions, type ScreenedFetchOptions, type SessionArtifactStore, type SessionArtifactStoreOptions, SsrfBlockedError, type TodoItem, type TodolistTool, type ToolGuidanceMap, type TruncationOptions, type TruncationResult, type VitestSummary, type WebSearchCallback, type WebSearchResult, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withShellExitGuidance, withToolResultGuidance };
919
+ export { CatastrophicCommandError, type CommandPolicy, type CreateApplyPatchToolOptions, type CreateBraveWebSearchAdapterOptions, type CreateEditFileToolOptions, type CreateGenericHttpSearchAdapterOptions, type CreateGitDiffToolOptions, type CreateGlobToolOptions, type CreateListDirToolOptions, type CreateReadFileToolOptions, type CreateRunVitestToolOptions, type CreateSearchTextToolOptions, type CreateShellToolOptions, type CreateWebFetchToolOptions, type CreateWebSearchToolOptions, type CreateWriteFileToolOptions, DEFAULT_TOOL_GUIDANCE, type EnvContextOptions, type PlanModeTool, type PlanModeToolOptions, type PlanModeToolWithStore, type PlanNode, type QuestionTool, type QuestionToolOptions, ReadTracker, RedirectBlockedError, type RepoMapOptions, type ResolveAndScreenOptions, type ScreenedFetchOptions, type SessionArtifactStore, type SessionArtifactStoreOptions, SsrfBlockedError, type TodoItem, type TodolistTool, type ToolGuidanceMap, type TruncationOptions, type TruncationResult, type VitestSummary, type WebSearchCallback, type WebSearchResult, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withShellExitGuidance, withToolResultGuidance };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CustomTool, ConfigurationError } from '@theokit/sdk';
2
+ import { FilesystemProvider } from '@theokit/sdk/filesystem';
2
3
 
3
4
  /**
4
5
  * `apply_patch` — built-in tool for coding agents.
@@ -504,6 +505,25 @@ interface QuestionTool {
504
505
  }
505
506
  declare function createQuestionTool(opts: QuestionToolOptions): QuestionTool;
506
507
 
508
+ /**
509
+ * `ReadTracker` — SE32 read-before-write safety.
510
+ *
511
+ * A per-run map of `path → mtimeMs` recorded whenever the read tool reads a
512
+ * file. The write tool (with `requireReadBeforeWrite` on) consults it before an
513
+ * overwrite: a file that was never read, or that changed on disk since it was
514
+ * read, is refused instead of silently clobbered. Scope one tracker per run /
515
+ * session — it is deliberately NOT a global singleton (no cross-run leak).
516
+ *
517
+ * @public
518
+ */
519
+ declare class ReadTracker {
520
+ private readonly seen;
521
+ /** Record the mtime observed when `path` was read. */
522
+ record(path: string, mtimeMs: number): void;
523
+ /** The mtime last recorded for `path`, or `undefined` if never read. */
524
+ expected(path: string): number | undefined;
525
+ }
526
+
507
527
  /**
508
528
  * `read_file` — built-in tool for coding agents.
509
529
  *
@@ -533,6 +553,12 @@ declare function createQuestionTool(opts: QuestionToolOptions): QuestionTool;
533
553
  interface CreateReadFileToolOptions {
534
554
  /** Absolute path to the project root. Every read is gated against this boundary. */
535
555
  projectRoot: string;
556
+ /**
557
+ * SE32 — optional read-before-write tracker. When provided, a successful read
558
+ * records the file's mtime so a paired `write_file` (with
559
+ * `requireReadBeforeWrite`) can refuse a blind or stale overwrite.
560
+ */
561
+ readTracker?: ReadTracker;
536
562
  }
537
563
  declare function createReadFileTool(opts: CreateReadFileToolOptions): CustomTool;
538
564
 
@@ -862,10 +888,32 @@ declare function createGenericHttpSearchAdapter(opts?: CreateGenericHttpSearchAd
862
888
  * 'binary_file' }` on refusal
863
889
  */
864
890
 
891
+ /** The run-scoped context a `defineTool` handler receives as its 2nd argument. */
892
+ type WriteToolContext = {
893
+ signal?: AbortSignal;
894
+ context?: unknown;
895
+ };
865
896
  interface CreateWriteFileToolOptions {
866
897
  /** Absolute path to the project root. Every write is gated against this boundary. */
867
898
  projectRoot: string;
899
+ /**
900
+ * SE31 — optional pluggable filesystem backend (`@theokit/sdk/filesystem`), or
901
+ * a per-request resolver `(ctx) => FilesystemBackend`. When provided, writes
902
+ * route through it (its own boundary + `readOnly` + per-request root) instead
903
+ * of the local project fs. Omitted ⇒ identical current behavior (local
904
+ * `projectRoot`). The `.env`/`.git` policy still applies (storage-independent).
905
+ */
906
+ filesystem?: FilesystemProvider<WriteToolContext>;
907
+ /**
908
+ * SE32 — when true (and a {@link ReadTracker} is supplied), refuse to
909
+ * overwrite an existing file that was not read first, or that changed on disk
910
+ * since it was read (`read_required` / `stale_file`). A NEW file writes
911
+ * freely. Default OFF (unchanged behavior).
912
+ */
913
+ requireReadBeforeWrite?: boolean;
914
+ /** SE32 — the per-run tracker populated by the paired `read_file` tool. */
915
+ readTracker?: ReadTracker;
868
916
  }
869
917
  declare function createWriteFileTool(opts: CreateWriteFileToolOptions): CustomTool;
870
918
 
871
- export { CatastrophicCommandError, type CommandPolicy, type CreateApplyPatchToolOptions, type CreateBraveWebSearchAdapterOptions, type CreateEditFileToolOptions, type CreateGenericHttpSearchAdapterOptions, type CreateGitDiffToolOptions, type CreateGlobToolOptions, type CreateListDirToolOptions, type CreateReadFileToolOptions, type CreateRunVitestToolOptions, type CreateSearchTextToolOptions, type CreateShellToolOptions, type CreateWebFetchToolOptions, type CreateWebSearchToolOptions, type CreateWriteFileToolOptions, DEFAULT_TOOL_GUIDANCE, type EnvContextOptions, type PlanModeTool, type PlanModeToolOptions, type PlanModeToolWithStore, type PlanNode, type QuestionTool, type QuestionToolOptions, RedirectBlockedError, type RepoMapOptions, type ResolveAndScreenOptions, type ScreenedFetchOptions, type SessionArtifactStore, type SessionArtifactStoreOptions, SsrfBlockedError, type TodoItem, type TodolistTool, type ToolGuidanceMap, type TruncationOptions, type TruncationResult, type VitestSummary, type WebSearchCallback, type WebSearchResult, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withShellExitGuidance, withToolResultGuidance };
919
+ export { CatastrophicCommandError, type CommandPolicy, type CreateApplyPatchToolOptions, type CreateBraveWebSearchAdapterOptions, type CreateEditFileToolOptions, type CreateGenericHttpSearchAdapterOptions, type CreateGitDiffToolOptions, type CreateGlobToolOptions, type CreateListDirToolOptions, type CreateReadFileToolOptions, type CreateRunVitestToolOptions, type CreateSearchTextToolOptions, type CreateShellToolOptions, type CreateWebFetchToolOptions, type CreateWebSearchToolOptions, type CreateWriteFileToolOptions, DEFAULT_TOOL_GUIDANCE, type EnvContextOptions, type PlanModeTool, type PlanModeToolOptions, type PlanModeToolWithStore, type PlanNode, type QuestionTool, type QuestionToolOptions, ReadTracker, RedirectBlockedError, type RepoMapOptions, type ResolveAndScreenOptions, type ScreenedFetchOptions, type SessionArtifactStore, type SessionArtifactStoreOptions, SsrfBlockedError, type TodoItem, type TodolistTool, type ToolGuidanceMap, type TruncationOptions, type TruncationResult, type VitestSummary, type WebSearchCallback, type WebSearchResult, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withShellExitGuidance, withToolResultGuidance };
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { safeFilenameForId, safePathJoin as safePathJoin$1 } from '@theokit/sdk/
8
8
  import { spawn } from 'child_process';
9
9
  import { lookup } from 'dns/promises';
10
10
  import { isIP } from 'net';
11
+ import { resolveFilesystem, FilesystemSecurityError, FilesystemReadOnlyError, StaleFileError, FilesystemError, FileNotFoundError } from '@theokit/sdk/filesystem';
11
12
 
12
13
  // src/apply-patch.ts
13
14
  var PathTraversalError = class extends ConfigurationError {
@@ -1052,7 +1053,10 @@ function withToolResultGuidance(tool, guidance) {
1052
1053
  name: tool.name,
1053
1054
  description: tool.description,
1054
1055
  inputSchema: tool.inputSchema,
1055
- handler: async (input) => injectGuidance(await tool.handler(input), guidance)
1056
+ handler: async (input) => {
1057
+ const out = await tool.handler(input);
1058
+ return typeof out === "string" ? injectGuidance(out, guidance) : out;
1059
+ }
1056
1060
  };
1057
1061
  }
1058
1062
  function withDefaultGuidance(tool) {
@@ -1066,6 +1070,7 @@ function withShellExitGuidance(tool) {
1066
1070
  inputSchema: tool.inputSchema,
1067
1071
  handler: async (input) => {
1068
1072
  const out = await tool.handler(input);
1073
+ if (typeof out !== "string") return out;
1069
1074
  let parsed;
1070
1075
  try {
1071
1076
  parsed = JSON.parse(out);
@@ -1262,7 +1267,7 @@ function createQuestionTool(opts) {
1262
1267
  var MAX_FILE_SIZE = 5 * 1024 * 1024;
1263
1268
  var BINARY_PROBE_BYTES = 8 * 1024;
1264
1269
  function createReadFileTool(opts) {
1265
- const { projectRoot } = opts;
1270
+ const { projectRoot, readTracker } = opts;
1266
1271
  return defineTool({
1267
1272
  name: "read_file",
1268
1273
  description: "Read a project-relative text file as UTF-8. ALWAYS read a file before you edit it (edit_file) or overwrite it (write_file), so your old_string / new content matches the real bytes exactly. Returns the WHOLE file (there is no offset or line-range parameter); to locate a symbol inside a large file, use search_text instead of re-reading. Refuses paths that escape the project root, sensitive files (.env, .git/, node_modules/, .theo/, lock files), and binary files (null byte in the first 8 KB); caps at 5 MB. Returns { ok, content, size } or { ok: false, error }.",
@@ -1278,7 +1283,11 @@ function createReadFileTool(opts) {
1278
1283
  const opened = await openHandleSafe(boundary.absolutePath, path);
1279
1284
  if ("error" in opened) return opened.error;
1280
1285
  try {
1281
- return await readContent(opened.handle, path);
1286
+ return await readContent(
1287
+ opened.handle,
1288
+ path,
1289
+ (mtimeMs) => readTracker?.record(path, mtimeMs)
1290
+ );
1282
1291
  } finally {
1283
1292
  await opened.handle.close();
1284
1293
  }
@@ -1309,7 +1318,7 @@ async function openHandleSafe(absolutePath, path) {
1309
1318
  throw err;
1310
1319
  }
1311
1320
  }
1312
- async function readContent(handle, path) {
1321
+ async function readContent(handle, path, onRead) {
1313
1322
  const stat2 = await handle.stat();
1314
1323
  if (stat2.size > MAX_FILE_SIZE) {
1315
1324
  return JSON.stringify({
@@ -1324,6 +1333,7 @@ async function readContent(handle, path) {
1324
1333
  return JSON.stringify({ ok: false, error: "binary_file", path, size: stat2.size });
1325
1334
  }
1326
1335
  const content = await handle.readFile({ encoding: "utf-8" });
1336
+ onRead?.(stat2.mtimeMs);
1327
1337
  return JSON.stringify({ ok: true, content, size: stat2.size });
1328
1338
  }
1329
1339
  async function isBinaryProbe(handle, size) {
@@ -1336,6 +1346,26 @@ async function isBinaryProbe(handle, size) {
1336
1346
  }
1337
1347
  return false;
1338
1348
  }
1349
+
1350
+ // src/read-tracker.ts
1351
+ var ReadTracker = class {
1352
+ seen = /* @__PURE__ */ new Map();
1353
+ /** Record the mtime observed when `path` was read. */
1354
+ record(path, mtimeMs) {
1355
+ this.seen.set(path, mtimeMs);
1356
+ }
1357
+ /** The mtime last recorded for `path`, or `undefined` if never read. */
1358
+ expected(path) {
1359
+ return this.seen.get(path);
1360
+ }
1361
+ };
1362
+ function evaluateReadBeforeWrite(tracker, path, currentMtimeMs) {
1363
+ if (currentMtimeMs === null) return "ok";
1364
+ const recorded = tracker.expected(path);
1365
+ if (recorded === void 0) return "read_required";
1366
+ if (recorded !== currentMtimeMs) return "stale";
1367
+ return "ok";
1368
+ }
1339
1369
  var DEFAULT_TIMEOUT_MS2 = 12e4;
1340
1370
  var DEFAULT_MAX_STDOUT_BYTES2 = 10 * 1024 * 1024;
1341
1371
  function createRunVitestTool(opts) {
@@ -1968,38 +1998,111 @@ function createGenericHttpSearchAdapter(opts = {}) {
1968
1998
  }
1969
1999
  var BINARY_PROBE_BYTES3 = 8 * 1024;
1970
2000
  function createWriteFileTool(opts) {
1971
- const { projectRoot } = opts;
2001
+ const { projectRoot, filesystem } = opts;
2002
+ if (opts.requireReadBeforeWrite && !opts.readTracker) {
2003
+ throw new Error(
2004
+ "createWriteFileTool: requireReadBeforeWrite is true but no readTracker was provided \u2014 pass the same ReadTracker instance to createReadFileTool and createWriteFileTool."
2005
+ );
2006
+ }
2007
+ const guard = opts.requireReadBeforeWrite ? opts.readTracker : void 0;
1972
2008
  return defineTool({
1973
2009
  name: "write_file",
1974
- description: "Write UTF-8 content to a project-relative file, creating parent directories as needed. OVERWRITES any existing file at the path. Prefer editing an existing file with edit_file over rewriting it; use write_file to create a NEW file or fully replace a small one. If the file already exists, read_file it first so you do not discard content you have not seen. Refuses paths that escape the project root, sensitive files (.env, .git/, node_modules/, .theo/, lock files), and binary-file overwrites. Returns { ok, path, bytes } or { ok: false, error }.",
2010
+ description: "Write UTF-8 content to a project-relative file, creating parent directories as needed. OVERWRITES any existing file at the path. Prefer editing an existing file with edit_file over rewriting it; use write_file to create a NEW file or fully replace a small one. If the file already exists, read_file it first so you do not discard content you have not seen. Refuses paths that escape the write root and sensitive files (.env, .git/, node_modules/, .theo/, lock files); the default local root also refuses binary-file overwrites. Returns { ok, path, bytes } or { ok: false, error }.",
1975
2011
  inputSchema: z.object({
1976
2012
  path: z.string().min(1).describe("Project-relative file path."),
1977
2013
  content: z.string().describe("UTF-8 content to write.")
1978
2014
  }),
1979
- handler: async ({ path, content }) => {
2015
+ handler: async ({ path, content }, ctx) => {
1980
2016
  if (isForbiddenPath(path)) {
1981
2017
  return JSON.stringify({ ok: false, error: "forbidden_path", path });
1982
2018
  }
1983
- let absolutePath;
1984
- try {
1985
- absolutePath = safePathJoin(projectRoot, path);
1986
- assertNoSymlinkEscape(absolutePath, projectRoot);
1987
- } catch (err) {
1988
- if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
1989
- return JSON.stringify({ ok: false, error: "path_traversal", path });
1990
- }
1991
- throw err;
1992
- }
1993
- if (await isBinaryFile(absolutePath)) {
1994
- return JSON.stringify({ ok: false, error: "binary_file", path });
2019
+ if (filesystem) {
2020
+ const backend = await resolveFilesystem(filesystem, ctx ?? {});
2021
+ return writeViaBackend(backend, path, content, guard);
1995
2022
  }
1996
- await mkdir(dirname(absolutePath), { recursive: true });
1997
- await writeFile(absolutePath, content, "utf-8");
1998
- const bytes = Buffer.byteLength(content, "utf-8");
1999
- return JSON.stringify({ ok: true, path, bytes });
2023
+ return writeViaLocalFs(projectRoot, path, content, guard);
2000
2024
  }
2001
2025
  });
2002
2026
  }
2027
+ function readBeforeWriteError(guard, path, currentMtimeMs) {
2028
+ if (!guard) return null;
2029
+ const decision = evaluateReadBeforeWrite(guard, path, currentMtimeMs);
2030
+ if (decision === "read_required")
2031
+ return JSON.stringify({ ok: false, error: "read_required", path });
2032
+ if (decision === "stale") return JSON.stringify({ ok: false, error: "stale_file", path });
2033
+ return null;
2034
+ }
2035
+ async function writeViaLocalFs(projectRoot, path, content, guard) {
2036
+ let absolutePath;
2037
+ try {
2038
+ absolutePath = safePathJoin(projectRoot, path);
2039
+ assertNoSymlinkEscape(absolutePath, projectRoot);
2040
+ } catch (err) {
2041
+ if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
2042
+ return JSON.stringify({ ok: false, error: "path_traversal", path });
2043
+ }
2044
+ throw err;
2045
+ }
2046
+ const rbw = readBeforeWriteError(guard, path, await statMtimeOrNull(absolutePath));
2047
+ if (rbw) return rbw;
2048
+ if (await isBinaryFile(absolutePath)) {
2049
+ return JSON.stringify({ ok: false, error: "binary_file", path });
2050
+ }
2051
+ await mkdir(dirname(absolutePath), { recursive: true });
2052
+ await writeFile(absolutePath, content, "utf-8");
2053
+ const bytes = Buffer.byteLength(content, "utf-8");
2054
+ return JSON.stringify({ ok: true, path, bytes });
2055
+ }
2056
+ async function statMtimeOrNull(absolutePath) {
2057
+ try {
2058
+ return (await stat(absolutePath)).mtimeMs;
2059
+ } catch (err) {
2060
+ if (err.code === "ENOENT") return null;
2061
+ throw err;
2062
+ }
2063
+ }
2064
+ async function backendMtimeOrNull(backend, path) {
2065
+ try {
2066
+ return (await backend.stat(path)).mtimeMs;
2067
+ } catch (err) {
2068
+ if (err instanceof FileNotFoundError) return null;
2069
+ throw err;
2070
+ }
2071
+ }
2072
+ async function writeViaBackend(backend, path, content, guard) {
2073
+ try {
2074
+ let expectedMtime;
2075
+ if (guard) {
2076
+ const current = await backendMtimeOrNull(backend, path);
2077
+ const rbw = readBeforeWriteError(guard, path, current);
2078
+ if (rbw) return rbw;
2079
+ expectedMtime = current ?? void 0;
2080
+ }
2081
+ const stat2 = await backend.writeFile(
2082
+ path,
2083
+ content,
2084
+ expectedMtime !== void 0 ? { expectedMtime } : void 0
2085
+ );
2086
+ return JSON.stringify({ ok: true, path, bytes: stat2.size });
2087
+ } catch (err) {
2088
+ return backendErrorToJson(err, path);
2089
+ }
2090
+ }
2091
+ function backendErrorToJson(err, path) {
2092
+ if (err instanceof FilesystemSecurityError) {
2093
+ return JSON.stringify({ ok: false, error: "path_traversal", path });
2094
+ }
2095
+ if (err instanceof FilesystemReadOnlyError) {
2096
+ return JSON.stringify({ ok: false, error: "read_only", path });
2097
+ }
2098
+ if (err instanceof StaleFileError) {
2099
+ return JSON.stringify({ ok: false, error: "stale_file", path });
2100
+ }
2101
+ if (err instanceof FilesystemError) {
2102
+ return JSON.stringify({ ok: false, error: "write_failed", path });
2103
+ }
2104
+ throw err;
2105
+ }
2003
2106
  async function isBinaryFile(absolutePath) {
2004
2107
  let handle;
2005
2108
  try {
@@ -2022,6 +2125,6 @@ async function isBinaryFile(absolutePath) {
2022
2125
  }
2023
2126
  }
2024
2127
 
2025
- export { CatastrophicCommandError, DEFAULT_TOOL_GUIDANCE, RedirectBlockedError, SsrfBlockedError, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withShellExitGuidance, withToolResultGuidance };
2128
+ export { CatastrophicCommandError, DEFAULT_TOOL_GUIDANCE, ReadTracker, RedirectBlockedError, SsrfBlockedError, buildEnvContext, buildRepoMap, catastrophicShellReason, commandDenialReason, createApplyPatchTool, createBraveWebSearchAdapter, createEditFileTool, createGenericHttpSearchAdapter, createGitDiffTool, createGlobTool, createListDirTool, createPlanModeTool, createQuestionTool, createReadFileTool, createRunVitestTool, createSearchTextTool, createSessionArtifactStore, createShellTool, createTodolistTool, createWebFetchTool, createWebSearchTool, createWriteFileTool, denyCatastrophicCommands, formatCode, formatDiff, formatError, formatFileList, injectGuidance, isBlockedIp, isCommandAllowed, renderToolList, resolveAndScreen, screenedFetch, todoItemsToPlanNodes, truncateOutput, withDefaultGuidance, withDescription, withShellExitGuidance, withToolResultGuidance };
2026
2129
  //# sourceMappingURL=index.js.map
2027
2130
  //# sourceMappingURL=index.js.map