@arnilo/prism-coding-agent 0.0.4 → 0.0.6

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/CHANGELOG.md CHANGED
@@ -5,7 +5,25 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.0.6] - 2026-07-19
9
+
10
+ ### Added
11
+
12
+ - Finite validated defaults/hard caps for text scans, image/file/input/edit counts, shell wall time, display output, and total shell output.
13
+ - Exported coding limit constants plus bounded `ReadOperations.readText` page contracts.
14
+
15
+ ### Changed
16
+
17
+ - Text reads stream one bounded page instead of loading the entire file; edit/image reads use a shared bounded file reader after stat checks.
18
+ - Shell defaults to a 600-second timeout, kills the operation at 64 MiB combined output, and creates exclusive Unix `0600` spill files.
19
+ - Failed/aborted/timed-out/output-limited shell calls delete unpublished spill files; successful truncated output remains host-owned at `metadata.fullOutputPath`.
20
+ - Custom `ReadOperations` now require `readText` and `statFile`; custom `EditOperations` require `statFile` and receive byte/signal options.
21
+ - Removed non-exported filesystem edit-preview helpers that duplicated the edit tool's file read path.
22
+
23
+ ## [0.0.5] - 2026-07-16
24
+
25
+ - Pinned the required `@arnilo/prism` peer and package metadata to 0.0.5; runtime behavior is unchanged.
26
+
9
27
 
10
28
  ## [0.0.4] - 2026-07-14
11
29
 
package/README.md CHANGED
@@ -33,6 +33,8 @@ import { createReadOnlyTools } from "@arnilo/prism-coding-agent";
33
33
  const tools = createToolRegistry(createReadOnlyTools(process.cwd()));
34
34
  ```
35
35
 
36
+ Shared `ToolsOptions.executionPolicy` applies to every tool returned by full, all, and read-only aggregators unless a per-tool policy overrides it.
37
+
36
38
  Individual tools with options:
37
39
 
38
40
  ```ts
@@ -42,6 +44,8 @@ const shell = createShellTool(process.cwd(), {
42
44
  shellPath: "/bin/bash", // force bash; default: SHELL env → /bin/bash → sh
43
45
  commandPrefix: "set -euo pipefail",
44
46
  maxLines: 500,
47
+ timeout: 600,
48
+ maxTotalOutputBytes: 64 * 1024 * 1024,
45
49
  });
46
50
 
47
51
  const remoteWrite = createWriteTool(process.cwd(), {
@@ -56,10 +60,10 @@ const remoteWrite = createWriteTool(process.cwd(), {
56
60
 
57
61
  | Tool | Input | Result |
58
62
  | --- | --- | --- |
59
- | `shell` | `{ command, timeout? }` | Combined output + `metadata.exitCode`. Non-zero exit is **not** an error. |
60
- | `read` | `{ path, offset?, limit? }` | `TextContent` (text) or `[note, ImageContent]` (image). |
61
- | `write` | `{ path, content }` | `Successfully wrote N bytes (M lines) to <abs>`. |
62
- | `edit` | `{ path, edits: [{oldText,newText}] }` | `Successfully replaced N block(s)` + `metadata.{diff,patch,firstChangedLine}`. |
63
+ | `shell` | `{ command, timeout? }` | Combined output + `metadata.exitCode`; 600-second default timeout and 64 MiB total-output cap. Non-zero exit is **not** an error. |
64
+ | `read` | `{ path, offset?, limit? }` | Streamed bounded text page or bounded `[note, ImageContent]`. |
65
+ | `write` | `{ path, content }` | Bounded UTF-8 input; `Successfully wrote N bytes (M lines) to <abs>`. |
66
+ | `edit` | `{ path, edits: [{oldText,newText}] }` | Bounded target/input/count; `Successfully replaced N block(s)` + diff metadata. |
63
67
 
64
68
  ### pi name mapping
65
69
 
@@ -72,9 +76,11 @@ const remoteWrite = createWriteTool(process.cwd(), {
72
76
 
73
77
  Factories: `createShellTool`, `createReadTool`, `createWriteTool`, `createEditTool`, `createCodingTools`, `createReadOnlyTools`, `createAllTools`, `createLocalBashOperations`.
74
78
 
75
- Helpers: `detectSupportedImageMimeType`, `detectSupportedImageMimeTypeFromFile`, `getShellConfig`, `killProcessTree`, `waitForChildProcess`, `withFileMutationQueue`.
79
+ Helpers: `detectSupportedImageMimeType`, `detectSupportedImageMimeTypeFromFile`, `getShellConfig`, `killProcessTree`, `waitForChildProcess`, `withFileMutationQueue`. Default/hard coding limit constants are exported for host configuration.
80
+
81
+ Option/operation types: `ToolsOptions`, `ShellToolOptions`/`BashOperations`, `ReadToolOptions`/`ReadOperations`/`ReadTextOptions`/`ReadTextResult`, `WriteToolOptions`/`WriteOperations`, `EditToolOptions`/`EditOperations`/`EditToolDetails`.
76
82
 
77
- Option/operation types: `ToolsOptions`, `ShellToolOptions`/`BashOperations`, `ReadToolOptions`/`ReadOperations`, `WriteToolOptions`/`WriteOperations`, `EditToolOptions`/`EditOperations`/`EditToolDetails`.
83
+ Text reads stop after one page or `maxScanBytes` instead of loading the file. Custom `ReadOperations` must implement bounded `readText` and `statFile`; custom `EditOperations` must implement `statFile` and honor the supplied read cap/signal. Successful truncated shell output is retained in an exclusive Unix `0600` temp file owned by the host; timeout, abort, output-limit, and spill failures remove unpublished spill files. Hosts should delete published `metadata.fullOutputPath` files after use.
78
84
 
79
85
  ## License
80
86
 
@@ -0,0 +1,2 @@
1
+ /** Read one regular-file snapshot no larger than `maxBytes`; always closes its handle. */
2
+ export declare function readFileBounded(path: string, maxBytes: number, signal?: AbortSignal): Promise<Buffer>;
@@ -0,0 +1,25 @@
1
+ import { open } from "node:fs/promises";
2
+ /** Read one regular-file snapshot no larger than `maxBytes`; always closes its handle. */
3
+ export async function readFileBounded(path, maxBytes, signal) {
4
+ const handle = await open(path, "r");
5
+ try {
6
+ const size = (await handle.stat()).size;
7
+ if (size > maxBytes)
8
+ throw new Error(`File exceeds ${maxBytes} byte limit`);
9
+ const buffer = Buffer.allocUnsafe(size);
10
+ let offset = 0;
11
+ while (offset < size) {
12
+ if (signal?.aborted)
13
+ throw new Error("Operation aborted");
14
+ const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
15
+ if (bytesRead === 0)
16
+ break;
17
+ offset += bytesRead;
18
+ }
19
+ return buffer.subarray(0, offset);
20
+ }
21
+ finally {
22
+ await handle.close();
23
+ }
24
+ }
25
+ //# sourceMappingURL=bounded-file.js.map
@@ -75,19 +75,9 @@ export interface EditDiffResult {
75
75
  diff: string;
76
76
  firstChangedLine: number | undefined;
77
77
  }
78
- export interface EditDiffError {
79
- error: string;
80
- }
81
78
  /**
82
79
  * Generate a display-oriented diff string with line numbers and context.
83
80
  * Returns both the diff string and the first changed line number (in the new file).
84
81
  */
85
82
  export declare function generateDiffString(oldContent: string, newContent: string, contextLines?: number): EditDiffResult;
86
- /**
87
- * Compute the diff for one or more edit operations without applying them.
88
- * Used for preview before the edit tool executes.
89
- */
90
- export declare function computeEditsDiff(path: string, edits: Edit[], cwd: string): Promise<EditDiffResult | EditDiffError>;
91
- /** Compute the diff for a single edit operation without applying it. */
92
- export declare function computeEditDiff(path: string, oldText: string, newText: string, cwd: string): Promise<EditDiffResult | EditDiffError>;
93
83
  export {};
package/dist/edit-diff.js CHANGED
@@ -2,14 +2,11 @@
2
2
  * Shared diff computation utilities for the edit and similar tools.
3
3
  *
4
4
  * Behavioral port of pi's core/tools/edit-diff for @arnilo/prism-coding-agent.
5
- * stdlib (node:fs/promises, node:fs constants) plus the `diff` package for
5
+ * The `diff` package provides unified-patch / display-diff generation;
6
6
  * unified-patch / display-diff generation. Fuzzy matching, replacement
7
7
  * preservation, BOM/line-ending handling are dep-free.
8
8
  */
9
9
  import * as Diff from "diff";
10
- import { constants } from "node:fs";
11
- import { access, readFile } from "node:fs/promises";
12
- import { resolveToCwd } from "./path-utils.js";
13
10
  export function detectLineEnding(content) {
14
11
  const crlfIdx = content.indexOf("\r\n");
15
12
  const lfIdx = content.indexOf("\n");
@@ -387,33 +384,4 @@ export function generateDiffString(oldContent, newContent, contextLines = 4) {
387
384
  }
388
385
  return { diff: output.join("\n"), firstChangedLine };
389
386
  }
390
- /**
391
- * Compute the diff for one or more edit operations without applying them.
392
- * Used for preview before the edit tool executes.
393
- */
394
- export async function computeEditsDiff(path, edits, cwd) {
395
- const absolutePath = resolveToCwd(path, cwd);
396
- try {
397
- try {
398
- await access(absolutePath, constants.R_OK);
399
- }
400
- catch (error) {
401
- const errorMessage = error instanceof Error && "code" in error ? `Error code: ${String(error.code)}` : String(error);
402
- return { error: `Could not edit file: ${path}. ${errorMessage}.` };
403
- }
404
- const rawContent = await readFile(absolutePath, "utf-8");
405
- // Strip BOM before matching (LLM won't include invisible BOM in oldText).
406
- const { text: content } = stripBom(rawContent);
407
- const normalizedContent = normalizeToLF(content);
408
- const { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);
409
- return generateDiffString(baseContent, newContent);
410
- }
411
- catch (err) {
412
- return { error: err instanceof Error ? err.message : String(err) };
413
- }
414
- }
415
- /** Compute the diff for a single edit operation without applying it. */
416
- export async function computeEditDiff(path, oldText, newText, cwd) {
417
- return computeEditsDiff(path, [{ oldText, newText }], cwd);
418
- }
419
387
  //# sourceMappingURL=edit-diff.js.map
package/dist/edit.d.ts CHANGED
@@ -39,17 +39,36 @@ export interface EditToolDetails {
39
39
  * (e.g. SSH) while keeping the tool's matching + per-path serialization behavior.
40
40
  */
41
41
  export interface EditOperations {
42
- /** Read file contents as a Buffer. */
43
- readFile: (absolutePath: string) => Promise<Buffer>;
42
+ /** Read bounded file contents as a Buffer. */
43
+ readFile: (absolutePath: string, options: {
44
+ maxBytes: number;
45
+ signal?: AbortSignal;
46
+ }) => Promise<Buffer>;
44
47
  /** Write content to a file. */
45
- writeFile: (absolutePath: string, content: string) => Promise<void>;
48
+ writeFile: (absolutePath: string, content: string, options?: {
49
+ signal?: AbortSignal;
50
+ }) => Promise<void>;
46
51
  /** Check the file is readable and writable (throw if not). */
47
- access: (absolutePath: string) => Promise<void>;
52
+ access: (absolutePath: string, options?: {
53
+ signal?: AbortSignal;
54
+ }) => Promise<void>;
55
+ /** Return target size before the bounded read. */
56
+ statFile: (absolutePath: string, options?: {
57
+ signal?: AbortSignal;
58
+ }) => Promise<{
59
+ size: number;
60
+ }>;
48
61
  }
49
62
  export interface EditToolOptions {
50
63
  /** Structured pre-execution policy checked before filesystem writes. */
51
64
  executionPolicy?: ExecutionPolicy;
52
65
  /** Custom operations backend (default: local filesystem). */
53
66
  operations?: EditOperations;
67
+ /** Maximum target file bytes read for matching (default 8 MiB). */
68
+ maxFileBytes?: number;
69
+ /** Maximum aggregate UTF-8 bytes across old/new edit text (default 2 MiB). */
70
+ maxInputBytes?: number;
71
+ /** Maximum replacements per call (default 100). */
72
+ maxEdits?: number;
54
73
  }
55
74
  export declare function createEditTool(cwd: string, options?: EditToolOptions): ToolDefinition;
package/dist/edit.js CHANGED
@@ -1,13 +1,38 @@
1
- import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "node:fs/promises";
1
+ /**
2
+ * Edit tool: precise text replacement in an existing file via exact-then-fuzzy matching.
3
+ *
4
+ * Behavioral port of pi's core/tools/edit for @arnilo/prism-coding-agent, adapted to Prism's
5
+ * `ToolDefinition` contract. Faithfully ports the access → read → stripBom → line-ending-normalize →
6
+ * `applyEditsToNormalizedContent` (exact then fuzzy, with duplicate/overlap/empty/no-change guards) →
7
+ * write flow, serialized per-path via `withFileMutationQueue`. Also ports `prepareEditArguments`
8
+ * (tolerates models that send `edits` as a JSON string or as legacy top-level `oldText`/`newText`).
9
+ *
10
+ * Drops pi's TUI (`renderCall`/`renderResult`, live preview cache, theme/syntax-highlight).
11
+ *
12
+ * Deviations from pi (documented):
13
+ * - Abort + every failure (missing/unreadable file, no-match, duplicate, overlap, empty oldText,
14
+ * no-change) return a Prism `error` result (pi throws/rejects). On no-match the file is untouched
15
+ * because `applyEditsToNormalizedContent` throws before `writeFile`.
16
+ * - pi's per-tool `details: { diff, patch, firstChangedLine }` (TUI-facing) is surfaced as
17
+ * `ToolResult.metadata` (host-readable, keeps model context small — the model only sees the short
18
+ * `Successfully replaced N block(s)` confirmation).
19
+ * - The post-`writeFile` abort check is dropped (consistent with the write tool): if the write
20
+ * completed, the edit is real and is reported as success rather than a misleading "aborted".
21
+ */
22
+ import { Buffer } from "node:buffer";
23
+ import { access as fsAccess, stat as fsStat, writeFile as fsWriteFile, } from "node:fs/promises";
2
24
  import { constants } from "node:fs";
25
+ import { readFileBounded } from "./bounded-file.js";
3
26
  import { enforceExecutionPolicy } from "./execution-policy.js";
4
27
  import { resolveToCwd } from "./path-utils.js";
5
28
  import { withFileMutationQueue } from "./file-mutation-queue.js";
29
+ import { DEFAULT_MAX_EDIT_FILE_BYTES, DEFAULT_MAX_EDIT_INPUT_BYTES, DEFAULT_MAX_EDITS, HARD_MAX_EDIT_FILE_BYTES, HARD_MAX_EDIT_INPUT_BYTES, HARD_MAX_EDITS, validateCodingLimit, } from "./limits.js";
6
30
  import { applyEditsToNormalizedContent, detectLineEnding, generateDiffString, generateUnifiedPatch, normalizeToLF, restoreLineEndings, stripBom, } from "./edit-diff.js";
7
31
  const defaultEditOperations = {
8
- readFile: (path) => fsReadFile(path),
9
- writeFile: (path, content) => fsWriteFile(path, content, "utf-8"),
32
+ readFile: (path, options) => readFileBounded(path, options.maxBytes, options.signal),
33
+ writeFile: (path, content, options) => fsWriteFile(path, content, { encoding: "utf-8", signal: options?.signal }),
10
34
  access: (path) => fsAccess(path, constants.R_OK | constants.W_OK),
35
+ statFile: async (path) => ({ size: (await fsStat(path)).size }),
11
36
  };
12
37
  function errorResult(toolCallId, message) {
13
38
  return {
@@ -40,22 +65,32 @@ function prepareEditArguments(input) {
40
65
  }
41
66
  return { path, edits };
42
67
  }
43
- function validateEdits(edits) {
68
+ function validateEdits(edits, maxEdits, maxInputBytes) {
44
69
  if (!Array.isArray(edits) || edits.length === 0) {
45
70
  return "edits must contain at least one replacement.";
46
71
  }
72
+ if (edits.length > maxEdits)
73
+ return `edits contains ${edits.length} replacements, exceeds ${maxEdits} limit.`;
47
74
  const out = [];
75
+ let inputBytes = 0;
48
76
  for (let i = 0; i < edits.length; i++) {
49
77
  const e = edits[i];
50
78
  if (typeof e?.oldText !== "string" || typeof e?.newText !== "string") {
51
79
  return `edits[${i}] must have string oldText and newText.`;
52
80
  }
81
+ inputBytes += Buffer.byteLength(e.oldText, "utf-8") + Buffer.byteLength(e.newText, "utf-8");
82
+ if (inputBytes > maxInputBytes) {
83
+ return `edit input is ${inputBytes} bytes, exceeds ${maxInputBytes} byte limit.`;
84
+ }
53
85
  out.push({ oldText: e.oldText, newText: e.newText });
54
86
  }
55
87
  return out;
56
88
  }
57
89
  export function createEditTool(cwd, options) {
58
90
  const ops = options?.operations ?? defaultEditOperations;
91
+ const maxFileBytes = validateCodingLimit("maxFileBytes", options?.maxFileBytes ?? DEFAULT_MAX_EDIT_FILE_BYTES, HARD_MAX_EDIT_FILE_BYTES);
92
+ const maxInputBytes = validateCodingLimit("maxInputBytes", options?.maxInputBytes ?? DEFAULT_MAX_EDIT_INPUT_BYTES, HARD_MAX_EDIT_INPUT_BYTES);
93
+ const maxEdits = validateCodingLimit("maxEdits", options?.maxEdits ?? DEFAULT_MAX_EDITS, HARD_MAX_EDITS);
59
94
  return {
60
95
  name: "edit",
61
96
  description: "Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.",
@@ -85,11 +120,14 @@ export function createEditTool(cwd, options) {
85
120
  },
86
121
  async execute(args, context) {
87
122
  const toolCallId = context.toolCallId;
123
+ if (typeof args.edits === "string" && Buffer.byteLength(args.edits, "utf-8") > maxInputBytes) {
124
+ return errorResult(toolCallId, `edit input exceeds ${maxInputBytes} byte limit.`);
125
+ }
88
126
  const prepared = prepareEditArguments(args);
89
127
  if (prepared.path.length === 0) {
90
128
  return errorResult(toolCallId, "path is required and must be a non-empty string.");
91
129
  }
92
- const editsOrError = validateEdits(prepared.edits);
130
+ const editsOrError = validateEdits(prepared.edits, maxEdits, maxInputBytes);
93
131
  if (typeof editsOrError === "string") {
94
132
  return errorResult(toolCallId, editsOrError);
95
133
  }
@@ -101,7 +139,7 @@ export function createEditTool(cwd, options) {
101
139
  operation: "edit",
102
140
  paths: [absolutePath],
103
141
  risk: "medium",
104
- metadata: { editCount: edits.length, signal: context.signal },
142
+ metadata: { editCount: edits.length, sessionId: context.sessionId, runId: context.runId, signal: context.signal },
105
143
  }, toolCallId, "edit");
106
144
  if (!policyCheck.allowed)
107
145
  return policyCheck.result;
@@ -111,7 +149,7 @@ export function createEditTool(cwd, options) {
111
149
  return errorResult(toolCallId, "Operation aborted");
112
150
  // Check the file is readable + writable.
113
151
  try {
114
- await ops.access(allowedPath);
152
+ await ops.access(allowedPath, { signal: context.signal });
115
153
  }
116
154
  catch (error) {
117
155
  if (context.signal?.aborted)
@@ -122,7 +160,16 @@ export function createEditTool(cwd, options) {
122
160
  }
123
161
  if (context.signal?.aborted)
124
162
  return errorResult(toolCallId, "Operation aborted");
125
- const buffer = await ops.readFile(allowedPath);
163
+ const { size } = await ops.statFile(allowedPath, { signal: context.signal });
164
+ if (size > maxFileBytes) {
165
+ return errorResult(toolCallId, `Edit target is ${size} bytes, exceeds ${maxFileBytes} byte limit.`);
166
+ }
167
+ if (context.signal?.aborted)
168
+ return errorResult(toolCallId, "Operation aborted");
169
+ const buffer = await ops.readFile(allowedPath, { maxBytes: maxFileBytes, signal: context.signal });
170
+ if (buffer.length > maxFileBytes) {
171
+ return errorResult(toolCallId, `Edit target is ${buffer.length} bytes, exceeds ${maxFileBytes} byte limit.`);
172
+ }
126
173
  if (context.signal?.aborted)
127
174
  return errorResult(toolCallId, "Operation aborted");
128
175
  const rawContent = buffer.toString("utf-8");
@@ -143,7 +190,7 @@ export function createEditTool(cwd, options) {
143
190
  if (context.signal?.aborted)
144
191
  return errorResult(toolCallId, "Operation aborted");
145
192
  const finalContent = bom + restoreLineEndings(newContent, originalEnding);
146
- await ops.writeFile(allowedPath, finalContent);
193
+ await ops.writeFile(allowedPath, finalContent, { signal: context.signal });
147
194
  const diffResult = generateDiffString(baseContent, newContent);
148
195
  const patch = generateUnifiedPatch(prepared.path, baseContent, newContent);
149
196
  return {
package/dist/index.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  export { createShellTool, createLocalBashOperations, getShellConfig, killProcessTree, waitForChildProcess, } from "./shell.js";
2
2
  export type { ShellToolOptions, ShellConfig, BashOperations, BashExecOptions, BashSpawnContext, BashSpawnHook, } from "./shell.js";
3
3
  export { createReadTool, detectSupportedImageMimeType, detectSupportedImageMimeTypeFromFile, DEFAULT_MAX_IMAGE_BYTES, } from "./read.js";
4
- export type { ReadToolOptions, ReadOperations, TransformImage, TransformImageInput, } from "./read.js";
4
+ export type { ReadToolOptions, ReadOperations, ReadTextOptions, ReadTextResult, TransformImage, TransformImageInput, } from "./read.js";
5
5
  export { createWriteTool } from "./write.js";
6
6
  export type { WriteToolOptions, WriteOperations } from "./write.js";
7
7
  export { createEditTool } from "./edit.js";
8
8
  export type { EditToolOptions, EditOperations, EditToolDetails, Edit } from "./edit.js";
9
9
  export { withFileMutationQueue } from "./file-mutation-queue.js";
10
10
  export { enforceExecutionPolicy } from "./execution-policy.js";
11
+ export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, DEFAULT_MAX_TEXT_SCAN_BYTES, DEFAULT_MAX_WRITE_BYTES, DEFAULT_MAX_EDIT_FILE_BYTES, DEFAULT_MAX_EDIT_INPUT_BYTES, DEFAULT_MAX_EDITS, DEFAULT_SHELL_TIMEOUT_SECONDS, DEFAULT_MAX_TOTAL_OUTPUT_BYTES, HARD_MAX_BYTES, HARD_MAX_LINES, HARD_MAX_TEXT_SCAN_BYTES, HARD_MAX_IMAGE_BYTES, HARD_MAX_WRITE_BYTES, HARD_MAX_EDIT_FILE_BYTES, HARD_MAX_EDIT_INPUT_BYTES, HARD_MAX_EDITS, HARD_SHELL_TIMEOUT_SECONDS, HARD_MAX_TOTAL_OUTPUT_BYTES, } from "./limits.js";
11
12
  import type { ExecutionPolicy, ToolDefinition } from "@arnilo/prism";
12
13
  import type { ShellToolOptions } from "./shell.js";
13
14
  import type { ReadToolOptions } from "./read.js";
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ export { createEditTool } from "./edit.js";
11
11
  // --- generic primitives (re-exported for hosts that want them) ---
12
12
  export { withFileMutationQueue } from "./file-mutation-queue.js";
13
13
  export { enforceExecutionPolicy } from "./execution-policy.js";
14
+ export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, DEFAULT_MAX_TEXT_SCAN_BYTES, DEFAULT_MAX_WRITE_BYTES, DEFAULT_MAX_EDIT_FILE_BYTES, DEFAULT_MAX_EDIT_INPUT_BYTES, DEFAULT_MAX_EDITS, DEFAULT_SHELL_TIMEOUT_SECONDS, DEFAULT_MAX_TOTAL_OUTPUT_BYTES, HARD_MAX_BYTES, HARD_MAX_LINES, HARD_MAX_TEXT_SCAN_BYTES, HARD_MAX_IMAGE_BYTES, HARD_MAX_WRITE_BYTES, HARD_MAX_EDIT_FILE_BYTES, HARD_MAX_EDIT_INPUT_BYTES, HARD_MAX_EDITS, HARD_SHELL_TIMEOUT_SECONDS, HARD_MAX_TOTAL_OUTPUT_BYTES, } from "./limits.js";
14
15
  import { createShellTool } from "./shell.js";
15
16
  import { createReadTool } from "./read.js";
16
17
  import { createWriteTool } from "./write.js";
@@ -34,7 +35,7 @@ export function createCodingTools(cwd, options) {
34
35
  }
35
36
  /** Read-only subset: `read` only (this package ships no grep/find/ls). */
36
37
  export function createReadOnlyTools(cwd, options) {
37
- return [createReadTool(cwd, options?.read)];
38
+ return [createReadTool(cwd, withSharedExecutionPolicy(options?.read, options?.executionPolicy))];
38
39
  }
39
40
  /** Every tool this package provides — identical to {@link createCodingTools} for now. */
40
41
  export function createAllTools(cwd, options) {
@@ -0,0 +1,22 @@
1
+ export declare const DEFAULT_MAX_LINES = 2000;
2
+ export declare const HARD_MAX_LINES = 100000;
3
+ export declare const DEFAULT_MAX_BYTES: number;
4
+ export declare const HARD_MAX_BYTES: number;
5
+ export declare const DEFAULT_MAX_TEXT_SCAN_BYTES: number;
6
+ export declare const HARD_MAX_TEXT_SCAN_BYTES: number;
7
+ export declare const DEFAULT_MAX_IMAGE_BYTES = 10000000;
8
+ export declare const HARD_MAX_IMAGE_BYTES: number;
9
+ export declare const DEFAULT_MAX_WRITE_BYTES: number;
10
+ export declare const HARD_MAX_WRITE_BYTES: number;
11
+ export declare const DEFAULT_MAX_EDIT_FILE_BYTES: number;
12
+ export declare const HARD_MAX_EDIT_FILE_BYTES: number;
13
+ export declare const DEFAULT_MAX_EDIT_INPUT_BYTES: number;
14
+ export declare const HARD_MAX_EDIT_INPUT_BYTES: number;
15
+ export declare const DEFAULT_MAX_EDITS = 100;
16
+ export declare const HARD_MAX_EDITS = 1000;
17
+ export declare const DEFAULT_SHELL_TIMEOUT_SECONDS = 600;
18
+ export declare const HARD_SHELL_TIMEOUT_SECONDS = 3600;
19
+ export declare const DEFAULT_MAX_TOTAL_OUTPUT_BYTES: number;
20
+ export declare const HARD_MAX_TOTAL_OUTPUT_BYTES: number;
21
+ /** Validate one configurable coding resource limit. Invalid values fail instead of clamping. */
22
+ export declare function validateCodingLimit(name: string, value: number, hardCap: number): number;
package/dist/limits.js ADDED
@@ -0,0 +1,28 @@
1
+ export const DEFAULT_MAX_LINES = 2_000;
2
+ export const HARD_MAX_LINES = 100_000;
3
+ export const DEFAULT_MAX_BYTES = 50 * 1024;
4
+ export const HARD_MAX_BYTES = 1024 * 1024;
5
+ export const DEFAULT_MAX_TEXT_SCAN_BYTES = 64 * 1024 * 1024;
6
+ export const HARD_MAX_TEXT_SCAN_BYTES = 1024 * 1024 * 1024;
7
+ export const DEFAULT_MAX_IMAGE_BYTES = 10_000_000;
8
+ export const HARD_MAX_IMAGE_BYTES = 32 * 1024 * 1024;
9
+ export const DEFAULT_MAX_WRITE_BYTES = 8 * 1024 * 1024;
10
+ export const HARD_MAX_WRITE_BYTES = 64 * 1024 * 1024;
11
+ export const DEFAULT_MAX_EDIT_FILE_BYTES = 8 * 1024 * 1024;
12
+ export const HARD_MAX_EDIT_FILE_BYTES = 64 * 1024 * 1024;
13
+ export const DEFAULT_MAX_EDIT_INPUT_BYTES = 2 * 1024 * 1024;
14
+ export const HARD_MAX_EDIT_INPUT_BYTES = 16 * 1024 * 1024;
15
+ export const DEFAULT_MAX_EDITS = 100;
16
+ export const HARD_MAX_EDITS = 1_000;
17
+ export const DEFAULT_SHELL_TIMEOUT_SECONDS = 600;
18
+ export const HARD_SHELL_TIMEOUT_SECONDS = 3_600;
19
+ export const DEFAULT_MAX_TOTAL_OUTPUT_BYTES = 64 * 1024 * 1024;
20
+ export const HARD_MAX_TOTAL_OUTPUT_BYTES = 1024 * 1024 * 1024;
21
+ /** Validate one configurable coding resource limit. Invalid values fail instead of clamping. */
22
+ export function validateCodingLimit(name, value, hardCap) {
23
+ if (!Number.isSafeInteger(value) || value < 1 || value > hardCap) {
24
+ throw new Error(`${name} must be a positive safe integer at most ${hardCap}`);
25
+ }
26
+ return value;
27
+ }
28
+ //# sourceMappingURL=limits.js.map
@@ -1,26 +1,25 @@
1
- import type { TruncationResult } from "./truncate.js";
1
+ import { type TruncationResult } from "./truncate.js";
2
2
  export interface OutputAccumulatorOptions {
3
3
  maxLines?: number;
4
4
  maxBytes?: number;
5
+ maxTotalOutputBytes?: number;
5
6
  tempFilePrefix?: string;
7
+ onLimit?: () => void;
8
+ onStorageError?: () => void;
6
9
  }
7
10
  export interface OutputSnapshot {
8
11
  content: string;
9
12
  truncation: TruncationResult;
10
13
  fullOutputPath?: string;
11
14
  }
12
- /**
13
- * Incrementally tracks streaming output with bounded memory.
14
- *
15
- * Appends decode chunks with a streaming UTF-8 decoder, keeps only a decoded
16
- * tail for display snapshots, and opens a temp file when the full output needs
17
- * to be preserved.
18
- */
19
15
  export declare class OutputAccumulator {
20
16
  private readonly maxLines;
21
17
  private readonly maxBytes;
18
+ private readonly maxTotalOutputBytes;
22
19
  private readonly maxRollingBytes;
23
20
  private readonly tempFilePrefix;
21
+ private readonly onLimit?;
22
+ private readonly onStorageError?;
24
23
  private readonly decoder;
25
24
  private rawChunks;
26
25
  private tailText;
@@ -33,19 +32,27 @@ export declare class OutputAccumulator {
33
32
  private currentLineBytes;
34
33
  private hasOpenLine;
35
34
  private finished;
35
+ private exceeded;
36
36
  private tempFilePath?;
37
- private tempFileStream?;
37
+ private tempFileFd?;
38
+ private tempFileError?;
38
39
  constructor(options?: OutputAccumulatorOptions);
39
- append(data: Buffer): void;
40
+ append(data: Buffer): boolean;
40
41
  finish(): void;
41
42
  snapshot(options?: {
42
43
  persistIfTruncated?: boolean;
43
44
  }): OutputSnapshot;
44
45
  closeTempFile(): Promise<void>;
46
+ cleanupTempFile(): Promise<void>;
45
47
  getLastLineBytes(): number;
48
+ getTotalRawBytes(): number;
49
+ isOutputLimitExceeded(): boolean;
50
+ hasStorageError(): boolean;
46
51
  private appendDecodedText;
47
52
  private trimTail;
48
53
  private getSnapshotText;
49
54
  private shouldUseTempFile;
50
55
  private ensureTempFile;
56
+ private writeTemp;
57
+ private recordTempError;
51
58
  }