@arnilo/prism-coding-agent 0.2.9 → 0.3.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/CHANGELOG.md +13 -0
- package/dist/acp-operations.d.ts +22 -0
- package/dist/acp-operations.js +122 -0
- package/dist/coding-checkpoint.js +51 -8
- package/dist/edit-diff.d.ts +1 -0
- package/dist/edit-diff.js +32 -6
- package/dist/edit.d.ts +2 -0
- package/dist/edit.js +6 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/read.js +60 -3
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1] - 2026-08-29
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Plan 035-039 changed-package cut: additive runtime performance, tooling, and documentation deltas; peer window refresh.
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-08-20
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `read.findText` performs bounded literal/case-insensitive paging without extending `ReadOperations`.
|
|
13
|
+
- Edit misses include bounded nearby line context; Unicode-normalized fuzzy matches are reported in confirmation text and `metadata.fuzzy`.
|
|
14
|
+
- `createAcpFilesystemOperations` adapts ACP text-file methods to read/write/edit without touching host disk; image/document paths fail closed.
|
|
15
|
+
|
|
3
16
|
## [0.2.6] - unreleased
|
|
4
17
|
|
|
5
18
|
### Added
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EditOperations } from "./edit.js";
|
|
2
|
+
import type { ReadOperations } from "./read.js";
|
|
3
|
+
import type { WriteOperations } from "./write.js";
|
|
4
|
+
/** Duck-typed subset of the ACP client filesystem adapter. */
|
|
5
|
+
export interface TextFileClient {
|
|
6
|
+
readTextFile(input: {
|
|
7
|
+
path: string;
|
|
8
|
+
line?: number;
|
|
9
|
+
limit?: number;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
text: string;
|
|
12
|
+
}>;
|
|
13
|
+
writeTextFile(input: {
|
|
14
|
+
path: string;
|
|
15
|
+
content: string;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare function createAcpFilesystemOperations(client: TextFileClient): {
|
|
19
|
+
read: ReadOperations;
|
|
20
|
+
write: WriteOperations;
|
|
21
|
+
edit: EditOperations;
|
|
22
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP text-file client adapter for the coding read/write/edit operations.
|
|
3
|
+
*
|
|
4
|
+
* ACP exposes text reads and writes, not stat, mkdir, binary reads, or image sniffing. This adapter
|
|
5
|
+
* keeps those limits explicit: all reads go through the client, mkdir is inert, and binary/image
|
|
6
|
+
* handling is unavailable rather than falling back to the host filesystem.
|
|
7
|
+
*/
|
|
8
|
+
import { Buffer } from "node:buffer";
|
|
9
|
+
function checkAbort(signal) {
|
|
10
|
+
if (signal?.aborted)
|
|
11
|
+
throw new Error("Operation aborted");
|
|
12
|
+
}
|
|
13
|
+
async function readTextFile(client, path, input, signal) {
|
|
14
|
+
checkAbort(signal);
|
|
15
|
+
const request = input === undefined ? { path } : { path, line: input.line, limit: input.limit };
|
|
16
|
+
const response = await client.readTextFile(request);
|
|
17
|
+
checkAbort(signal);
|
|
18
|
+
if (!response || typeof response.text !== "string") {
|
|
19
|
+
throw new Error("TextFileClient.readTextFile must return { text: string }");
|
|
20
|
+
}
|
|
21
|
+
return response.text;
|
|
22
|
+
}
|
|
23
|
+
function splitTextLines(text) {
|
|
24
|
+
const lines = text.split("\n");
|
|
25
|
+
if (lines.length > 1 && lines[lines.length - 1] === "")
|
|
26
|
+
lines.pop();
|
|
27
|
+
return lines;
|
|
28
|
+
}
|
|
29
|
+
function readPage(text, options) {
|
|
30
|
+
const requestedLines = options.limit ?? options.maxLines;
|
|
31
|
+
const lines = splitTextLines(text);
|
|
32
|
+
const totalBytes = Buffer.byteLength(text, "utf8");
|
|
33
|
+
if (totalBytes > options.maxScanBytes) {
|
|
34
|
+
throw new Error(`Text read exceeded ${options.maxScanBytes} byte scan limit`);
|
|
35
|
+
}
|
|
36
|
+
const output = [];
|
|
37
|
+
let outputBytes = 0;
|
|
38
|
+
let truncatedBy = null;
|
|
39
|
+
let firstLineExceedsLimit = false;
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
if (output.length >= requestedLines) {
|
|
42
|
+
truncatedBy = "lines";
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
46
|
+
if (output.length === 0 && lineBytes > options.maxBytes) {
|
|
47
|
+
firstLineExceedsLimit = true;
|
|
48
|
+
truncatedBy = "bytes";
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
const withSeparator = output.length === 0 ? lineBytes : lineBytes + 1;
|
|
52
|
+
if (outputBytes + withSeparator > options.maxBytes) {
|
|
53
|
+
truncatedBy = "bytes";
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
output.push(line);
|
|
57
|
+
outputBytes += withSeparator;
|
|
58
|
+
}
|
|
59
|
+
const clientReturnedPage = lines.length >= requestedLines && requestedLines > 0;
|
|
60
|
+
const hasMore = !firstLineExceedsLimit && (output.length < lines.length || clientReturnedPage);
|
|
61
|
+
const nextOffset = hasMore && output.length > 0 ? options.offset + output.length : undefined;
|
|
62
|
+
const totalLines = clientReturnedPage ? undefined : lines.length;
|
|
63
|
+
return {
|
|
64
|
+
content: firstLineExceedsLimit ? "" : output.join("\n"),
|
|
65
|
+
startLine: options.offset,
|
|
66
|
+
outputLines: firstLineExceedsLimit ? 0 : output.length,
|
|
67
|
+
hasMore,
|
|
68
|
+
nextOffset,
|
|
69
|
+
truncatedBy,
|
|
70
|
+
firstLineExceedsLimit,
|
|
71
|
+
scannedBytes: totalBytes,
|
|
72
|
+
totalLines,
|
|
73
|
+
totalBytes,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function createAcpFilesystemOperations(client) {
|
|
77
|
+
const readFile = async (path, options) => {
|
|
78
|
+
const text = await readTextFile(client, path, undefined, options.signal);
|
|
79
|
+
const buffer = Buffer.from(text, "utf8");
|
|
80
|
+
if (buffer.byteLength > options.maxBytes) {
|
|
81
|
+
throw new Error(`File is ${buffer.byteLength} bytes, exceeds ${options.maxBytes} byte limit`);
|
|
82
|
+
}
|
|
83
|
+
return buffer;
|
|
84
|
+
};
|
|
85
|
+
const writeFile = async (path, content, options) => {
|
|
86
|
+
checkAbort(options?.signal);
|
|
87
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
88
|
+
if (options?.maxBytes !== undefined && bytes > options.maxBytes) {
|
|
89
|
+
throw new Error(`Write input is ${bytes} bytes, exceeds ${options.maxBytes} byte limit`);
|
|
90
|
+
}
|
|
91
|
+
await client.writeTextFile({ path, content });
|
|
92
|
+
};
|
|
93
|
+
const access = (path, options) => readTextFile(client, path, { line: 1, limit: 1 }, options?.signal).then(() => undefined);
|
|
94
|
+
const statFile = async (path, options) => ({
|
|
95
|
+
size: Buffer.byteLength(await readTextFile(client, path, undefined, options?.signal), "utf8"),
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
read: {
|
|
99
|
+
readFile,
|
|
100
|
+
readText: async (path, options) => {
|
|
101
|
+
const text = await readTextFile(client, path, { line: options.offset, limit: options.limit ?? options.maxLines }, options.signal);
|
|
102
|
+
return readPage(text, options);
|
|
103
|
+
},
|
|
104
|
+
access,
|
|
105
|
+
statFile,
|
|
106
|
+
detectImageMimeType: async () => null,
|
|
107
|
+
},
|
|
108
|
+
write: {
|
|
109
|
+
writeFile,
|
|
110
|
+
mkdir: async (_path, options) => {
|
|
111
|
+
checkAbort(options?.signal);
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
edit: {
|
|
115
|
+
readFile,
|
|
116
|
+
writeFile,
|
|
117
|
+
access,
|
|
118
|
+
statFile,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=acp-operations.js.map
|
|
@@ -16,7 +16,35 @@ export const CODING_STATE_KEY = "coding";
|
|
|
16
16
|
const SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
17
17
|
const TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/;
|
|
18
18
|
const BRANCH = /^[^\s]{1,255}$/;
|
|
19
|
-
|
|
19
|
+
// Linear todo-line scanner replaces `/^\s*[-*]\s+\[([ xX])\]\s+(.+?)\s*$/` (CodeQL js/polynomial-redos, alert 4)
|
|
20
|
+
function parseTodoLine(line) {
|
|
21
|
+
let i = 0;
|
|
22
|
+
while (i < line.length && /\s/.test(line[i]))
|
|
23
|
+
i += 1;
|
|
24
|
+
if (line[i] !== "-" && line[i] !== "*")
|
|
25
|
+
return undefined;
|
|
26
|
+
i += 1;
|
|
27
|
+
while (i < line.length && /\s/.test(line[i]))
|
|
28
|
+
i += 1;
|
|
29
|
+
if (line[i] !== "[")
|
|
30
|
+
return undefined;
|
|
31
|
+
i += 1;
|
|
32
|
+
const doneChar = line[i];
|
|
33
|
+
if (doneChar !== "x" && doneChar !== "X" && doneChar !== " ")
|
|
34
|
+
return undefined;
|
|
35
|
+
i += 1;
|
|
36
|
+
if (line[i] !== "]")
|
|
37
|
+
return undefined;
|
|
38
|
+
i += 1;
|
|
39
|
+
while (i < line.length && /\s/.test(line[i]))
|
|
40
|
+
i += 1;
|
|
41
|
+
let end = line.length;
|
|
42
|
+
while (end > i && /\s/.test(line[end - 1]))
|
|
43
|
+
end -= 1;
|
|
44
|
+
if (end <= i)
|
|
45
|
+
return undefined;
|
|
46
|
+
return { done: doneChar.toLowerCase() === "x", raw: line.slice(i, end) };
|
|
47
|
+
}
|
|
20
48
|
const FORBIDDEN_METADATA_KEYS = new Set([
|
|
21
49
|
"credentials",
|
|
22
50
|
"credential",
|
|
@@ -123,17 +151,32 @@ export function parseCodingPlanTodos(markdown, limits) {
|
|
|
123
151
|
assertByteLimit("plan", markdown, resolved.maxPlanBytes);
|
|
124
152
|
const todos = [];
|
|
125
153
|
for (const line of markdown.split(/\r?\n/)) {
|
|
126
|
-
const match =
|
|
154
|
+
const match = parseTodoLine(line);
|
|
127
155
|
if (!match)
|
|
128
156
|
continue;
|
|
129
|
-
const done = match
|
|
130
|
-
const raw = match
|
|
157
|
+
const done = match.done;
|
|
158
|
+
const raw = match.raw;
|
|
131
159
|
assertTodoText(raw, resolved.maxTodoTextBytes);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
160
|
+
// Linear id-prefix parse replaces `/^\[([A-Za-z0-9._-]{1,64})\]\s+(.+)$/` (CodeQL js/polynomial-redos, alert 5)
|
|
161
|
+
let id;
|
|
162
|
+
let text = raw;
|
|
163
|
+
if (raw.startsWith("[")) {
|
|
164
|
+
const close = raw.indexOf("]", 1);
|
|
165
|
+
if (close !== -1) {
|
|
166
|
+
const candidate = raw.slice(1, close);
|
|
167
|
+
const after = raw.slice(close + 1);
|
|
168
|
+
if (candidate.length >= 1 &&
|
|
169
|
+
candidate.length <= 64 &&
|
|
170
|
+
/^[A-Za-z0-9._-]+$/u.test(candidate) &&
|
|
171
|
+
/^\s/u.test(after) &&
|
|
172
|
+
after.trim().length > 0) {
|
|
173
|
+
id = candidate;
|
|
174
|
+
text = after.trim();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
135
178
|
assertTodoText(text, resolved.maxTodoTextBytes);
|
|
136
|
-
todos.push({ id
|
|
179
|
+
todos.push({ id: id ?? `todo-${todos.length + 1}`, text, done });
|
|
137
180
|
if (todos.length > resolved.maxTodos) {
|
|
138
181
|
throw new CodingCheckpointError(`Plan exceeds ${resolved.maxTodos} todo limit`);
|
|
139
182
|
}
|
package/dist/edit-diff.d.ts
CHANGED
package/dist/edit-diff.js
CHANGED
|
@@ -179,11 +179,37 @@ function countOccurrences(content, oldText) {
|
|
|
179
179
|
const fuzzyOldText = normalizeForFuzzyMatch(oldText);
|
|
180
180
|
return fuzzyContent.split(fuzzyOldText).length - 1;
|
|
181
181
|
}
|
|
182
|
-
function getNotFoundError(path, editIndex, totalEdits) {
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
function getNotFoundError(path, content, oldText, editIndex, totalEdits) {
|
|
183
|
+
const base = totalEdits === 1
|
|
184
|
+
? `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`
|
|
185
|
+
: `Could not find edits[${editIndex}] in ${path}. The oldText must match exactly including all whitespace and newlines.`;
|
|
186
|
+
// Cheap first-line substring scan: collect up to 3 unique nearby lines so the model can correct
|
|
187
|
+
// its oldText. No extra file read — `content` is the already-loaded target. Needle is clipped to
|
|
188
|
+
// 16 chars (min 4) to avoid noise from tiny/short oldText.
|
|
189
|
+
const firstLine = oldText
|
|
190
|
+
.split("\n")
|
|
191
|
+
.map((l) => l.trim())
|
|
192
|
+
.find((l) => l.length > 0);
|
|
193
|
+
if (!firstLine)
|
|
194
|
+
return new Error(base);
|
|
195
|
+
const needle = firstLine.slice(0, 16);
|
|
196
|
+
if (needle.length < 4)
|
|
197
|
+
return new Error(base);
|
|
198
|
+
const lines = content.split("\n");
|
|
199
|
+
const seen = new Set();
|
|
200
|
+
const nearby = [];
|
|
201
|
+
for (let i = 0; i < lines.length && nearby.length < 3; i++) {
|
|
202
|
+
if (lines[i].includes(needle)) {
|
|
203
|
+
const snippet = lines[i].slice(0, 120);
|
|
204
|
+
if (seen.has(snippet))
|
|
205
|
+
continue;
|
|
206
|
+
seen.add(snippet);
|
|
207
|
+
nearby.push(` L${i + 1}: ${snippet}`);
|
|
208
|
+
}
|
|
185
209
|
}
|
|
186
|
-
|
|
210
|
+
if (nearby.length === 0)
|
|
211
|
+
return new Error(base);
|
|
212
|
+
return new Error(`${base}\nNearby:\n${nearby.join("\n")}`);
|
|
187
213
|
}
|
|
188
214
|
function getDuplicateError(path, editIndex, totalEdits, occurrences) {
|
|
189
215
|
if (totalEdits === 1) {
|
|
@@ -230,7 +256,7 @@ export function applyEditsToNormalizedContent(normalizedContent, edits, path) {
|
|
|
230
256
|
const edit = normalizedEdits[i];
|
|
231
257
|
const matchResult = fuzzyFindText(replacementBaseContent, edit.oldText);
|
|
232
258
|
if (!matchResult.found) {
|
|
233
|
-
throw getNotFoundError(path, i, normalizedEdits.length);
|
|
259
|
+
throw getNotFoundError(path, normalizedContent, edit.oldText, i, normalizedEdits.length);
|
|
234
260
|
}
|
|
235
261
|
const occurrences = countOccurrences(replacementBaseContent, edit.oldText);
|
|
236
262
|
if (occurrences > 1) {
|
|
@@ -258,7 +284,7 @@ export function applyEditsToNormalizedContent(normalizedContent, edits, path) {
|
|
|
258
284
|
if (baseContent === newContent) {
|
|
259
285
|
throw getNoChangeError(path, normalizedEdits.length);
|
|
260
286
|
}
|
|
261
|
-
return { baseContent, newContent };
|
|
287
|
+
return { baseContent, newContent, usedFuzzyMatch };
|
|
262
288
|
}
|
|
263
289
|
/** Generate a standard unified patch. */
|
|
264
290
|
export function generateUnifiedPatch(path, oldContent, newContent, contextLines = 4) {
|
package/dist/edit.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface EditToolDetails {
|
|
|
37
37
|
patch: string;
|
|
38
38
|
/** Line number of the first change in the new file (for editor navigation). */
|
|
39
39
|
firstChangedLine?: number;
|
|
40
|
+
/** `true` when the replacement applied via fuzzy (not exact) matching. Absent for exact. */
|
|
41
|
+
fuzzy?: boolean;
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
44
|
* Pluggable operations for the edit tool. Override to delegate file editing to remote systems
|
package/dist/edit.js
CHANGED
|
@@ -98,7 +98,7 @@ export function createEditTool(cwd, options) {
|
|
|
98
98
|
name: "edit",
|
|
99
99
|
kind: "edit",
|
|
100
100
|
effect: CODING_LOCAL_EFFECT,
|
|
101
|
-
description: "Edit a single file using exact-then-fuzzy text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. Exact match is tried first; if it fails, fuzzy match (unicode normalize + whitespace collapse) may still succeed
|
|
101
|
+
description: "Edit a single file using exact-then-fuzzy text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. Exact match is tried first; if it fails, fuzzy match (unicode normalize + whitespace collapse) may still succeed and is reported as `(fuzzy match)` — prefer exact `oldText` copied from a fresh `read` to avoid wrong-region edits. Duplicate/ambiguous matches fail closed and leave the file unchanged. If two changes affect the same block or nearby lines, merge them into one edit. Do not include large unchanged regions just to connect distant changes. When the host enabled requireReadBeforeWrite, read the path first or pass force=true.",
|
|
102
102
|
parameters: {
|
|
103
103
|
type: "object",
|
|
104
104
|
properties: {
|
|
@@ -193,8 +193,9 @@ export function createEditTool(cwd, options) {
|
|
|
193
193
|
// Apply exact-then-fuzzy matching. Throws on no-match / duplicate / overlap / empty / no-change.
|
|
194
194
|
let baseContent;
|
|
195
195
|
let newContent;
|
|
196
|
+
let usedFuzzyMatch = false;
|
|
196
197
|
try {
|
|
197
|
-
({ baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, prepared.path));
|
|
198
|
+
({ baseContent, newContent, usedFuzzyMatch } = applyEditsToNormalizedContent(normalizedContent, edits, prepared.path));
|
|
198
199
|
}
|
|
199
200
|
catch (error) {
|
|
200
201
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -207,15 +208,17 @@ export function createEditTool(cwd, options) {
|
|
|
207
208
|
options?.onEvent?.({ type: "file_changed", path: allowedPath, op: "edit", toolCallId });
|
|
208
209
|
const diffResult = generateDiffString(baseContent, newContent);
|
|
209
210
|
const patch = generateUnifiedPatch(prepared.path, baseContent, newContent);
|
|
211
|
+
const confirmation = `Successfully replaced ${edits.length} block(s) in ${prepared.path}${usedFuzzyMatch ? " (fuzzy match)" : ""}.`;
|
|
210
212
|
return {
|
|
211
213
|
toolCallId,
|
|
212
214
|
name: "edit",
|
|
213
|
-
content: [{ type: "text", text:
|
|
215
|
+
content: [{ type: "text", text: confirmation }],
|
|
214
216
|
metadata: {
|
|
215
217
|
path: allowedPath,
|
|
216
218
|
diff: diffResult.diff,
|
|
217
219
|
patch,
|
|
218
220
|
firstChangedLine: diffResult.firstChangedLine,
|
|
221
|
+
...(usedFuzzyMatch ? { fuzzy: true } : {}),
|
|
219
222
|
},
|
|
220
223
|
};
|
|
221
224
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type { TextFileClient } from "./acp-operations.js";
|
|
2
|
+
export { createAcpFilesystemOperations } from "./acp-operations.js";
|
|
1
3
|
export { createDirectoryArtifactWriter, createTempArtifactWriter, sha256Hex } from "./artifacts.js";
|
|
2
4
|
export type { AskUserDecisionAnswer, AskUserDecisionHandler, AskUserDecisionOption, AskUserDecisionRequest, AskUserDecisionSelectionMode, AskUserDecisionSuspendData, AskUserDecisionToolOptions, ResolvedAskUserDecisionAnswer, ResolvedAskUserDecisionLimits, SuspendAskUserDecisionOptions, } from "./ask-user-decision.js";
|
|
3
5
|
export { ASK_USER_DECISION_RATIONALE_COUNT, ASK_USER_DECISION_SUSPEND_REASON, ASK_USER_DECISION_TOOL_NAME, askUserDecisionResumeSchema, createAskUserDecisionResumeValidator, createAskUserDecisionTool, DEFAULT_MAX_ASK_USER_DECISION_BULLET_BYTES, DEFAULT_MAX_ASK_USER_DECISION_CUSTOM_BYTES, DEFAULT_MAX_ASK_USER_DECISION_LABEL_BYTES, DEFAULT_MAX_ASK_USER_DECISION_OPTIONS, DEFAULT_MAX_ASK_USER_DECISION_QUESTION_BYTES, HARD_MAX_ASK_USER_DECISION_BULLET_BYTES, HARD_MAX_ASK_USER_DECISION_CUSTOM_BYTES, HARD_MAX_ASK_USER_DECISION_LABEL_BYTES, HARD_MAX_ASK_USER_DECISION_OPTIONS, HARD_MAX_ASK_USER_DECISION_QUESTION_BYTES, parseAskUserDecisionArgs, resolveAskUserDecisionAnswer, resolveAskUserDecisionLimits, suspendAskUserDecision, toAskUserDecisionSuspendData, validateAskUserDecisionAgentResume, validateAskUserDecisionResume, } from "./ask-user-decision.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// First-party coding tools for the Prism agent harness. Factory functions return Prism
|
|
4
4
|
// `ToolDefinition`s that hosts register into a `ToolRegistry` (e.g.
|
|
5
5
|
// `createToolRegistry(createCodingTools(cwd))`). No tools are auto-registered — import what you need.
|
|
6
|
-
|
|
6
|
+
export { createAcpFilesystemOperations } from "./acp-operations.js";
|
|
7
7
|
export { createDirectoryArtifactWriter, createTempArtifactWriter, sha256Hex } from "./artifacts.js";
|
|
8
8
|
export { ASK_USER_DECISION_RATIONALE_COUNT, ASK_USER_DECISION_SUSPEND_REASON, ASK_USER_DECISION_TOOL_NAME, askUserDecisionResumeSchema, createAskUserDecisionResumeValidator, createAskUserDecisionTool, DEFAULT_MAX_ASK_USER_DECISION_BULLET_BYTES, DEFAULT_MAX_ASK_USER_DECISION_CUSTOM_BYTES, DEFAULT_MAX_ASK_USER_DECISION_LABEL_BYTES, DEFAULT_MAX_ASK_USER_DECISION_OPTIONS, DEFAULT_MAX_ASK_USER_DECISION_QUESTION_BYTES, HARD_MAX_ASK_USER_DECISION_BULLET_BYTES, HARD_MAX_ASK_USER_DECISION_CUSTOM_BYTES, HARD_MAX_ASK_USER_DECISION_LABEL_BYTES, HARD_MAX_ASK_USER_DECISION_OPTIONS, HARD_MAX_ASK_USER_DECISION_QUESTION_BYTES, parseAskUserDecisionArgs, resolveAskUserDecisionAnswer, resolveAskUserDecisionLimits, suspendAskUserDecision, toAskUserDecisionSuspendData, validateAskUserDecisionAgentResume, validateAskUserDecisionResume, } from "./ask-user-decision.js";
|
|
9
9
|
export { createCodingCheckTool } from "./checks.js";
|
package/dist/read.js
CHANGED
|
@@ -25,7 +25,7 @@ import { readFileBounded } from "./bounded-file.js";
|
|
|
25
25
|
import { CODING_OBSERVATION_EFFECT } from "./effects.js";
|
|
26
26
|
import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
27
27
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_IMAGE_BYTES, DEFAULT_MAX_LINES, DEFAULT_MAX_TEXT_SCAN_BYTES, HARD_MAX_BYTES, HARD_MAX_IMAGE_BYTES, HARD_MAX_LINES, HARD_MAX_TEXT_SCAN_BYTES, validateCodingLimit, } from "./limits.js";
|
|
28
|
-
import { resolveReadPathAsync } from "./path-utils.js";
|
|
28
|
+
import { resolveReadPathAsync, resolveToCwd } from "./path-utils.js";
|
|
29
29
|
import { formatSize } from "./truncate.js";
|
|
30
30
|
// --- magic-byte image MIME detection (faithful port of pi utils/mime.js, pure JS, no deps) ---
|
|
31
31
|
const IMAGE_TYPE_SNIFF_BYTES = 4100;
|
|
@@ -296,6 +296,41 @@ function errorResult(toolCallId, message) {
|
|
|
296
296
|
error: { message },
|
|
297
297
|
};
|
|
298
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Scan paginated `readText` output for the first line containing `findText` (literal substring,
|
|
301
|
+
* no regex). Starts at `startLine` and advances via `nextOffset` until a hit, EOF, or the scan
|
|
302
|
+
* cap. Returns the 1-indexed hit line, or `undefined` when the needle is not found before EOF.
|
|
303
|
+
* Throws on scan-limit / abort / no-progress so the caller's error path surfaces a clean result.
|
|
304
|
+
* Pages with `limit` unset so each backend page is `maxLines`-sized (fewer round trips).
|
|
305
|
+
*/
|
|
306
|
+
async function findMatchLine(ops, absolutePath, findText, findMode, startLine, maxLines, maxBytes, maxScanBytes, signal) {
|
|
307
|
+
const caseInsensitive = findMode === "case-insensitive";
|
|
308
|
+
const needle = caseInsensitive ? findText.toLowerCase() : findText;
|
|
309
|
+
let offset = startLine;
|
|
310
|
+
let guard = 0; // ponytail: safety cap for backends that report hasMore without advancing
|
|
311
|
+
while (true) {
|
|
312
|
+
if (signal?.aborted)
|
|
313
|
+
throw new Error("Operation aborted");
|
|
314
|
+
const page = await ops.readText(absolutePath, { offset, maxLines, maxBytes, maxScanBytes, signal });
|
|
315
|
+
if (!page.firstLineExceedsLimit && page.content.length > 0) {
|
|
316
|
+
const lines = page.content.split("\n");
|
|
317
|
+
for (let index = 0; index < lines.length; index++) {
|
|
318
|
+
const line = caseInsensitive ? lines[index].toLowerCase() : lines[index];
|
|
319
|
+
if (line.includes(needle))
|
|
320
|
+
return page.startLine + index;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (!page.hasMore || page.nextOffset === undefined)
|
|
324
|
+
return undefined;
|
|
325
|
+
if (page.scannedBytes >= maxScanBytes)
|
|
326
|
+
throw new Error(`findText did not match within ${formatSize(maxScanBytes)} scan limit`);
|
|
327
|
+
if (page.nextOffset <= offset)
|
|
328
|
+
throw new Error(`findText could not advance past line ${offset}`);
|
|
329
|
+
offset = page.nextOffset;
|
|
330
|
+
if (++guard > 100_000)
|
|
331
|
+
throw new Error("findText exceeded iteration guard");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
299
334
|
export function createReadTool(cwd, options) {
|
|
300
335
|
if (options && "autoResizeImages" in options) {
|
|
301
336
|
throw new TypeError('Read tool: "autoResizeImages" was removed in 0.1.5; use "transformImage" instead.');
|
|
@@ -316,6 +351,11 @@ export function createReadTool(cwd, options) {
|
|
|
316
351
|
path: { type: "string", description: "Path to the file to read (relative or absolute)" },
|
|
317
352
|
offset: { type: "number", description: "Line number to start reading from (1-indexed)" },
|
|
318
353
|
limit: { type: "number", description: "Maximum number of lines to read" },
|
|
354
|
+
findText: {
|
|
355
|
+
type: "string",
|
|
356
|
+
description: "Literal substring to search for (no regex). Returns the page starting at the first matching line at/after offset. No match is an error result.",
|
|
357
|
+
},
|
|
358
|
+
findMode: { type: "string", enum: ["exact", "case-insensitive"], description: "Match mode for findText (default 'exact')." },
|
|
319
359
|
},
|
|
320
360
|
required: ["path"],
|
|
321
361
|
additionalProperties: false,
|
|
@@ -328,13 +368,23 @@ export function createReadTool(cwd, options) {
|
|
|
328
368
|
const path = typeof args.path === "string" ? args.path : "";
|
|
329
369
|
const offset = typeof args.offset === "number" ? args.offset : undefined;
|
|
330
370
|
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
371
|
+
const findText = typeof args.findText === "string" ? args.findText : undefined;
|
|
372
|
+
const findModeRaw = typeof args.findMode === "string" ? args.findMode : "exact";
|
|
331
373
|
if (path.length === 0) {
|
|
332
374
|
return errorResult(toolCallId, "path is required and must be a non-empty string.");
|
|
333
375
|
}
|
|
376
|
+
if (findText !== undefined) {
|
|
377
|
+
if (findText.length === 0) {
|
|
378
|
+
return errorResult(toolCallId, "findText must be a non-empty string.");
|
|
379
|
+
}
|
|
380
|
+
if (findModeRaw !== "exact" && findModeRaw !== "case-insensitive") {
|
|
381
|
+
return errorResult(toolCallId, "findMode must be 'exact' or 'case-insensitive'.");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
334
384
|
try {
|
|
335
|
-
|
|
385
|
+
let startLine = validateCodingLimit("offset", offset ?? 1, Number.MAX_SAFE_INTEGER);
|
|
336
386
|
const requestedLines = limit === undefined ? undefined : validateCodingLimit("limit", limit, HARD_MAX_LINES);
|
|
337
|
-
const absolutePath = await resolveReadPathAsync(path, cwd);
|
|
387
|
+
const absolutePath = options?.operations ? resolveToCwd(path, cwd) : await resolveReadPathAsync(path, cwd);
|
|
338
388
|
const policyCheck = await enforceExecutionPolicy(options?.executionPolicy, {
|
|
339
389
|
kind: "read",
|
|
340
390
|
operation: "read",
|
|
@@ -393,6 +443,13 @@ export function createReadTool(cwd, options) {
|
|
|
393
443
|
};
|
|
394
444
|
}
|
|
395
445
|
}
|
|
446
|
+
if (findText !== undefined) {
|
|
447
|
+
const hit = await findMatchLine(ops, allowedPath, findText, findModeRaw, startLine, maxLines, maxBytes, maxScanBytes, context.signal);
|
|
448
|
+
if (hit === undefined) {
|
|
449
|
+
return errorResult(toolCallId, `Could not find ${JSON.stringify(findText)} in ${path}.`);
|
|
450
|
+
}
|
|
451
|
+
startLine = hit;
|
|
452
|
+
}
|
|
396
453
|
const page = await ops.readText(allowedPath, {
|
|
397
454
|
offset: startLine,
|
|
398
455
|
limit: requestedLines,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arnilo/prism-coding-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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.
|
|
32
|
-
"@arnilo/prism-workflows": "0.
|
|
31
|
+
"@arnilo/prism": "^0.3.1",
|
|
32
|
+
"@arnilo/prism-workflows": "^0.3.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@arnilo/prism": "file:../..",
|