@arnilo/prism-coding-agent 0.1.5 → 0.1.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 +7 -0
- package/dist/delete.js +80 -8
- package/dist/glob-match.d.ts +17 -2
- package/dist/glob-match.js +64 -3
- package/dist/glob.d.ts +2 -0
- package/dist/glob.js +8 -1
- package/dist/index.d.ts +1 -1
- package/dist/read.d.ts +33 -0
- package/dist/read.js +29 -0
- package/dist/repository.d.ts +2 -0
- package/dist/repository.js +14 -4
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.6] - 2026-08-11
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Bounded PDF/Office document reader** (plan 018 closeout `doc-reader`): additive `documentReader` slot on `createReadTool` — `DocumentReader.extract` runs after image sniffing, magic-byte format gating (never extension sniffing), null fall-through to the 0.1.5 text path, input/page/text caps re-checked in the read flow; the optional peer package `@arnilo/prism-document-reader` supplies the concrete parsers.
|
|
7
|
+
- **Recursive delete + brace-expanding glob** (plan 018 closeout `delete-glob`): `delete` gains per-call opt-in `recursive: true` (iterative post-order walk, symlink children unlinked never followed, fan-out cap 10,000 default / 100,000 hard, partial deletion reported never silent, `maxEntries` bound); `glob` gains host-selected and per-call `braceExpansion` (`{a,b}` textual expansion, max 128 alternatives / 4096 expanded bytes, unbalanced/nested/empty braces and overflow fail closed; default matcher semantics unchanged).
|
|
8
|
+
- **Checkpoint persistence for loaded-skill bodies** (plan 018 closeout `checkpoint-bodies`): durable runs may set `includeSkillBodies: true` on BOTH run and resume options — the exact loaded-skill instructions ride the checkpoint so resume re-renders them registry-independently (no `load_skill` round-trip); names-only stays the default, 0.1.3 checkpoint shapes byte-identical, `maxStateBytes` refuses oversize bodies.
|
|
9
|
+
|
|
3
10
|
## [0.1.5] - 2026-08-11
|
|
4
11
|
|
|
5
12
|
### Changed
|
package/dist/delete.js
CHANGED
|
@@ -3,6 +3,11 @@ import { CODING_LOCAL_EFFECT } from "./effects.js";
|
|
|
3
3
|
import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
4
4
|
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
5
5
|
import { resolveContainedMutationPath } from "./mutation-path.js";
|
|
6
|
+
// Per-call recursive-delete fan-out cap: default 10,000 entries, hard 100,000.
|
|
7
|
+
// ponytail: single global constants (not tunable options) — the recursive flag is
|
|
8
|
+
// per-call opt-in; hosts that need a different ceiling can deny via executionPolicy.
|
|
9
|
+
const DEFAULT_MAX_RECURSIVE_DELETE_ENTRIES = 10_000;
|
|
10
|
+
const HARD_MAX_RECURSIVE_DELETE_ENTRIES = 100_000;
|
|
6
11
|
const defaultDeleteOperations = {
|
|
7
12
|
lstat: async (path) => {
|
|
8
13
|
const st = await lstat(path);
|
|
@@ -30,11 +35,22 @@ export function createDeleteTool(cwd, options) {
|
|
|
30
35
|
return {
|
|
31
36
|
name: "delete",
|
|
32
37
|
effect: CODING_LOCAL_EFFECT,
|
|
33
|
-
description: "High-risk: permanently delete a single file or empty directory in the workspace. Non-empty directories are rejected (
|
|
38
|
+
description: "High-risk: permanently delete a single file or empty directory in the workspace. Non-empty directories are rejected unless recursive: true (opt-in per call; recursive refuses symlinked-directory traversal and enforces a per-call fan-out cap, default 10,000 entries). No trash/recycle — host undo is not automatic. Prefer edit/write when content can be fixed in place.",
|
|
34
39
|
parameters: {
|
|
35
40
|
type: "object",
|
|
36
41
|
properties: {
|
|
37
|
-
path: {
|
|
42
|
+
path: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "Path to the file, empty directory, or (with recursive: true) directory tree to delete (relative or absolute)",
|
|
45
|
+
},
|
|
46
|
+
recursive: {
|
|
47
|
+
type: "boolean",
|
|
48
|
+
description: "Opt-in per-call recursive directory delete (default false). Symlink children are unlinked as links, never followed; fan-out capped.",
|
|
49
|
+
},
|
|
50
|
+
maxEntries: {
|
|
51
|
+
type: "number",
|
|
52
|
+
description: `Per-call recursive fan-out cap (default ${DEFAULT_MAX_RECURSIVE_DELETE_ENTRIES}, hard ${HARD_MAX_RECURSIVE_DELETE_ENTRIES})`,
|
|
53
|
+
},
|
|
38
54
|
},
|
|
39
55
|
required: ["path"],
|
|
40
56
|
additionalProperties: false,
|
|
@@ -42,6 +58,15 @@ export function createDeleteTool(cwd, options) {
|
|
|
42
58
|
async execute(args, context) {
|
|
43
59
|
const toolCallId = context.toolCallId;
|
|
44
60
|
const path = typeof args.path === "string" ? args.path : "";
|
|
61
|
+
const recursive = args.recursive === true;
|
|
62
|
+
let maxEntries = DEFAULT_MAX_RECURSIVE_DELETE_ENTRIES;
|
|
63
|
+
if (args.maxEntries !== undefined) {
|
|
64
|
+
const raw = args.maxEntries;
|
|
65
|
+
if (!Number.isInteger(raw) || raw < 1 || raw > HARD_MAX_RECURSIVE_DELETE_ENTRIES) {
|
|
66
|
+
return errorResult(toolCallId, `maxEntries must be an integer between 1 and ${HARD_MAX_RECURSIVE_DELETE_ENTRIES}`);
|
|
67
|
+
}
|
|
68
|
+
maxEntries = raw;
|
|
69
|
+
}
|
|
45
70
|
if (path.length === 0) {
|
|
46
71
|
return errorResult(toolCallId, "path is required and must be a non-empty string.");
|
|
47
72
|
}
|
|
@@ -94,16 +119,63 @@ export function createDeleteTool(cwd, options) {
|
|
|
94
119
|
}
|
|
95
120
|
if (st.isDirectory()) {
|
|
96
121
|
const entries = await ops.readdir(allowedPath, { signal: context.signal });
|
|
97
|
-
if (entries.length > 0) {
|
|
98
|
-
return errorResult(toolCallId, `Directory is not empty: ${path}. Recursive delete is not supported.`);
|
|
122
|
+
if (entries.length > 0 && !recursive) {
|
|
123
|
+
return errorResult(toolCallId, `Directory is not empty: ${path}. Recursive delete is not supported (set recursive: true to opt in per call).`);
|
|
124
|
+
}
|
|
125
|
+
if (entries.length === 0) {
|
|
126
|
+
await ops.rmdir(allowedPath, { signal: context.signal });
|
|
127
|
+
options?.onEvent?.({ type: "file_changed", path: allowedPath, op: "delete", toolCallId });
|
|
128
|
+
return {
|
|
129
|
+
toolCallId,
|
|
130
|
+
name: "delete",
|
|
131
|
+
content: [{ type: "text", text: `Successfully deleted empty directory ${allowedPath}` }],
|
|
132
|
+
metadata: { path: allowedPath, kind: "directory" },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// Recursive delete: iterative post-order walk. Symlink children are
|
|
136
|
+
// UNLINKED as links and never followed — a symlinked directory can
|
|
137
|
+
// never drag the deletion outside the workspace root. The walk counts
|
|
138
|
+
// every entry against the per-call fan-out cap and checks the abort
|
|
139
|
+
// signal per entry; exceeding the cap stops with an error naming it.
|
|
140
|
+
const stack = [{ dir: allowedPath }];
|
|
141
|
+
const dirs = [allowedPath];
|
|
142
|
+
let count = 0;
|
|
143
|
+
while (stack.length > 0) {
|
|
144
|
+
if (context.signal?.aborted) {
|
|
145
|
+
return errorResult(toolCallId, `Operation aborted after deleting ${count} entries`);
|
|
146
|
+
}
|
|
147
|
+
const { dir } = stack.pop();
|
|
148
|
+
const children = await ops.readdir(dir, { signal: context.signal });
|
|
149
|
+
for (const child of children) {
|
|
150
|
+
count++;
|
|
151
|
+
if (count > maxEntries) {
|
|
152
|
+
return errorResult(toolCallId, `Recursive delete exceeded the per-call fan-out cap of ${maxEntries} entries after deleting ${count - 1} entries; nothing beyond the cap was touched.`);
|
|
153
|
+
}
|
|
154
|
+
const childPath = `${dir}/${child}`;
|
|
155
|
+
const childStat = await ops.lstat(childPath, { signal: context.signal });
|
|
156
|
+
if (childStat.isDirectory()) {
|
|
157
|
+
dirs.push(childPath);
|
|
158
|
+
stack.push({ dir: childPath });
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
await ops.unlink(childPath, { signal: context.signal });
|
|
162
|
+
options?.onEvent?.({ type: "file_changed", path: childPath, op: "delete", toolCallId });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Remove directories deepest-first, then the root directory itself.
|
|
167
|
+
for (let i = dirs.length - 1; i >= 0; i--) {
|
|
168
|
+
if (context.signal?.aborted) {
|
|
169
|
+
return errorResult(toolCallId, `Operation aborted after deleting ${count} entries`);
|
|
170
|
+
}
|
|
171
|
+
await ops.rmdir(dirs[i], { signal: context.signal });
|
|
172
|
+
options?.onEvent?.({ type: "file_changed", path: dirs[i], op: "delete", toolCallId });
|
|
99
173
|
}
|
|
100
|
-
await ops.rmdir(allowedPath, { signal: context.signal });
|
|
101
|
-
options?.onEvent?.({ type: "file_changed", path: allowedPath, op: "delete", toolCallId });
|
|
102
174
|
return {
|
|
103
175
|
toolCallId,
|
|
104
176
|
name: "delete",
|
|
105
|
-
content: [{ type: "text", text: `Successfully deleted
|
|
106
|
-
metadata: { path: allowedPath, kind: "directory" },
|
|
177
|
+
content: [{ type: "text", text: `Successfully deleted ${allowedPath} (${count} entries)` }],
|
|
178
|
+
metadata: { path: allowedPath, kind: "directory", recursive: true, entriesDeleted: count },
|
|
107
179
|
};
|
|
108
180
|
}
|
|
109
181
|
return errorResult(toolCallId, `Unsupported file type: ${path}`);
|
package/dist/glob-match.d.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Minimal glob matcher: `*`, `?`, and `**` only.
|
|
2
|
+
* Minimal glob matcher: `*`, `?`, and `**` only. Brace expansion is an opt-in,
|
|
3
|
+
* bounded extension (see `expandGlobBraces`). No regex, no dependency.
|
|
3
4
|
*/
|
|
4
|
-
export
|
|
5
|
+
export interface BraceExpansionOptions {
|
|
6
|
+
/** Max alternatives produced by expansion (default 128). */
|
|
7
|
+
maxAlternatives?: number;
|
|
8
|
+
/** Max total bytes across all expanded alternatives (default 4096). */
|
|
9
|
+
maxExpandedBytes?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Expand `{a,b}` groups in a pattern (cartesian across multiple groups).
|
|
13
|
+
* Bounded: max alternatives and max total expanded bytes; unbalanced or nested
|
|
14
|
+
* braces fail closed. With no braces (or empty text), returns `[pattern]`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function expandGlobBraces(pattern: string, options?: BraceExpansionOptions): string[];
|
|
17
|
+
export declare function validateGlobPattern(pattern: string, maxPatternBytes: number, options?: {
|
|
18
|
+
braceExpansion?: boolean;
|
|
19
|
+
}): void;
|
|
5
20
|
/** Match a workspace-relative path against a glob pattern using `/` separators. */
|
|
6
21
|
export declare function matchGlobPattern(pattern: string, path: string): boolean;
|
package/dist/glob-match.js
CHANGED
|
@@ -1,7 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Minimal glob matcher: `*`, `?`, and `**` only.
|
|
2
|
+
* Minimal glob matcher: `*`, `?`, and `**` only. Brace expansion is an opt-in,
|
|
3
|
+
* bounded extension (see `expandGlobBraces`). No regex, no dependency.
|
|
3
4
|
*/
|
|
4
|
-
|
|
5
|
+
const DEFAULT_MAX_BRACE_ALTERNATIVES = 128;
|
|
6
|
+
const DEFAULT_MAX_BRACE_EXPANDED_BYTES = 4_096;
|
|
7
|
+
/**
|
|
8
|
+
* Expand `{a,b}` groups in a pattern (cartesian across multiple groups).
|
|
9
|
+
* Bounded: max alternatives and max total expanded bytes; unbalanced or nested
|
|
10
|
+
* braces fail closed. With no braces (or empty text), returns `[pattern]`.
|
|
11
|
+
*/
|
|
12
|
+
export function expandGlobBraces(pattern, options) {
|
|
13
|
+
const maxAlternatives = options?.maxAlternatives ?? DEFAULT_MAX_BRACE_ALTERNATIVES;
|
|
14
|
+
const maxExpandedBytes = options?.maxExpandedBytes ?? DEFAULT_MAX_BRACE_EXPANDED_BYTES;
|
|
15
|
+
const results = [];
|
|
16
|
+
const go = (start, prefix) => {
|
|
17
|
+
if (results.length >= maxAlternatives) {
|
|
18
|
+
throw new Error(`brace expansion exceeds ${maxAlternatives} alternative limit`);
|
|
19
|
+
}
|
|
20
|
+
const open = pattern.indexOf("{", start);
|
|
21
|
+
if (open === -1) {
|
|
22
|
+
const out = prefix + pattern.slice(start);
|
|
23
|
+
if (Buffer.byteLength(out, "utf8") > maxExpandedBytes) {
|
|
24
|
+
throw new Error(`brace expansion exceeds ${maxExpandedBytes} byte limit`);
|
|
25
|
+
}
|
|
26
|
+
results.push(out);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
let depth = 1;
|
|
30
|
+
let close = -1;
|
|
31
|
+
for (let i = open + 1; i < pattern.length; i++) {
|
|
32
|
+
const ch = pattern[i];
|
|
33
|
+
if (ch === "{")
|
|
34
|
+
depth++;
|
|
35
|
+
else if (ch === "}") {
|
|
36
|
+
depth--;
|
|
37
|
+
if (depth === 0) {
|
|
38
|
+
close = i;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (close === -1)
|
|
44
|
+
throw new Error(`unbalanced brace expansion in pattern: ${pattern}`);
|
|
45
|
+
if (depth !== 0)
|
|
46
|
+
throw new Error(`unbalanced brace expansion in pattern: ${pattern}`);
|
|
47
|
+
const body = pattern.slice(open + 1, close);
|
|
48
|
+
if (body.includes("{") || body.includes("}")) {
|
|
49
|
+
throw new Error(`nested brace expansion is not supported in pattern: ${pattern}`);
|
|
50
|
+
}
|
|
51
|
+
if (body.length === 0)
|
|
52
|
+
throw new Error(`empty brace expansion in pattern: ${pattern}`);
|
|
53
|
+
for (const alt of body.split(",")) {
|
|
54
|
+
go(close + 1, prefix + pattern.slice(start, open) + alt);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
go(0, "");
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
export function validateGlobPattern(pattern, maxPatternBytes, options) {
|
|
5
61
|
const patternBytes = Buffer.byteLength(pattern, "utf8");
|
|
6
62
|
if (patternBytes < 1)
|
|
7
63
|
throw new Error("pattern must be non-empty");
|
|
@@ -9,7 +65,12 @@ export function validateGlobPattern(pattern, maxPatternBytes) {
|
|
|
9
65
|
throw new Error(`pattern exceeds ${maxPatternBytes} byte pattern limit`);
|
|
10
66
|
}
|
|
11
67
|
if (pattern.includes("{") || pattern.includes("}")) {
|
|
12
|
-
|
|
68
|
+
if (options?.braceExpansion === true) {
|
|
69
|
+
// Bounded expansion is validated by expandGlobBraces (alternatives + byte caps).
|
|
70
|
+
expandGlobBraces(pattern);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
throw new Error("brace expansion is not supported in glob patterns (set braceExpansion: true to enable the bounded expansion)");
|
|
13
74
|
}
|
|
14
75
|
}
|
|
15
76
|
function matchSegment(pattern, segment) {
|
package/dist/glob.d.ts
CHANGED
|
@@ -10,5 +10,7 @@ export interface GlobToolOptions {
|
|
|
10
10
|
maxDepth?: number;
|
|
11
11
|
maxResults?: number;
|
|
12
12
|
exclude?: readonly string[];
|
|
13
|
+
/** Host-selected default for bounded `{a,b}` brace expansion (default false). */
|
|
14
|
+
braceExpansion?: boolean;
|
|
13
15
|
}
|
|
14
16
|
export declare function createGlobTool(cwd: string, options?: GlobToolOptions): ToolDefinition;
|
package/dist/glob.js
CHANGED
|
@@ -32,7 +32,7 @@ export function createGlobTool(cwd, options) {
|
|
|
32
32
|
return {
|
|
33
33
|
name: "glob",
|
|
34
34
|
effect: CODING_OBSERVATION_EFFECT,
|
|
35
|
-
description: `Find workspace files by glob pattern without shell find. Supports * (segment), ? (one char), and ** (directories). Brace expansion is rejected. Skips hidden names and excluded basenames (default: ${limits.exclude.join(", ")}) unless overridden. Does not follow symlinks. Results paginate with offset/maxResults (default ${limits.maxResults}). Depth default ${limits.maxDepth}. Prefer repo_list to enumerate directories and repo_search to find text inside files.`,
|
|
35
|
+
description: `Find workspace files by glob pattern without shell find. Supports * (segment), ? (one char), and ** (directories). Brace expansion is rejected unless braceExpansion: true (bounded: max 128 alternatives / 4096 expanded bytes; unbalanced or nested braces are errors). Skips hidden names and excluded basenames (default: ${limits.exclude.join(", ")}) unless overridden. Does not follow symlinks. Results paginate with offset/maxResults (default ${limits.maxResults}). Depth default ${limits.maxDepth}. Prefer repo_list to enumerate directories and repo_search to find text inside files.`,
|
|
36
36
|
parameters: {
|
|
37
37
|
type: "object",
|
|
38
38
|
properties: {
|
|
@@ -48,6 +48,10 @@ export function createGlobTool(cwd, options) {
|
|
|
48
48
|
type: "boolean",
|
|
49
49
|
description: "Include dotfile/dotdir names (default false). Excluded basenames still apply.",
|
|
50
50
|
},
|
|
51
|
+
braceExpansion: {
|
|
52
|
+
type: "boolean",
|
|
53
|
+
description: "Opt-in bounded {a,b} brace expansion (default: host option, else false)",
|
|
54
|
+
},
|
|
51
55
|
maxDepth: {
|
|
52
56
|
type: "number",
|
|
53
57
|
description: `Maximum directory depth to descend (default ${limits.maxDepth}, hard ${HARD_MAX_REPO_DEPTH})`,
|
|
@@ -73,6 +77,7 @@ export function createGlobTool(cwd, options) {
|
|
|
73
77
|
return errorResult(toolCallId, "pattern must be a non-empty string");
|
|
74
78
|
const path = typeof args.path === "string" ? args.path : undefined;
|
|
75
79
|
const includeHidden = args.includeHidden === true;
|
|
80
|
+
const braceExpansion = args.braceExpansion === true || (args.braceExpansion === undefined && options?.braceExpansion === true);
|
|
76
81
|
let maxDepth;
|
|
77
82
|
let maxResults;
|
|
78
83
|
let offset = 0;
|
|
@@ -98,6 +103,7 @@ export function createGlobTool(cwd, options) {
|
|
|
98
103
|
metadata: {
|
|
99
104
|
pattern,
|
|
100
105
|
includeHidden,
|
|
106
|
+
braceExpansion,
|
|
101
107
|
maxDepth,
|
|
102
108
|
maxResults,
|
|
103
109
|
offset,
|
|
@@ -114,6 +120,7 @@ export function createGlobTool(cwd, options) {
|
|
|
114
120
|
pattern,
|
|
115
121
|
path,
|
|
116
122
|
includeHidden,
|
|
123
|
+
braceExpansion,
|
|
117
124
|
exclude: limits.exclude,
|
|
118
125
|
maxDepth: maxDepth ?? limits.maxDepth,
|
|
119
126
|
maxResults: maxResults ?? limits.maxResults,
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type { ListToolOptions } from "./list.js";
|
|
|
26
26
|
export { createRepoListTool } from "./list.js";
|
|
27
27
|
export type { MoveOperations, MoveToolOptions } from "./move.js";
|
|
28
28
|
export { createMoveTool } from "./move.js";
|
|
29
|
-
export type { ReadOperations, ReadTextOptions, ReadTextResult, ReadToolOptions, TransformImage, TransformImageInput, } from "./read.js";
|
|
29
|
+
export type { ReadOperations, ReadTextOptions, ReadTextResult, ReadToolOptions, TransformImage, TransformImageInput, DocumentReader, DocumentReaderResult, } from "./read.js";
|
|
30
30
|
export { createReadTool, DEFAULT_MAX_IMAGE_BYTES, detectSupportedImageMimeType, detectSupportedImageMimeTypeFromFile, } from "./read.js";
|
|
31
31
|
export type { ReadPathSet } from "./read-path-set.js";
|
|
32
32
|
export { createReadPathSet, createReadPathSetPersistence, DEFAULT_MAX_PERSISTED_READ_PATHS, DEFAULT_MAX_PERSISTED_READ_PATH_CHARS, READ_PATH_SET_NAMESPACE, } from "./read-path-set.js";
|
package/dist/read.d.ts
CHANGED
|
@@ -59,6 +59,34 @@ export interface ReadTextResult {
|
|
|
59
59
|
readonly totalLines?: number;
|
|
60
60
|
readonly totalBytes?: number;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Optional host-selected document parser adapter (plan 018 closeout `doc-reader`).
|
|
64
|
+
*
|
|
65
|
+
* When wired into {@link ReadToolOptions.documentReader}, the read tool tries
|
|
66
|
+
* the reader after the image sniff and before the text page. The reader owns
|
|
67
|
+
* its bounds (input bytes, pages, output text); the read tool re-checks the
|
|
68
|
+
* output cap before returning. An unsupported buffer must return `null` so
|
|
69
|
+
* reads fall through to the 0.1.5 text path. No file-extension sniffing ever
|
|
70
|
+
* activates parsing — activation is explicit via this option.
|
|
71
|
+
*/
|
|
72
|
+
export interface DocumentReader {
|
|
73
|
+
/** Hard input size cap; reads refuse larger files before loading them. */
|
|
74
|
+
readonly maxInputBytes: number;
|
|
75
|
+
/** Hard cap on extracted literal text; the read tool refuses results beyond it. */
|
|
76
|
+
readonly maxTextBytes: number;
|
|
77
|
+
/** Extract literal text; return null when the buffer is not a supported document format. */
|
|
78
|
+
extract(input: {
|
|
79
|
+
readonly buffer: Buffer;
|
|
80
|
+
readonly path: string;
|
|
81
|
+
readonly signal?: AbortSignal;
|
|
82
|
+
}): Promise<DocumentReaderResult | null>;
|
|
83
|
+
}
|
|
84
|
+
export interface DocumentReaderResult {
|
|
85
|
+
readonly text: string;
|
|
86
|
+
readonly format: string;
|
|
87
|
+
readonly pages: number;
|
|
88
|
+
readonly truncatedBy: "pages" | "bytes" | null;
|
|
89
|
+
}
|
|
62
90
|
export interface ReadOperations {
|
|
63
91
|
/** Read a bounded binary file. Backends must honor `maxBytes` before retaining more data. */
|
|
64
92
|
readFile: (absolutePath: string, options: {
|
|
@@ -97,6 +125,11 @@ export interface ReadToolOptions {
|
|
|
97
125
|
maxBytes?: number;
|
|
98
126
|
/** When set, successful reads record the resolved absolute path for read-before-write guards. */
|
|
99
127
|
readPathSet?: ReadPathSet;
|
|
128
|
+
/**
|
|
129
|
+
* Optional host-selected document reader (PDF/Office literal-text extraction).
|
|
130
|
+
* Additive: absent reader means exactly the 0.1.5 text/image behavior.
|
|
131
|
+
*/
|
|
132
|
+
documentReader?: DocumentReader;
|
|
100
133
|
}
|
|
101
134
|
export declare function createReadTool(cwd: string, options?: ReadToolOptions): ToolDefinition;
|
|
102
135
|
/** Re-exported for hosts building custom read tools or analyzing truncation metadata. */
|
package/dist/read.js
CHANGED
|
@@ -363,6 +363,35 @@ export function createReadTool(cwd, options) {
|
|
|
363
363
|
metadata: { image: { mimeType, resized, bytes: buffer.length } },
|
|
364
364
|
};
|
|
365
365
|
}
|
|
366
|
+
const documentReader = options?.documentReader;
|
|
367
|
+
if (documentReader) {
|
|
368
|
+
const { size } = await ops.statFile(allowedPath, { signal: context.signal });
|
|
369
|
+
if (size > documentReader.maxInputBytes) {
|
|
370
|
+
return errorResult(toolCallId, `Document is ${formatSize(size)}, exceeds ${formatSize(documentReader.maxInputBytes)} limit.`);
|
|
371
|
+
}
|
|
372
|
+
const buffer = await ops.readFile(allowedPath, {
|
|
373
|
+
maxBytes: documentReader.maxInputBytes,
|
|
374
|
+
signal: context.signal,
|
|
375
|
+
});
|
|
376
|
+
if (buffer.length > documentReader.maxInputBytes) {
|
|
377
|
+
return errorResult(toolCallId, `Document is ${formatSize(buffer.length)}, exceeds ${formatSize(documentReader.maxInputBytes)} limit.`);
|
|
378
|
+
}
|
|
379
|
+
const extracted = await documentReader.extract({ buffer, path: allowedPath, signal: context.signal });
|
|
380
|
+
if (extracted) {
|
|
381
|
+
if (Buffer.byteLength(extracted.text, "utf8") > documentReader.maxTextBytes) {
|
|
382
|
+
throw new Error("DocumentReader.extract returned text beyond its maxTextBytes cap");
|
|
383
|
+
}
|
|
384
|
+
options?.readPathSet?.add(allowedPath);
|
|
385
|
+
return {
|
|
386
|
+
toolCallId,
|
|
387
|
+
name: "read",
|
|
388
|
+
content: [{ type: "text", text: extracted.text }],
|
|
389
|
+
metadata: {
|
|
390
|
+
document: { format: extracted.format, pages: extracted.pages, truncatedBy: extracted.truncatedBy },
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
366
395
|
const page = await ops.readText(allowedPath, {
|
|
367
396
|
offset: startLine,
|
|
368
397
|
limit: requestedLines,
|
package/dist/repository.d.ts
CHANGED
|
@@ -114,6 +114,8 @@ export interface RepositoryGlobRequest {
|
|
|
114
114
|
readonly maxDepth?: number;
|
|
115
115
|
readonly maxResults?: number;
|
|
116
116
|
readonly offset?: number;
|
|
117
|
+
/** Opt-in bounded `{a,b}` expansion (default false; expansion bounds in glob-match.ts). */
|
|
118
|
+
readonly braceExpansion?: boolean;
|
|
117
119
|
readonly signal?: AbortSignal;
|
|
118
120
|
readonly deadlineMs?: number;
|
|
119
121
|
}
|
package/dist/repository.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { lstat, open, opendir, realpath } from "node:fs/promises";
|
|
9
9
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
10
|
import { DEFAULT_BINARY_SNIFF_BYTES, DEFAULT_MAX_REPO_CONCURRENCY, DEFAULT_MAX_REPO_DEPTH, DEFAULT_MAX_REPO_ENTRIES, DEFAULT_MAX_REPO_FILES, DEFAULT_MAX_REPO_RESULTS, DEFAULT_MAX_SEARCH_CONTEXT_LINES, DEFAULT_MAX_SEARCH_FILE_BYTES, DEFAULT_MAX_SEARCH_LINE_BYTES, DEFAULT_MAX_SEARCH_MATCHES, DEFAULT_MAX_SEARCH_PATTERN_BYTES, DEFAULT_MAX_SEARCH_SCAN_BYTES, DEFAULT_MAX_SEARCH_TIME_MS, HARD_MAX_REPO_CONCURRENCY, HARD_MAX_REPO_DEPTH, HARD_MAX_REPO_ENTRIES, HARD_MAX_REPO_FILES, HARD_MAX_REPO_RESULTS, HARD_MAX_SEARCH_CONTEXT_LINES, HARD_MAX_SEARCH_FILE_BYTES, HARD_MAX_SEARCH_LINE_BYTES, HARD_MAX_SEARCH_MATCHES, HARD_MAX_SEARCH_PATTERN_BYTES, HARD_MAX_SEARCH_SCAN_BYTES, HARD_MAX_SEARCH_TIME_MS, validateCodingLimit, validateCodingLimitAllowZero, } from "./limits.js";
|
|
11
|
-
import { matchGlobPattern, validateGlobPattern } from "./glob-match.js";
|
|
11
|
+
import { expandGlobBraces, matchGlobPattern, validateGlobPattern } from "./glob-match.js";
|
|
12
12
|
import { resolveToCwd } from "./path-utils.js";
|
|
13
13
|
export const DEFAULT_REPO_EXCLUDE = Object.freeze([".git", "node_modules", "dist"]);
|
|
14
14
|
export class RepositoryError extends Error {
|
|
@@ -605,11 +605,14 @@ async function searchLocal(request, defaults, walk) {
|
|
|
605
605
|
}
|
|
606
606
|
async function globLocal(request, defaults, walk) {
|
|
607
607
|
try {
|
|
608
|
-
validateGlobPattern(request.pattern, defaults.maxPatternBytes);
|
|
608
|
+
validateGlobPattern(request.pattern, defaults.maxPatternBytes, { braceExpansion: request.braceExpansion === true });
|
|
609
609
|
}
|
|
610
610
|
catch (error) {
|
|
611
611
|
throw new RepositoryError(error instanceof Error ? error.message : String(error));
|
|
612
612
|
}
|
|
613
|
+
// Opt-in bounded brace expansion: textual alternatives only (never touches the
|
|
614
|
+
// filesystem); bounds enforced by expandGlobBraces (max alternatives / bytes).
|
|
615
|
+
const patterns = request.braceExpansion === true ? expandGlobBraces(request.pattern) : [request.pattern];
|
|
613
616
|
const resolved = await resolveRepoPath(request.root, request.path);
|
|
614
617
|
const maxResults = validateCodingLimit("maxResults", request.maxResults ?? defaults.maxResults, HARD_MAX_REPO_RESULTS);
|
|
615
618
|
const offset = validateCodingLimitAllowZero("offset", request.offset ?? 0, HARD_MAX_REPO_ENTRIES);
|
|
@@ -622,8 +625,15 @@ async function globLocal(request, defaults, walk) {
|
|
|
622
625
|
let seen = 0;
|
|
623
626
|
let truncated = false;
|
|
624
627
|
let truncatedBy = null;
|
|
628
|
+
const matchesAnyPattern = (relativePath) => {
|
|
629
|
+
for (const p of patterns) {
|
|
630
|
+
if (matchGlobPattern(p, relativePath))
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
return false;
|
|
634
|
+
};
|
|
625
635
|
const maybeCollect = (relativePath) => {
|
|
626
|
-
if (!
|
|
636
|
+
if (!matchesAnyPattern(relativePath))
|
|
627
637
|
return false;
|
|
628
638
|
if (seen < offset) {
|
|
629
639
|
seen++;
|
|
@@ -644,7 +654,7 @@ async function globLocal(request, defaults, walk) {
|
|
|
644
654
|
scannedEntries = 1;
|
|
645
655
|
if (startStat.isFile()) {
|
|
646
656
|
scannedFiles = 1;
|
|
647
|
-
if (
|
|
657
|
+
if (matchesAnyPattern(resolved.relative)) {
|
|
648
658
|
if (offset === 0 && maxResults > 0)
|
|
649
659
|
collected.push(resolved.relative);
|
|
650
660
|
else if (offset === 0 && maxResults === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arnilo/prism-coding-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Optional coding-agent tools (shell, read, write, edit, repo_list, repo_search, glob, delete, move, opt-in Git/check/ask-user-decision, and durable plan/checkpoint helpers) package for Prism.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"diff": "^9.0.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@arnilo/prism": "0.1.
|
|
32
|
-
"@arnilo/prism-workflows": "0.1.
|
|
31
|
+
"@arnilo/prism": "0.1.6",
|
|
32
|
+
"@arnilo/prism-workflows": "0.1.6"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@arnilo/prism": "file:../..",
|