@swoop111/dsh-tool-fs 0.2.0-rc.2 → 0.2.0-rc.2.1

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/lib/index.js CHANGED
@@ -8,16 +8,16 @@ import { structuredPatch } from "diff";
8
8
  import { basename, extname } from "node:path";
9
9
  import { AttachmentError, AttachmentId } from "@deepseek-ai/dsh-attachment";
10
10
  import { ESCALATION_TARGETS, approveEscalation, escalationHintMarker, sandboxDenialMarker, sandboxPermissionsDescription, validateEscalationArgs } from "@deepseek-ai/dsh-sandbox";
11
- //#region lib/types/read-render.js
12
- /**
13
- * Pure read presentation: turn provider-decoded text into a bounded, line-numbered window and
14
- * model-facing envelope. Chunk scanning caps the current line, so even one newline-free giant
15
- * line cannot grow memory without bound.
16
- * @module @deepseek-ai/dsh-tool-fs/read-render
17
- */
18
- /** Default maximum characters returned for a single line (the `readMaxLineLength` config). */
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
19
  const READ_MAX_LINE_LENGTH = 2e3;
20
- /** Default maximum bytes returned for selected file lines (the `readMaxBytes` config). */
20
+
21
21
  const READ_MAX_BYTES = 50 * 1024;
22
22
  function newAccumulator() {
23
23
  return {
@@ -60,14 +60,14 @@ function finish(acc, request, displayPath) {
60
60
  };
61
61
  }
62
62
  const OFFSET_OUT_OF_RANGE_RE = /^offset (-?\d+) is out of range for "(?:[^"\\]|\\"|\\\\)*" \((\d+) lines\)$/u;
63
- /**
64
- * Recognize this package's own offset-out-of-range `FsError` and return its scan
65
- * facts, so the read tool can tail-anchor the window instead of erroring. The
66
- * message and this parser live in the same module on purpose: the format is a
67
- * package-internal contract, pinned by tests on both sides.
68
- * @param error - the caught value from {@link buildWindow}.
69
- * @returns the requested offset and observed line count, or `undefined` for any other failure.
70
- */
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
71
  function parseOffsetOutOfRange(error) {
72
72
  if (!(error instanceof FsError) || error.code !== "FS_NOT_FOUND") return void 0;
73
73
  const match = error.message.match(OFFSET_OUT_OF_RANGE_RE);
@@ -79,15 +79,15 @@ function parseOffsetOutOfRange(error) {
79
79
  totalLines
80
80
  } : void 0;
81
81
  }
82
- /**
83
- * Build one window from streamed or whole-file chunks, enforcing line and byte caps while still
84
- * scanning to an exact total line count, and throwing `FS_NOT_FOUND` when the requested offset is
85
- * past EOF.
86
- * @param chunks - decoded text chunks in file order; chunk boundaries carry no meaning.
87
- * @param request - the resolved window; the caller has already applied its defaults and caps.
88
- * @param displayPath - the caller-facing path used in the offset-out-of-range error.
89
- * @returns the numbered window lines, the total line count seen, and the byte-cap truncation flag.
90
- */
82
+
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+
91
91
  async function buildWindow(chunks, request, displayPath) {
92
92
  const acc = newAccumulator();
93
93
  const lineBufferCap = request.maxLineLength + 1;
@@ -114,12 +114,12 @@ async function buildWindow(chunks, request, displayPath) {
114
114
  if (lineBuffer.length > 0) flushLine();
115
115
  return finish(acc, request, displayPath);
116
116
  }
117
- /**
118
- * Format a read outcome as one OpenCode-style line-numbered text block body.
119
- * @param displayPath - the backend-resolved path rendered in the envelope's `<path>` element.
120
- * @param outcome - the windowed read to render.
121
- * @returns the model-facing envelope: numbered lines, a continuation or end-of-file footer, and the repair disclosure when one fired.
122
- */
117
+
118
+
119
+
120
+
121
+
122
+
123
123
  function formatReadOutput(displayPath, outcome) {
124
124
  const endLine = outcome.lines.at(-1)?.number ?? Math.max(0, outcome.offset - 1);
125
125
  let footer;
@@ -132,30 +132,30 @@ function formatReadOutput(displayPath, outcome) {
132
132
  ${outcome.lines.length > 0 ? `${outcome.lines.map((line) => `${line.number}: ${line.text}`).join("\n")}\n\n${footer}` : footer}${outcome.repairNote === void 0 ? "" : `\n(repair: ${outcome.repairNote})`}
133
133
  </content>`;
134
134
  }
135
- /**
136
- * Whether `value` is a valid {@link FileTextLine} (defensive narrowing from
137
- * opaque `meta`). `number` must be a 1-based integer line number, since a card
138
- * rendered from a zero, fractional, or non-finite line number would violate the
139
- * 1-based numbering contract the read window promises.
140
- */
135
+
136
+
137
+
138
+
139
+
140
+
141
141
  function isFileTextLine(value) {
142
142
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
143
143
  const { number, text } = value;
144
144
  return typeof number === "number" && Number.isInteger(number) && number >= 1 && typeof text === "string";
145
145
  }
146
- /**
147
- * Narrow opaque live or replayed result metadata to a structured read window.
148
- * Malformed metadata returns `undefined` so presentation can fall back to the
149
- * generic text card instead of throwing during replay. Beyond shape, the
150
- * semantic contract of a read window is enforced against replayed JSON that is
151
- * well-typed but out of range: `offset` must be a 1-based integer, `totalLines`
152
- * must be a non-negative integer, each line number must be a 1-based integer no
153
- * less than `offset`, the line numbers must strictly increase, and no line number
154
- * may exceed `totalLines`. Any violation declines to the generic fallback rather
155
- * than emitting a card that misnumbers or overcounts.
156
- * @param meta - result metadata.
157
- * @returns the validated read window, or `undefined` for absent, malformed, or semantically invalid data.
158
- */
146
+
147
+
148
+
149
+
150
+
151
+
152
+
153
+
154
+
155
+
156
+
157
+
158
+
159
159
  function readMetaFromMeta(meta) {
160
160
  if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
161
161
  const { path, offset, lines, totalLines, lang } = meta;
@@ -177,30 +177,30 @@ function readMetaFromMeta(meta) {
177
177
  ...lang === void 0 ? {} : { lang }
178
178
  };
179
179
  }
180
- //#endregion
181
- //#region lib/types/session-cwd.js
182
- /**
183
- * Derive the working directory a filesystem tool resolves relative paths against: the calling
184
- * agent's per-session workspace (`exec.agent.session.header.cwd`), so each session's
185
- * `read`/`write`/`edit` act on its workspace, not the server's launch directory.
186
- * Non-agent calls return `undefined`, leaving the fallback in the provider rather than reading
187
- * `process.cwd()` at the tool boundary.
188
- * @module @deepseek-ai/dsh-tool-fs/session-cwd
189
- */
190
- /**
191
- * The session workspace cwd for this call, or `undefined` when none applies.
192
- * @param exec - the tool-execution context; only its optional `agent` is read.
193
- * @returns the calling agent's session cwd, or undefined for a non-agent caller (the backend then applies its own default).
194
- */
180
+
181
+
182
+
183
+
184
+
185
+
186
+
187
+
188
+
189
+
190
+
191
+
192
+
193
+
194
+
195
195
  function sessionCwd(exec) {
196
196
  return exec.agent?.session.header.cwd;
197
197
  }
198
- /**
199
- * Resolution options shared by all model-facing filesystem tools.
200
- * @param exec - the tool-execution context supplying session cwd and cancellation.
201
- * @param policyWorkspaceRoot - resolved per-call root, when a mutation carries sandbox policy.
202
- * @returns provider resolution options for the current tool call.
203
- */
198
+
199
+
200
+
201
+
202
+
203
+
204
204
  function sessionResolveOptions(exec, policyWorkspaceRoot) {
205
205
  const cwd = policyWorkspaceRoot ?? sessionCwd(exec);
206
206
  return {
@@ -208,19 +208,19 @@ function sessionResolveOptions(exec, policyWorkspaceRoot) {
208
208
  signal: exec.signal
209
209
  };
210
210
  }
211
- //#endregion
212
- //#region lib/types/read-target.js
213
- /**
214
- * Shared path resolution and regular-file validation for model-facing read tools.
215
- * @module @deepseek-ai/dsh-tool-fs/src/read-target
216
- */
217
- /**
218
- * Resolve a model-supplied path, observe absence, and require a regular file.
219
- * @param ctx - the plugin context providing filesystem resolution and observation events.
220
- * @param exec - the current tool execution, including session cwd and cancellation.
221
- * @param requestedPath - the raw path supplied to the tool.
222
- * @returns the resolved target and its single stat result.
223
- */
211
+
212
+
213
+
214
+
215
+
216
+
217
+
218
+
219
+
220
+
221
+
222
+
223
+
224
224
  async function resolveRegularReadTarget(ctx, exec, requestedPath) {
225
225
  const target = await ctx.fs.resolve(requestedPath, sessionResolveOptions(exec));
226
226
  const info = await ctx.fs.stat(target, exec.signal);
@@ -234,18 +234,18 @@ async function resolveRegularReadTarget(ctx, exec, requestedPath) {
234
234
  info
235
235
  };
236
236
  }
237
- /**
238
- * Attempt the engine's missing-path composition for a failed tool target: a
239
- * unique match among the session's prior successful path arguments, then a
240
- * unique directory-tree completion, each proven present before adoption. A
241
- * tool never re-anchors on ambiguity, so a wrong guess cannot replace a real
242
- * answer — the caller keeps its verbatim error when this returns `undefined`.
243
- * @param ctx - the plugin context providing filesystem resolution and listing.
244
- * @param exec - the current tool execution, including session cwd and cancellation.
245
- * @param requestedPath - the raw path the model supplied.
246
- * @param knownPaths - the session's prior successful path arguments, most recent first.
247
- * @returns the verified replacement path with its disclosure, or `undefined`.
248
- */
237
+
238
+
239
+
240
+
241
+
242
+
243
+
244
+
245
+
246
+
247
+
248
+
249
249
  async function repairTargetPath(ctx, exec, requestedPath, knownPaths) {
250
250
  const cwd = sessionCwd(exec);
251
251
  return await repairMissingPath(requestedPath, {
@@ -258,7 +258,7 @@ async function repairTargetPath(ctx, exec, requestedPath, knownPaths) {
258
258
  }
259
259
  });
260
260
  }
261
- /** The filesystem itself as the engine's read-only probe: entry names, undefined when unreadable. */
261
+
262
262
  function fsPathExplorer(ctx, signal) {
263
263
  return { listDir: async (directory) => {
264
264
  const target = await ctx.fs.resolve(directory, { signal }).catch(() => void 0);
@@ -266,23 +266,23 @@ function fsPathExplorer(ctx, signal) {
266
266
  return await ctx.fs.listDir(target, signal).then((entries) => entries.map((entry) => entry.name)).catch(() => void 0);
267
267
  } };
268
268
  }
269
- //#endregion
270
- //#region lib/types/structure.js
271
- /**
272
- * Structural analysis of file text for read-window completion: markdown
273
- * fences, bracket nesting, and indentation blocks. The analyzers are pure
274
- * functions over already-delivered text; they decide which construct a window
275
- * boundary falls inside, and how many neighbouring lines close an open one.
276
- *
277
- * Precision is traded for never touching delivered content: a wrong verdict
278
- * only costs a few appended real lines, or a silent non-completion. String
279
- * literals are scanned per line (an unterminated quote never carries into the
280
- * next line), line comments and C-style block comments are skipped, and
281
- * brackets inside an open fence are content, so prose and code samples do not
282
- * fabricate unbalanced constructs.
283
- * @module @deepseek-ai/dsh-tool-fs/structure
284
- */
285
- /** The fence marker of one line, or `undefined` when the line is not a fence. */
269
+
270
+
271
+
272
+
273
+
274
+
275
+
276
+
277
+
278
+
279
+
280
+
281
+
282
+
283
+
284
+
285
+
286
286
  function fenceLine(line) {
287
287
  const rest = line.slice(line.length - line.trimStart().length);
288
288
  if (line.length - rest.length > 3) return void 0;
@@ -298,12 +298,12 @@ function fenceLine(line) {
298
298
  info
299
299
  };
300
300
  }
301
- /** Whether one line closes an open fence: same family, at least as long, no info string. */
301
+
302
302
  function closesFence(marker, line) {
303
303
  const candidate = fenceLine(line);
304
304
  return candidate !== void 0 && candidate.marker.slice(0, 1) === marker.slice(0, 1) && candidate.marker.length >= marker.length && candidate.info.trim().length === 0;
305
305
  }
306
- /** Code brackets of one line: string literals and comments are skipped. */
306
+
307
307
  function codeBrackets(line, state) {
308
308
  const found = [];
309
309
  let quote;
@@ -344,15 +344,15 @@ function codeBrackets(line, state) {
344
344
  }
345
345
  return found;
346
346
  }
347
- /** Whether one bracket character opens a level. */
347
+
348
348
  function isOpener(ch) {
349
349
  return ch === "(" || ch === "[" || ch === "{";
350
350
  }
351
- /**
352
- * Scan a line sequence for the constructs it leaves open.
353
- * @param lines - the lines to scan, in file order.
354
- * @returns the open fence and the opening lines of every still-open bracket level.
355
- */
351
+
352
+
353
+
354
+
355
+
356
356
  function scan(lines) {
357
357
  const state = { inBlockComment: false };
358
358
  const openBracketLines = [];
@@ -378,13 +378,13 @@ function scan(lines) {
378
378
  openBracketLines
379
379
  };
380
380
  }
381
- /**
382
- * The indentation block a line sequence ends inside: its last non-blank line is
383
- * indented deeper than the nearest preceding shallower line, so a Python-style
384
- * block may still be open.
385
- * @param lines - the lines to analyze, in file order.
386
- * @returns the open indented block, or `undefined` when the tail is balanced.
387
- */
381
+
382
+
383
+
384
+
385
+
386
+
387
+
388
388
  function detectOpenIndent(lines) {
389
389
  const lastLine = lines.findLast((line) => line.trim().length > 0);
390
390
  if (lastLine === void 0) return void 0;
@@ -407,11 +407,11 @@ function detectOpenIndent(lines) {
407
407
  lineIndex: 0
408
408
  };
409
409
  }
410
- /**
411
- * The construct a window ends inside.
412
- * @param lines - the delivered window lines, in file order.
413
- * @returns the construct to close, or `undefined` when the tail is balanced.
414
- */
410
+
411
+
412
+
413
+
414
+
415
415
  function detectOpenTail(lines) {
416
416
  const facts = scan(lines);
417
417
  if (facts.fence !== void 0) return {
@@ -427,14 +427,14 @@ function detectOpenTail(lines) {
427
427
  };
428
428
  return detectOpenIndent(lines);
429
429
  }
430
- /**
431
- * The indentation block a window starts inside: the window's first non-blank
432
- * line is indented, and some earlier line is shallower, so the block header
433
- * lies above the window.
434
- * @param window - the delivered window lines, in file order.
435
- * @param preceding - the lines immediately before the window, in file order.
436
- * @returns the open indented block, or `undefined` when the window starts at a block boundary.
437
- */
430
+
431
+
432
+
433
+
434
+
435
+
436
+
437
+
438
438
  function detectOpenIndentHead(window, preceding) {
439
439
  const firstContent = window.find((line) => line.trim().length > 0);
440
440
  if (firstContent === void 0) return void 0;
@@ -448,15 +448,15 @@ function detectOpenIndentHead(window, preceding) {
448
448
  };
449
449
  }
450
450
  }
451
- /**
452
- * The construct a window starts inside, resolved from the lines that precede it.
453
- * A fence outranks a bracket block, which outranks an indented block. A
454
- * construct opened before the supplied preceding lines is not provable and
455
- * reports nothing, so the completion stays silent rather than guessing.
456
- * @param window - the delivered window lines, in file order.
457
- * @param preceding - the lines immediately before the window, in file order; the last entry is the line that precedes the window.
458
- * @returns the leading context, or `undefined` when the window starts at a construct boundary.
459
- */
451
+
452
+
453
+
454
+
455
+
456
+
457
+
458
+
459
+
460
460
  function detectOpenHead(window, preceding) {
461
461
  if (preceding.length === 0) return void 0;
462
462
  const facts = scan(preceding);
@@ -471,12 +471,12 @@ function detectOpenHead(window, preceding) {
471
471
  };
472
472
  return detectOpenIndentHead(window, preceding);
473
473
  }
474
- /**
475
- * How many of the following lines close an open construct.
476
- * @param construct - the construct {@link detectOpenTail} found.
477
- * @param appended - the lines following the window, in file order.
478
- * @returns the count of lines to append, or `0` when the sequence closes nothing.
479
- */
474
+
475
+
476
+
477
+
478
+
479
+
480
480
  function closureLength(construct, appended) {
481
481
  if (construct.kind === "fence") {
482
482
  for (const [index, line] of appended.entries()) if (closesFence(construct.marker, line)) return index + 1;
@@ -499,7 +499,7 @@ function closureLength(construct, appended) {
499
499
  }
500
500
  return cut + 1;
501
501
  }
502
- /** Leading indentation width of one line, counting a tab as four columns. */
502
+
503
503
  function indentWidth(line) {
504
504
  let width = 0;
505
505
  for (const ch of line) if (ch === " ") width += 1;
@@ -507,28 +507,28 @@ function indentWidth(line) {
507
507
  else break;
508
508
  return width;
509
509
  }
510
- //#endregion
511
- //#region lib/types/completion.js
512
- /**
513
- * Window completion for reads: the neighbouring lines that give a delivered
514
- * window its structural boundaries. The head is the single line that opened
515
- * the construct the window starts inside; the tail is the run of following
516
- * lines that closes the construct the window ends inside. Neither ever
517
- * modifies, reorders, or drops a delivered line — a window either gains
518
- * context or stays exactly as the caller asked for it.
519
- *
520
- * The head is one line because everything between that opening line and the
521
- * window is the content the caller chose to skip; the tail is a run because
522
- * the construct cannot be called closed until its closer arrives.
523
- * @module @deepseek-ai/dsh-tool-fs/completion
524
- */
525
- /**
526
- * Complete a window's structural boundaries.
527
- * @param lines - the delivered window lines, in file order.
528
- * @param supply - the line sources on each side of the window.
529
- * @param tuning - the hard caps for the added lines.
530
- * @returns the head and tail lines with the rules that produced them; `undefined` when neither side completes.
531
- */
510
+
511
+
512
+
513
+
514
+
515
+
516
+
517
+
518
+
519
+
520
+
521
+
522
+
523
+
524
+
525
+
526
+
527
+
528
+
529
+
530
+
531
+
532
532
  async function completeWindow(lines, supply, tuning) {
533
533
  if (lines.length === 0) return void 0;
534
534
  let head = [];
@@ -563,35 +563,35 @@ async function completeWindow(lines, supply, tuning) {
563
563
  ...tailRule === void 0 ? {} : { tailRule }
564
564
  };
565
565
  }
566
- //#endregion
567
- //#region lib/types/read.js
568
- /**
569
- * Model-facing UTF-8 read. It performs one provider stat for type, routing, and observed version,
570
- * streams large or size-unknown files, renders a bounded window, then emits the observation.
571
- * @module @deepseek-ai/dsh-tool-fs/src/read
572
- */
573
- /** Default and maximum number of lines returned by one `read` call (the `readLimit` config). */
566
+
567
+
568
+
569
+
570
+
571
+
572
+
573
+
574
574
  const READ_LIMIT = 2e3;
575
- /**
576
- * Default streaming threshold (the `readStreamMinSize` config): files at or
577
- * above this size stream; smaller files read whole into memory.
578
- */
575
+
576
+
577
+
578
+
579
579
  const STREAM_MIN_SIZE = 10 * 1024 * 1024;
580
580
  function parsePositiveInteger(value, name) {
581
581
  if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) throw new Error(`${name} must be a positive integer`);
582
582
  return value;
583
583
  }
584
- /**
585
- * Validate value constraints the schema DSL can't express. `maxLimit` is the deployment's line cap.
586
- * With repair enabled, the measured addressing failures are normalized first through
587
- * {@link repairReadArgs}: a 0-based or negative start index and an over-cap or
588
- * non-positive limit all carry unambiguous intent, so the value is substituted
589
- * and the rule reported instead of erroring.
590
- * @param args - the schema-validated raw tool arguments; `offset`/`limit` must be positive integers when given unrepairable shapes.
591
- * @param maxLimit - the configured line cap: both the default `limit` and the largest one accepted.
592
- * @param repairEnabled - the read repair engine's switch; disabled keeps the strict validation errors.
593
- * @returns the validated input with `offset` defaulted to 1 and `limit` to `maxLimit`, plus the rules that fired.
594
- */
584
+
585
+
586
+
587
+
588
+
589
+
590
+
591
+
592
+
593
+
594
+
595
595
  function parseReadArgs(args, maxLimit, repairEnabled = true) {
596
596
  if (args.file_path.trim().length === 0) throw new Error("file_path must be a non-empty string");
597
597
  const repair = repairEnabled ? repairReadArgs(args.offset, args.limit, maxLimit) : void 0;
@@ -606,20 +606,20 @@ function parseReadArgs(args, maxLimit, repairEnabled = true) {
606
606
  ...repair === void 0 ? {} : { repairs: repair.repairs }
607
607
  };
608
608
  }
609
- /**
610
- * The model-facing failure for a blocked duplicate read: the file is unchanged,
611
- * what was delivered and how many steps ago, and the ways past the refusal.
612
- */
609
+
610
+
611
+
612
+
613
613
  function duplicateMessage(verdict) {
614
614
  const { record, steps } = verdict;
615
615
  return `duplicate read blocked: lines ${record.offset}-${record.endLine} unchanged, delivered ${steps} step${steps === 1 ? "" : "s"} ago; that content is still above. Use offset=${record.endLine + 1}, or force=true.`;
616
616
  }
617
- /**
618
- * Register the `read` tool and its scope-aware system-prompt guidance.
619
- * @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service.
620
- * @param caps - the deployment's resolved read caps (plugin config after defaulting).
621
- * @param repair - the read repair tuning; disabled keeps every failure's verbatim error.
622
- */
617
+
618
+
619
+
620
+
621
+
622
+
623
623
  function applyReadTool(ctx, caps, repair) {
624
624
  ctx.systemPrompt.section({
625
625
  name: "tool:read",
@@ -848,17 +848,17 @@ function applyReadTool(ctx, caps, repair) {
848
848
  }
849
849
  }));
850
850
  }
851
- /**
852
- * Compute one {@link FileDiff} per hunk between `before` and `after`, each carrying the
853
- * applied change plus {@link DIFF_CONTEXT} context lines. Pure insertions use `oldText: null`,
854
- * patch-only no-newline markers are omitted, and scattered replacements remain separate hunks.
855
- *
856
- * @param path - the path stamped on every produced diff (the model-facing `file_path`; the
857
- * bridge relativizes it).
858
- * @param before - the file text before the change (the backend's LF-normalized diff basis).
859
- * @param after - the file text after the change, on the same basis.
860
- * @returns one diff per applied hunk, in file order; empty when the texts are identical.
861
- */
851
+
852
+
853
+
854
+
855
+
856
+
857
+
858
+
859
+
860
+
861
+
862
862
  function computeHunkDiffs(path, before, after) {
863
863
  const patch = structuredPatch("", "", before, after, void 0, void 0, { context: 3 });
864
864
  const diffs = [];
@@ -883,63 +883,63 @@ function computeHunkDiffs(path, before, after) {
883
883
  }
884
884
  return diffs;
885
885
  }
886
- /** Whether `value` is a valid {@link FileDiff} (defensive narrowing from opaque `meta`). */
886
+
887
887
  function isFileDiff(value) {
888
888
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
889
889
  const { path, oldText, newText } = value;
890
890
  return typeof path === "string" && (oldText === null || typeof oldText === "string") && typeof newText === "string";
891
891
  }
892
- /**
893
- * Narrow opaque live or replayed result metadata to non-empty file diffs. Malformed metadata
894
- * returns `undefined` so presentation can fall back instead of throwing during replay.
895
- * @param meta - result metadata.
896
- * @returns validated hunks, or `undefined` for absent or malformed data.
897
- */
892
+
893
+
894
+
895
+
896
+
897
+
898
898
  function diffsFromMeta(meta) {
899
899
  if (typeof meta !== "object" || meta === null || Array.isArray(meta)) return void 0;
900
900
  const diffs = meta.diffs;
901
901
  if (!Array.isArray(diffs) || diffs.length === 0 || !diffs.every(isFileDiff)) return void 0;
902
902
  return diffs;
903
903
  }
904
- //#endregion
905
- //#region lib/types/error.js
906
- /**
907
- * Model-facing diagnostics for guarded-mutation failures. Providers and
908
- * policies retain operation-specific causes, while this package owns the
909
- * stable message shown to the model.
910
- * @module @deepseek-ai/dsh-tool-fs/src/error
911
- */
912
- /**
913
- * Render the stable model-facing diagnostic for a guarded-mutation failure.
914
- * `FS_STALE_VERSION` keeps the provider's reason and appends its re-read
915
- * remedy. `FS_NOT_OBSERVED` replaces operation-specific policy/provider text
916
- * with one path-aware reason and read remedy. The original error remains the
917
- * cause, and both diagnostics preserve its code for machine routing. Anything
918
- * else passes through untouched.
919
- * @param error - the caught value from a write/edit execution.
920
- * @param displayPath - the resolved target path shown to the model.
921
- * @returns a remediated `FsError` for the two guarded-mutation codes, else the original value.
922
- */
904
+
905
+
906
+
907
+
908
+
909
+
910
+
911
+
912
+
913
+
914
+
915
+
916
+
917
+
918
+
919
+
920
+
921
+
922
+
923
923
  function remediateFsError(error, displayPath) {
924
924
  if (!(error instanceof FsError)) return error;
925
925
  if (error.code === "FS_NOT_OBSERVED") return new FsError(`cannot modify "${displayPath}": file has not been read — read the file, then retry`, error.code, { cause: error });
926
926
  if (error.code === "FS_STALE_VERSION") return new FsError(`${error.message} — re-read the file, then retry`, error.code, { cause: error });
927
927
  return error;
928
928
  }
929
- //#endregion
930
- //#region lib/types/write.js
931
- /**
932
- * Model-facing full-file write. It obtains an optional intent from the single policy slot, calls
933
- * `ctx.fs.writeText` without a stat, then records the resulting version; no policy means an
934
- * unconditional atomic create-or-overwrite.
935
- * @module @deepseek-ai/dsh-tool-fs/src/write
936
- */
937
- /**
938
- * Validate value constraints the schema DSL can't express: only a non-blank
939
- * `file_path` — an empty `content` is legitimate (it writes an empty file).
940
- * @param args - the schema-validated raw tool arguments.
941
- * @returns the camelCased input; `content` passes through untouched.
942
- */
929
+
930
+
931
+
932
+
933
+
934
+
935
+
936
+
937
+
938
+
939
+
940
+
941
+
942
+
943
943
  function parseWriteArgs(args) {
944
944
  if (args.file_path.trim().length === 0) throw new Error("file_path must be a non-empty string");
945
945
  return {
@@ -947,13 +947,13 @@ function parseWriteArgs(args) {
947
947
  content: args.content
948
948
  };
949
949
  }
950
- /**
951
- * Format a write outcome as one model-facing text block body.
952
- * @param displayPath - the backend-resolved path rendered in the envelope's `<path>` element.
953
- * @param outcome - the write outcome; its `operation` selects the Created/Updated wording.
954
- * @param hint - the drifted-path disclosure appended inside the content block, or undefined.
955
- * @returns the model-facing confirmation envelope (no file content is echoed back).
956
- */
950
+
951
+
952
+
953
+
954
+
955
+
956
+
957
957
  function formatWriteOutput(displayPath, outcome, hint) {
958
958
  return `<path>${displayPath}</path>
959
959
  <type>file</type>
@@ -961,12 +961,12 @@ function formatWriteOutput(displayPath, outcome, hint) {
961
961
  ${outcome.operation === "create" ? "Created" : "Updated"} file${hint === void 0 ? "" : `\n(hint: ${hint})`}
962
962
  </content>`;
963
963
  }
964
- /**
965
- * Register the `write` tool and its scope-aware system-prompt guidance.
966
- * @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service.
967
- * @param sandbox - the shared sandbox-escalation API (advertisement, mode stamping, denial mapping).
968
- * @param hints - the drifted-path hint tuning; disabled keeps creates silent.
969
- */
964
+
965
+
966
+
967
+
968
+
969
+
970
970
  function applyWriteTool(ctx, sandbox, hints) {
971
971
  ctx.systemPrompt.section({
972
972
  name: "tool:write",
@@ -1082,37 +1082,37 @@ function applyWriteTool(ctx, sandbox, hints) {
1082
1082
  }
1083
1083
  }));
1084
1084
  }
1085
- //#endregion
1086
- //#region lib/types/edit.js
1087
- /**
1088
- * Model-facing literal edit, unique-match by default. It obtains an optional guard from the
1089
- * single intent slot, calls `ctx.fs.editText` without a separate stat, then records the observed
1090
- * version; no policy means an unconditional atomic edit. When the literal match fails or is
1091
- * ambiguous, the `@deepseek-ai/dsh-fs-edit-repair` engine re-anchors the pair before the error
1092
- * reaches the model.
1093
- * @module @deepseek-ai/dsh-tool-fs/src/edit
1094
- */
1095
- /**
1096
- * Error codes that can mean "the target does not exist": the policy's unread
1097
- * refusal, the provider's stale/missing guards on a guarded edit, and the
1098
- * no-match failure FakeFs-style providers raise for absent content. Each is
1099
- * only treated as a drifted path after a stat confirms the target's absence —
1100
- * a present target means the code carries its own ordinary meaning (an unread
1101
- * existing file, real staleness, a failed literal match).
1102
- */
1085
+
1086
+
1087
+
1088
+
1089
+
1090
+
1091
+
1092
+
1093
+
1094
+
1095
+
1096
+
1097
+
1098
+
1099
+
1100
+
1101
+
1102
+
1103
1103
  const ABSENT_TARGET_CODES = new Set([
1104
1104
  "FS_NOT_FOUND",
1105
1105
  "FS_STALE_VERSION",
1106
1106
  "FS_EDIT_NOT_FOUND",
1107
1107
  "FS_NOT_OBSERVED"
1108
1108
  ]);
1109
- /**
1110
- * Validate value constraints the schema DSL can't express: a non-blank
1111
- * `file_path`, a non-empty `old_string`, and `old_string !== new_string`
1112
- * (an equal pair would be a guaranteed no-op edit).
1113
- * @param args - the schema-validated raw tool arguments.
1114
- * @returns the camelCased input with `replace_all` defaulted to false.
1115
- */
1109
+
1110
+
1111
+
1112
+
1113
+
1114
+
1115
+
1116
1116
  function parseEditArgs(args) {
1117
1117
  if (args.file_path.trim().length === 0) throw new Error("file_path must be a non-empty string");
1118
1118
  if (args.old_string.length === 0) throw new Error("old_string must be a non-empty string");
@@ -1124,32 +1124,32 @@ function parseEditArgs(args) {
1124
1124
  replaceAll: args.replace_all ?? false
1125
1125
  };
1126
1126
  }
1127
- /**
1128
- * Format an edit success (single-match or replace-all) as a Claude-style model-facing message.
1129
- * @param displayPath - the backend-resolved path shown to the model.
1130
- * @param replaceAll - selects the all-occurrences wording over the single-replacement one.
1131
- * @returns the confirmation sentence the model sees as the tool result.
1132
- */
1127
+
1128
+
1129
+
1130
+
1131
+
1132
+
1133
1133
  function formatEditOutput(displayPath, replaceAll) {
1134
1134
  return replaceAll ? `The file ${displayPath} has been updated. All occurrences were successfully replaced.` : `The file ${displayPath} has been updated successfully.`;
1135
1135
  }
1136
- /**
1137
- * Repair a failed or ambiguous literal edit through the fs-edit-repair engine.
1138
- * A repaired pair is applied through the same guarded `editText` seam with the
1139
- * original intent, so version guards and sandbox policy stay in force; the
1140
- * outcome carries the repair note for the model. Without a safe patch the
1141
- * remediated error is re-thrown, enriched with the engine's near-match hints.
1142
- * @param ctx - the plugin context, used to re-read the target for repair.
1143
- * @param sandbox - the shared sandbox-escalation API, used for denial mapping.
1144
- * @param repair - the resolved repair tuning.
1145
- * @param target - the resolved edit target.
1146
- * @param input - the validated edit arguments.
1147
- * @param intent - the intent resolved by the `fs/edit-intent` waterfall.
1148
- * @param exec - the tool execution context.
1149
- * @param sandboxPolicy - the per-call sandbox policy.
1150
- * @param error - the failure thrown by the original `editText` call.
1151
- * @returns the outcome of the repaired edit plus its model-facing note.
1152
- */
1136
+
1137
+
1138
+
1139
+
1140
+
1141
+
1142
+
1143
+
1144
+
1145
+
1146
+
1147
+
1148
+
1149
+
1150
+
1151
+
1152
+
1153
1153
  async function repairEditFailure(ctx, sandbox, repair, target, input, intent, exec, sandboxPolicy, error) {
1154
1154
  const mapped = sandbox.mapError(error, sandboxPolicy);
1155
1155
  if (!repair.enabled || input.replaceAll || !(mapped instanceof FsError) || mapped.code !== "FS_EDIT_NOT_FOUND" && mapped.code !== "FS_AMBIGUOUS_EDIT") throw remediateFsError(mapped, target.displayPath);
@@ -1178,12 +1178,12 @@ async function repairEditFailure(ctx, sandbox, repair, target, input, intent, ex
1178
1178
  if (hints.length === 0 || !(base instanceof FsError)) throw base;
1179
1179
  throw new FsError(`${base.message} ${hints.join(" ")}`, base.code, { cause: base });
1180
1180
  }
1181
- /**
1182
- * Register the `edit` tool and its scope-aware system-prompt guidance.
1183
- * @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service.
1184
- * @param sandbox - the shared sandbox-escalation API (advertisement, mode stamping, denial mapping).
1185
- * @param repair - the resolved tuning for the failed-match repair engine and missing-path re-anchoring.
1186
- */
1181
+
1182
+
1183
+
1184
+
1185
+
1186
+
1187
1187
  function applyEditTool(ctx, sandbox, repair) {
1188
1188
  ctx.systemPrompt.section({
1189
1189
  name: "tool:edit",
@@ -1316,21 +1316,21 @@ function applyEditTool(ctx, sandbox, repair) {
1316
1316
  }
1317
1317
  }));
1318
1318
  }
1319
- //#endregion
1320
- //#region lib/types/read-image.js
1321
- /**
1322
- * The model-facing `read_image` tool commits a PNG/JPEG/WebP/GIF file. A path
1323
- * without a file extension is identified from its file signature, while the
1324
- * attachment service's full decode stays authoritative. The mounted `ctx.fs`
1325
- * backend owns path resolution and read access; names only declare media type.
1326
- *
1327
- * The route gate is deliberately stricter than the host upload preflight. An
1328
- * image-reading tool is useful only when the exact calling route can inspect
1329
- * its result, so unknown capability refuses instead of relying on an adapter
1330
- * failure after filesystem and attachment work.
1331
- * @module @deepseek-ai/dsh-tool-fs/src/read-image
1332
- */
1333
- /** Extensions `read_image` accepts; magic-byte validation at the attachment service stays authoritative. */
1319
+
1320
+
1321
+
1322
+
1323
+
1324
+
1325
+
1326
+
1327
+
1328
+
1329
+
1330
+
1331
+
1332
+
1333
+
1334
1334
  const IMAGE_EXTENSIONS = {
1335
1335
  ".png": "image/png",
1336
1336
  ".jpg": "image/jpeg",
@@ -1362,11 +1362,11 @@ function matchesAscii(data, offset, value) {
1362
1362
  for (let index = 0; index < value.length; index += 1) if (data[offset + index] !== value.charCodeAt(index)) return false;
1363
1363
  return true;
1364
1364
  }
1365
- /**
1366
- * Identify the media type declared by a supported image file signature.
1367
- * @param data - file bytes read through the current filesystem backend.
1368
- * @returns the detected supported media type, or undefined for other bytes.
1369
- */
1365
+
1366
+
1367
+
1368
+
1369
+
1370
1370
  function sniffImageMediaType(data) {
1371
1371
  if (matchesBytes(data, 0, PNG_SIGNATURE)) return "image/png";
1372
1372
  if (matchesBytes(data, 0, JPEG_SIGNATURE)) return "image/jpeg";
@@ -1421,22 +1421,22 @@ const IMAGE_VALUE_SCHEMA = {
1421
1421
  }
1422
1422
  }
1423
1423
  };
1424
- /**
1425
- * Map a model-supplied path to its declared image media type by extension.
1426
- * @param filePath - the raw `file_path` argument (not yet resolved).
1427
- * @returns the declared media type, or undefined when the path does not claim an image.
1428
- */
1424
+
1425
+
1426
+
1427
+
1428
+
1429
1429
  function imageMediaTypeForPath(filePath) {
1430
1430
  return IMAGE_EXTENSIONS[extname(filePath).toLowerCase()];
1431
1431
  }
1432
- /**
1433
- * Enforce the strict image-capability gate for the calling route. Resolves the
1434
- * session's latest routed provider/model (request header config, then agent
1435
- * options) and requires the exact resolved route to declare `image` input explicitly.
1436
- * @param ctx - the plugin context used to resolve the optional `llm` service.
1437
- * @param exec - the tool-execution context supplying the calling agent.
1438
- * @param requestedPath - the raw, not-yet-resolved path rendered in refusal messages.
1439
- */
1432
+
1433
+
1434
+
1435
+
1436
+
1437
+
1438
+
1439
+
1440
1440
  async function assertImageCapableRoute(ctx, exec, requestedPath) {
1441
1441
  const routed = exec.agent?.session.requestHeader()?.config;
1442
1442
  const provider = routed?.provider ?? exec.agent?.options.provider;
@@ -1446,16 +1446,16 @@ async function assertImageCapableRoute(ctx, exec, requestedPath) {
1446
1446
  const active = await llm.resolveModelInfo(provider, model, exec.signal);
1447
1447
  if (active.inputModalities === void 0 || !active.inputModalities.includes("image")) throw new Error(`cannot read "${requestedPath}" as an image: model "${model}" does not declare image input; switch to an image-capable model to read images`);
1448
1448
  }
1449
- /** Refuse a media type outside the deployment's accepted set, naming the offending path. */
1449
+
1450
1450
  function assertDeploymentAccepts(attachments, mediaType, displayPath) {
1451
1451
  if (!attachments.imageLimits.mediaTypes.includes(mediaType)) throw new Error(`cannot read "${displayPath}": ${mediaType} images are not accepted by this deployment`);
1452
1452
  }
1453
- /**
1454
- * Re-brand a structured image outcome into the durable attachment reference an
1455
- * `ImageBlock` carries.
1456
- * @param image - the image metadata from the output schema.
1457
- * @returns the branded attachment reference.
1458
- */
1453
+
1454
+
1455
+
1456
+
1457
+
1458
+
1459
1459
  function imageRefFromValue(image) {
1460
1460
  return {
1461
1461
  attachmentId: AttachmentId(image.attachmentId),
@@ -1467,14 +1467,14 @@ function imageRefFromValue(image) {
1467
1467
  ...image.originalDimensions === void 0 ? {} : { originalDimensions: { ...image.originalDimensions } }
1468
1468
  };
1469
1469
  }
1470
- /**
1471
- * Format an image read as the model-facing envelope beside its image block.
1472
- * A downscaled read names the on-disk dimensions and the multiplier that maps
1473
- * coordinates measured on the attached image back onto the original file.
1474
- * @param displayPath - the backend-resolved path rendered in the envelope's `<path>` element.
1475
- * @param image - the image metadata to summarize.
1476
- * @returns the model-facing envelope; the image itself rides the adjacent image block.
1477
- */
1470
+
1471
+
1472
+
1473
+
1474
+
1475
+
1476
+
1477
+
1478
1478
  function formatImageReadOutput(displayPath, image) {
1479
1479
  let scaled = "";
1480
1480
  if (image.originalDimensions !== void 0) {
@@ -1489,11 +1489,11 @@ function formatImageReadOutput(displayPath, image) {
1489
1489
  ${image.mediaType} image, ${image.width}x${image.height} px, ${image.bytes} bytes${scaled}
1490
1490
  </content>`;
1491
1491
  }
1492
- /**
1493
- * Project one structured image read into its model-facing envelope and image.
1494
- * @param value - the image-read outcome.
1495
- * @returns the two content blocks used by native and nested dispatches.
1496
- */
1492
+
1493
+
1494
+
1495
+
1496
+
1497
1497
  function imageReadContent(value) {
1498
1498
  return [{
1499
1499
  type: "text",
@@ -1503,15 +1503,15 @@ function imageReadContent(value) {
1503
1503
  attachment: imageRefFromValue(value.image)
1504
1504
  }];
1505
1505
  }
1506
- /**
1507
- * Register the `read_image` tool into the given context. The composing plugin
1508
- * owns the attachments gate: `src/index.ts` calls this inside
1509
- * `ctx.inject(['attachments'], …)` so the tool exists only while a durable
1510
- * store is mounted. Execution still re-checks `ctx.get('attachments')` for
1511
- * direct callers and gates on the calling route's declared image input.
1512
- * @param ctx - the registration scope; execution uses its `fs` service plus
1513
- * the optional `attachments`/`llm` services.
1514
- */
1506
+
1507
+
1508
+
1509
+
1510
+
1511
+
1512
+
1513
+
1514
+
1515
1515
  function applyReadImageTool(ctx) {
1516
1516
  ctx.tools.register(defineTool({
1517
1517
  name: "read_image",
@@ -1597,29 +1597,29 @@ function applyReadImageTool(ctx) {
1597
1597
  }
1598
1598
  }));
1599
1599
  }
1600
- //#endregion
1601
- //#region lib/types/sandbox.js
1602
- /**
1603
- * The sandbox-escalation API shared by the `write` and `edit` tools: the
1604
- * per-call policy resolution, the advertised escalation fields, and the denial-marker
1605
- * mapping — all delegating the vocabulary and the fail-closed approval
1606
- * sequence to `@deepseek-ai/dsh-sandbox` (the same pieces `@deepseek-ai/dsh-tool-bash`
1607
- * uses), so bash and fs escalate identically. Built ONCE per plugin from
1608
- * `ctx.fs.sandboxMode` (the capability fact — is a confining backend mounted?)
1609
- * and shared by both mutating tools.
1610
- *
1611
- * @module @deepseek-ai/dsh-tool-fs/sandbox
1612
- */
1613
- /**
1614
- * The filesystem escalation API: advertisement gating, per-call policy
1615
- * resolution, the one-approved wider retry, and denial-marker mapping. A pure
1616
- * product of `ctx` at plugin apply time.
1617
- */
1600
+
1601
+
1602
+
1603
+
1604
+
1605
+
1606
+
1607
+
1608
+
1609
+
1610
+
1611
+
1612
+
1613
+
1614
+
1615
+
1616
+
1617
+
1618
1618
  var FsSandboxController = class {
1619
1619
  ctx;
1620
- /** The escalation targets this composition advertises (`[]` when no confining backend is mounted). */
1620
+
1621
1621
  escalationModes;
1622
- /** Shared per-session policy resolver, required by a confining backend. */
1622
+
1623
1623
  policy;
1624
1624
  constructor(ctx) {
1625
1625
  this.ctx = ctx;
@@ -1628,13 +1628,13 @@ var FsSandboxController = class {
1628
1628
  this.policy = defaultMode === void 0 ? void 0 : ctx.get("sandboxPolicy");
1629
1629
  if (defaultMode !== void 0 && this.policy === void 0) throw new Error("tool-fs: the mounted filesystem confines but ctx.sandboxPolicy is missing");
1630
1630
  }
1631
- /**
1632
- * The escalation schema fields for a mutating tool's `parameters`. Call it
1633
- * only under a confining backend (guard on {@link escalationModes}); the
1634
- * enum pins the closed target vocabulary, the strict-wider check happens per
1635
- * call at execution.
1636
- * @returns the two escalation parameter specs.
1637
- */
1631
+
1632
+
1633
+
1634
+
1635
+
1636
+
1637
+
1638
1638
  schemaFields() {
1639
1639
  return {
1640
1640
  sandbox_permissions: {
@@ -1648,19 +1648,19 @@ var FsSandboxController = class {
1648
1648
  }
1649
1649
  };
1650
1650
  }
1651
- /**
1652
- * The policy to stamp onto this mutation: an approved escalation grant (a
1653
- * strictly wider retry resolved through `ctx.approval` before anything
1654
- * executes), else the session's standing mode. Repeating the standing mode
1655
- * requires no approval. The calling session's cwd is
1656
- * always carried as the workspace root. Validates the escalation argument
1657
- * pairing first.
1658
- * @param toolName - the mutating tool's name, for the approval audit trail.
1659
- * @param args - the call's escalation arguments.
1660
- * @param exec - the tool-execution context (agent, callId, signal).
1661
- * @returns the policy to pass to the mutation, or undefined for an
1662
- * unsandboxed backend.
1663
- */
1651
+
1652
+
1653
+
1654
+
1655
+
1656
+
1657
+
1658
+
1659
+
1660
+
1661
+
1662
+
1663
+
1664
1664
  async resolvePolicy(toolName, args, exec) {
1665
1665
  validateEscalationArgs(args.sandbox_permissions, args.justification);
1666
1666
  const standingPolicy = this.policy?.resolve({ ...exec.agent ? { session: exec.agent.session } : {} });
@@ -1684,45 +1684,45 @@ var FsSandboxController = class {
1684
1684
  mode: approvedMode
1685
1685
  };
1686
1686
  }
1687
- /**
1688
- * Map a thrown provider error for the model: a `FS_SANDBOX_DENIED` becomes a
1689
- * `FsError` whose text is the shared `[sandbox: …]` denial marker plus the
1690
- * same-turn escalation hint, so a policy denial reads identically to bash's
1691
- * WHILE keeping the structured `FS_SANDBOX_DENIED` code — `ToolRuntime`
1692
- * populates `result.error` only for `HarnessError` instances, so a plain
1693
- * `Error` would strip the code retry/observers key off. Any other error
1694
- * passes through unchanged. A `FS_SANDBOX_DENIED` only arises under a
1695
- * confining backend, which always advertises the escalation fields, so the
1696
- * hint always applies here.
1697
- * @param error - the error thrown by the mutation.
1698
- * @param policy - the policy stamped onto the call (names the mode in the marker).
1699
- * @returns the error to throw — the marker `FsError` for a sandbox denial, else the original.
1700
- */
1687
+
1688
+
1689
+
1690
+
1691
+
1692
+
1693
+
1694
+
1695
+
1696
+
1697
+
1698
+
1699
+
1700
+
1701
1701
  mapError(error, policy) {
1702
1702
  if (!(error instanceof FsError) || error.code !== "FS_SANDBOX_DENIED") return error;
1703
1703
  const mode = policy.mode;
1704
1704
  return new FsError(`${sandboxDenialMarker(mode)}\n${escalationHintMarker("operation")}`, "FS_SANDBOX_DENIED", { cause: error });
1705
1705
  }
1706
1706
  };
1707
- //#endregion
1708
- //#region lib/types/index.js
1709
- /**
1710
- * Model-facing read, read_image, write, and edit tools over `ctx.fs`. This package owns schemas, validation,
1711
- * read windows, formatting, and observation events, never a concrete provider. An optional
1712
- * event policy supplies mutation guards; without one the tools use unconditional provider calls.
1713
- * @module @deepseek-ai/dsh-tool-fs
1714
- */
1715
- /** Cordis plugin name used by loader diagnostics. */
1707
+
1708
+
1709
+
1710
+
1711
+
1712
+
1713
+
1714
+
1715
+
1716
1716
  const name = "tool-fs";
1717
- /** Services required by the filesystem tool suite. */
1717
+
1718
1718
  const inject = [
1719
1719
  "tools",
1720
1720
  "fs",
1721
1721
  "systemPrompt"
1722
1722
  ];
1723
- /** Defaults for the read repair sub-tunables. */
1723
+
1724
1724
  const DEFAULT_DUPLICATE_STEPS = 4;
1725
- /** Defaults for the structural completion sub-tunables. */
1725
+
1726
1726
  const DEFAULT_COMPLETION_MAX_LINES = 40;
1727
1727
  const Config = z.object({
1728
1728
  readLimit: z.number().default(READ_LIMIT),
@@ -1767,11 +1767,11 @@ const Config = z.object({
1767
1767
  }),
1768
1768
  writeRepair: z.object({ pathHint: z.boolean().default(true) }).default({ pathHint: true })
1769
1769
  });
1770
- /** Every read cap counts lines/chars/bytes — a positive integer, or windowing arithmetic misbehaves silently. */
1770
+
1771
1771
  function assertPositiveInteger(name, value) {
1772
1772
  if (!Number.isInteger(value) || value < 1) throw new Error(`tool-fs: ${name} must be a positive integer`);
1773
1773
  }
1774
- /** Register the full `read`/`write`/`edit` filesystem tool suite, plus `read_image` while `attachments` is mounted. */
1774
+
1775
1775
  function apply(ctx, config) {
1776
1776
  const resolved = config;
1777
1777
  assertPositiveInteger("readLimit", resolved.readLimit);
@@ -1834,5 +1834,5 @@ function apply(ctx, config) {
1834
1834
  priorPaths: (session, limit) => sessionPaths.priorPaths(session, limit)
1835
1835
  });
1836
1836
  }
1837
- //#endregion
1837
+
1838
1838
  export { Config, apply, inject, name };