pi-agent-flow 1.8.1 → 1.8.3
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/README.md +4 -30
- package/agents/audit.md +1 -2
- package/agents/build.md +1 -0
- package/agents/craft.md +12 -8
- package/agents/debug.md +2 -2
- package/agents/ideas.md +1 -0
- package/agents/scout.md +1 -0
- package/dist/agents.d.ts +41 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +283 -0
- package/dist/agents.js.map +1 -0
- package/dist/batch/batch-bash.d.ts +87 -0
- package/dist/batch/batch-bash.d.ts.map +1 -0
- package/dist/batch/batch-bash.js +369 -0
- package/dist/batch/batch-bash.js.map +1 -0
- package/dist/batch/constants.d.ts +100 -0
- package/dist/batch/constants.d.ts.map +1 -0
- package/dist/batch/constants.js +15 -0
- package/dist/batch/constants.js.map +1 -0
- package/dist/batch/execute.d.ts +21 -0
- package/dist/batch/execute.d.ts.map +1 -0
- package/dist/batch/execute.js +440 -0
- package/dist/batch/execute.js.map +1 -0
- package/dist/batch/fuzzy-edit.d.ts +29 -0
- package/dist/batch/fuzzy-edit.d.ts.map +1 -0
- package/dist/batch/fuzzy-edit.js +257 -0
- package/dist/batch/fuzzy-edit.js.map +1 -0
- package/dist/batch/index.d.ts +85 -0
- package/dist/batch/index.d.ts.map +1 -0
- package/dist/batch/index.js +422 -0
- package/dist/batch/index.js.map +1 -0
- package/dist/batch/render.d.ts +14 -0
- package/dist/batch/render.d.ts.map +1 -0
- package/dist/batch/render.js +74 -0
- package/dist/batch/render.js.map +1 -0
- package/dist/batch/symbols.d.ts +9 -0
- package/dist/batch/symbols.d.ts.map +1 -0
- package/dist/batch/symbols.js +310 -0
- package/dist/batch/symbols.js.map +1 -0
- package/dist/batch.d.ts +12 -0
- package/dist/batch.d.ts.map +1 -0
- package/dist/batch.js +11 -0
- package/dist/batch.js.map +1 -0
- package/dist/cli-args.d.ts +27 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +265 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/config.d.ts +58 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +296 -0
- package/dist/config.js.map +1 -0
- package/dist/depth.d.ts +25 -0
- package/dist/depth.d.ts.map +1 -0
- package/dist/depth.js +160 -0
- package/dist/depth.js.map +1 -0
- package/dist/executor.d.ts +87 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +295 -0
- package/dist/executor.js.map +1 -0
- package/dist/flow-prompt.d.ts +23 -0
- package/dist/flow-prompt.d.ts.map +1 -0
- package/dist/flow-prompt.js +99 -0
- package/dist/flow-prompt.js.map +1 -0
- package/dist/flow.d.ts +76 -0
- package/dist/flow.d.ts.map +1 -0
- package/dist/flow.js +704 -0
- package/dist/flow.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +327 -0
- package/dist/index.js.map +1 -0
- package/dist/reasoning-strip.d.ts +26 -0
- package/dist/reasoning-strip.d.ts.map +1 -0
- package/dist/reasoning-strip.js +58 -0
- package/dist/reasoning-strip.js.map +1 -0
- package/dist/render-utils.d.ts +42 -0
- package/dist/render-utils.d.ts.map +1 -0
- package/dist/render-utils.js +182 -0
- package/dist/render-utils.js.map +1 -0
- package/dist/render.d.ts +24 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +409 -0
- package/dist/render.js.map +1 -0
- package/dist/runner-events.d.ts +59 -0
- package/dist/runner-events.d.ts.map +1 -0
- package/dist/runner-events.js +539 -0
- package/dist/runner-events.js.map +1 -0
- package/dist/session-mode.d.ts +10 -0
- package/dist/session-mode.d.ts.map +1 -0
- package/dist/session-mode.js +25 -0
- package/dist/session-mode.js.map +1 -0
- package/dist/settings-resolver.d.ts +28 -0
- package/dist/settings-resolver.d.ts.map +1 -0
- package/dist/settings-resolver.js +148 -0
- package/dist/settings-resolver.js.map +1 -0
- package/dist/sliding-prompt.d.ts +40 -0
- package/dist/sliding-prompt.d.ts.map +1 -0
- package/dist/sliding-prompt.js +121 -0
- package/dist/sliding-prompt.js.map +1 -0
- package/dist/snapshot.d.ts +29 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +199 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/structured-output.d.ts +36 -0
- package/dist/structured-output.d.ts.map +1 -0
- package/dist/structured-output.js +244 -0
- package/dist/structured-output.js.map +1 -0
- package/dist/timed-bash.d.ts +45 -0
- package/dist/timed-bash.d.ts.map +1 -0
- package/dist/timed-bash.js +219 -0
- package/dist/timed-bash.js.map +1 -0
- package/dist/tool-utils.d.ts +20 -0
- package/dist/tool-utils.d.ts.map +1 -0
- package/dist/tool-utils.js +38 -0
- package/dist/tool-utils.js.map +1 -0
- package/dist/transitions.d.ts +39 -0
- package/dist/transitions.d.ts.map +1 -0
- package/dist/transitions.js +59 -0
- package/dist/transitions.js.map +1 -0
- package/dist/types.d.ts +207 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +143 -0
- package/dist/types.js.map +1 -0
- package/dist/web-tool.d.ts +35 -0
- package/dist/web-tool.d.ts.map +1 -0
- package/dist/web-tool.js +545 -0
- package/dist/web-tool.js.map +1 -0
- package/package.json +7 -5
- package/src/agents.ts +0 -299
- package/src/ambient.d.ts +0 -107
- package/src/batch/batch-bash.ts +0 -443
- package/src/batch/constants.ts +0 -128
- package/src/batch/execute.ts +0 -551
- package/src/batch/fuzzy-edit.ts +0 -323
- package/src/batch/index.ts +0 -494
- package/src/batch/render.ts +0 -81
- package/src/batch/symbols.ts +0 -341
- package/src/batch.ts +0 -28
- package/src/cli-args.ts +0 -315
- package/src/config.ts +0 -391
- package/src/executor.ts +0 -445
- package/src/flow.ts +0 -834
- package/src/hooks.ts +0 -294
- package/src/index.ts +0 -1132
- package/src/render-utils.ts +0 -205
- package/src/render.ts +0 -524
- package/src/runner-events.ts +0 -692
- package/src/session-mode.ts +0 -33
- package/src/sliding-prompt.ts +0 -144
- package/src/structured-output.ts +0 -195
- package/src/timed-bash.ts +0 -270
- package/src/transitions.ts +0 -86
- package/src/types.ts +0 -386
- package/src/web-tool.ts +0 -663
package/src/batch/execute.ts
DELETED
|
@@ -1,551 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* batch — operation execution engine.
|
|
3
|
-
*
|
|
4
|
-
* Orchestrates sequential file operations (read/write/edit/delete) with
|
|
5
|
-
* skip-on-failure semantics, error enrichment, and result summarisation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from "node:fs/promises";
|
|
9
|
-
import * as path from "node:path";
|
|
10
|
-
import {
|
|
11
|
-
type FileOpInput,
|
|
12
|
-
type ExecuteOptions,
|
|
13
|
-
type ReadOptions,
|
|
14
|
-
type OpResult,
|
|
15
|
-
MAX_LINES,
|
|
16
|
-
MAX_BYTES,
|
|
17
|
-
SAFE_FULL_READ_LIMIT,
|
|
18
|
-
TARGETED_READ_LINE_LIMIT,
|
|
19
|
-
MAX_TOTAL_RESULT_LINES,
|
|
20
|
-
} from "./constants.js";
|
|
21
|
-
import {
|
|
22
|
-
normalizeToLF,
|
|
23
|
-
restoreLineEndings,
|
|
24
|
-
detectLineEnding,
|
|
25
|
-
stripBom,
|
|
26
|
-
applyEdits,
|
|
27
|
-
levenshtein,
|
|
28
|
-
expandTilde,
|
|
29
|
-
validatePath,
|
|
30
|
-
} from "./fuzzy-edit.js";
|
|
31
|
-
import { buildFileContextMap } from "./symbols.js";
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// Read helpers
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
function isBatchRead(options: ExecuteOptions): boolean {
|
|
38
|
-
return options.readOptions?.toolName === "batch_read";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isFullFileRead(op: FileOpInput, totalLines: number): boolean {
|
|
42
|
-
const start = op.s ?? 1;
|
|
43
|
-
if (start !== 1) return false;
|
|
44
|
-
return op.l === undefined || op.l >= totalLines;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function buildBatchReadSafetyWarning(): string {
|
|
48
|
-
return `[batch_read safety] Raw content truncated at ${TARGETED_READ_LINE_LIMIT} lines to preserve context. Adjust your 's' and 'l' parameters to read further.`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function readWithOffsetLimit(
|
|
52
|
-
content: string,
|
|
53
|
-
offset?: number,
|
|
54
|
-
limit?: number,
|
|
55
|
-
filePath?: string,
|
|
56
|
-
options: ReadOptions = {},
|
|
57
|
-
): { content: string; truncated: boolean; nextOffset?: number; linesRead: number } {
|
|
58
|
-
const allLines = content.split("\n");
|
|
59
|
-
const totalFileLines = allLines.length;
|
|
60
|
-
const shouldTruncate = options.truncate !== false;
|
|
61
|
-
const toolName = options.toolName ?? "batch";
|
|
62
|
-
|
|
63
|
-
// Validate offset
|
|
64
|
-
if (offset !== undefined && offset > totalFileLines) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Determine the start line (convert 1-indexed to 0-indexed)
|
|
71
|
-
const startLine = offset !== undefined ? Math.max(0, offset - 1) : 0;
|
|
72
|
-
|
|
73
|
-
// Determine end line
|
|
74
|
-
let endLine = totalFileLines;
|
|
75
|
-
if (limit !== undefined) {
|
|
76
|
-
endLine = Math.min(startLine + limit, totalFileLines);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
let selectedLines = allLines.slice(startLine, endLine);
|
|
80
|
-
let truncated = false;
|
|
81
|
-
let nextOffset: number | undefined;
|
|
82
|
-
|
|
83
|
-
// Apply max-lines cap for regular batch reads. batch_read clamps oversized
|
|
84
|
-
// targeted reads before this helper and context-maps large full-file reads.
|
|
85
|
-
if (shouldTruncate && selectedLines.length > MAX_LINES) {
|
|
86
|
-
selectedLines = selectedLines.slice(0, MAX_LINES);
|
|
87
|
-
truncated = true;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// A single selected line that exceeds the byte cap is not safely splittable by
|
|
91
|
-
// line-oriented offsets, so keep the existing hard error in both modes.
|
|
92
|
-
for (let i = 0; i < selectedLines.length; i++) {
|
|
93
|
-
if (Buffer.byteLength(selectedLines[i], "utf-8") > MAX_BYTES) {
|
|
94
|
-
const lineDisplay = startLine + i + 1;
|
|
95
|
-
throw new Error(
|
|
96
|
-
`Line ${lineDisplay} exceeds limit. Try: ${toolName} with o:"read", s:${lineDisplay}, l:10, or use bash: head -c ... ${filePath ?? "<file>"}`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Join and check byte size
|
|
102
|
-
let result = selectedLines.join("\n");
|
|
103
|
-
|
|
104
|
-
// Truncate by total bytes for regular batch reads only. batch_read relies on
|
|
105
|
-
// its line-oriented safety guards and still rejects an individual huge line.
|
|
106
|
-
if (shouldTruncate && Buffer.byteLength(result, "utf-8") > MAX_BYTES) {
|
|
107
|
-
let byteAccum = 0;
|
|
108
|
-
let keepLines = 0;
|
|
109
|
-
for (let i = 0; i < selectedLines.length; i++) {
|
|
110
|
-
byteAccum += Buffer.byteLength(selectedLines[i], "utf-8") + (i > 0 ? 1 : 0); // newline separator between lines
|
|
111
|
-
if (byteAccum > MAX_BYTES) break;
|
|
112
|
-
keepLines = i + 1;
|
|
113
|
-
}
|
|
114
|
-
selectedLines = selectedLines.slice(0, keepLines);
|
|
115
|
-
result = selectedLines.join("\n");
|
|
116
|
-
truncated = true;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Calculate nextOffset for continuation
|
|
120
|
-
const lastLineRead = startLine + selectedLines.length;
|
|
121
|
-
if (truncated || (limit !== undefined && lastLineRead < totalFileLines)) {
|
|
122
|
-
nextOffset = lastLineRead + 1; // 1-indexed
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Append truncation/continuation hints
|
|
126
|
-
if (truncated) {
|
|
127
|
-
const endDisplay = startLine + selectedLines.length;
|
|
128
|
-
const startDisplay = startLine + 1;
|
|
129
|
-
result += `\n\n[Showing lines ${startDisplay}-${endDisplay} of ${totalFileLines}. Use s=${nextOffset} to continue.]`;
|
|
130
|
-
} else if (limit !== undefined && lastLineRead < totalFileLines) {
|
|
131
|
-
const remaining = totalFileLines - lastLineRead;
|
|
132
|
-
result += `\n\n[${remaining} more lines in file. Use s=${nextOffset} to continue.]`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return { content: result, truncated, nextOffset, linesRead: selectedLines.length };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
// Suggestions
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
export async function suggestSimilarFiles(
|
|
143
|
-
inputPath: string,
|
|
144
|
-
cwd: string,
|
|
145
|
-
): Promise<string[]> {
|
|
146
|
-
const resolved = path.resolve(cwd, inputPath);
|
|
147
|
-
const dir = path.dirname(resolved);
|
|
148
|
-
const target = path.basename(resolved);
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
152
|
-
const candidates: { name: string; dist: number }[] = [];
|
|
153
|
-
|
|
154
|
-
for (const entry of entries) {
|
|
155
|
-
const name = entry.name;
|
|
156
|
-
// Skip hidden files and node_modules
|
|
157
|
-
if (name.startsWith(".") || name === "node_modules") continue;
|
|
158
|
-
|
|
159
|
-
const dist = levenshtein(target.toLowerCase(), name.toLowerCase());
|
|
160
|
-
const maxLen = Math.max(target.length, name.length);
|
|
161
|
-
// Only suggest if reasonably similar (within 40% edit distance, or shares prefix)
|
|
162
|
-
if (dist <= Math.ceil(maxLen * 0.4) || name.startsWith(target.slice(0, 3))) {
|
|
163
|
-
candidates.push({ name: entry.isDirectory() ? name + "/" : name, dist });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return candidates
|
|
168
|
-
.sort((a, b) => a.dist - b.dist)
|
|
169
|
-
.slice(0, 3)
|
|
170
|
-
.map((c) => path.join(path.relative(cwd, dir), c.name));
|
|
171
|
-
} catch {
|
|
172
|
-
return [];
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ---------------------------------------------------------------------------
|
|
177
|
-
// Error hints
|
|
178
|
-
// ---------------------------------------------------------------------------
|
|
179
|
-
|
|
180
|
-
function getErrorHint(error: string): string {
|
|
181
|
-
if (error.includes("File not found") || error.includes("file not found"))
|
|
182
|
-
return "Verify the path exists.";
|
|
183
|
-
if (error.includes("Could not find"))
|
|
184
|
-
return "Re-read the file first, then retry with exact f (oldText).";
|
|
185
|
-
if (error.includes("occurrences"))
|
|
186
|
-
return "Add more surrounding context to make oldText unique.";
|
|
187
|
-
if (error.includes("overlap"))
|
|
188
|
-
return "Merge overlapping edits into one.";
|
|
189
|
-
if (error.includes("No changes"))
|
|
190
|
-
return "File already has this content. No edit needed.";
|
|
191
|
-
if (error.includes("is not readable") || error.includes("not readable"))
|
|
192
|
-
return "Check file permissions.";
|
|
193
|
-
if (error.includes("ENOENT") || error.includes("no such file"))
|
|
194
|
-
return "Verify the path exists.";
|
|
195
|
-
if (error.includes("is beyond end of file"))
|
|
196
|
-
return "Use a smaller offset within the file length.";
|
|
197
|
-
return "";
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ---------------------------------------------------------------------------
|
|
201
|
-
// Main execute function
|
|
202
|
-
// ---------------------------------------------------------------------------
|
|
203
|
-
|
|
204
|
-
export async function executeOperations(
|
|
205
|
-
operations: FileOpInput[],
|
|
206
|
-
cwd: string,
|
|
207
|
-
signal?: AbortSignal,
|
|
208
|
-
options: ExecuteOptions = {},
|
|
209
|
-
): Promise<{ summary: string; contentText: string; results: OpResult[] }> {
|
|
210
|
-
const results: OpResult[] = [];
|
|
211
|
-
let failed = false;
|
|
212
|
-
|
|
213
|
-
const counts = { read: 0, write: 0, edit: 0, delete: 0, error: 0, skipped: 0 };
|
|
214
|
-
const errors: { path: string; op: string; message: string; hint?: string }[] = [];
|
|
215
|
-
const truncatedFiles: { path: string; shown: number; total: number; nextOffset?: number }[] = [];
|
|
216
|
-
const aggregateLimitSkipped: { path: string }[] = [];
|
|
217
|
-
let aggregateLinesRead = 0;
|
|
218
|
-
const includeLimitWarnings = options.includeLimitWarnings ?? true;
|
|
219
|
-
|
|
220
|
-
for (const op of operations) {
|
|
221
|
-
if (signal?.aborted) {
|
|
222
|
-
results.push({ op: op.o, path: op.p, status: "skipped", error: "Operation aborted." });
|
|
223
|
-
counts.skipped++;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (failed) {
|
|
228
|
-
results.push({ op: op.o, path: op.p, status: "skipped" });
|
|
229
|
-
counts.skipped++;
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
const resolvedPath = await validatePath(op.p, cwd);
|
|
235
|
-
|
|
236
|
-
switch (op.o) {
|
|
237
|
-
case "read": {
|
|
238
|
-
if (aggregateLinesRead >= MAX_TOTAL_RESULT_LINES) {
|
|
239
|
-
results.push({
|
|
240
|
-
op: "read",
|
|
241
|
-
path: op.p,
|
|
242
|
-
status: "skipped",
|
|
243
|
-
error: `Skipped: aggregate line limit of ${MAX_TOTAL_RESULT_LINES} already reached. Use separate batch/batch_read calls.`,
|
|
244
|
-
});
|
|
245
|
-
counts.skipped++;
|
|
246
|
-
aggregateLimitSkipped.push({ path: op.p });
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Access check before reading
|
|
251
|
-
try {
|
|
252
|
-
await fs.access(resolvedPath);
|
|
253
|
-
} catch {
|
|
254
|
-
throw new Error(`File not found: ${op.p}`);
|
|
255
|
-
}
|
|
256
|
-
try {
|
|
257
|
-
await fs.access(resolvedPath, fs.constants.R_OK);
|
|
258
|
-
} catch {
|
|
259
|
-
throw new Error(`File not readable: ${op.p}`);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const rawContent = await fs.readFile(resolvedPath, "utf-8");
|
|
263
|
-
const { text } = stripBom(rawContent);
|
|
264
|
-
const allLines = text.split("\n");
|
|
265
|
-
const totalFileLines = allLines.length;
|
|
266
|
-
|
|
267
|
-
if (isBatchRead(options) && isFullFileRead(op, totalFileLines) && totalFileLines > SAFE_FULL_READ_LIMIT) {
|
|
268
|
-
const context = buildFileContextMap(op.p, allLines);
|
|
269
|
-
results.push({
|
|
270
|
-
op: "read",
|
|
271
|
-
path: op.p,
|
|
272
|
-
status: "ok",
|
|
273
|
-
totalLines: totalFileLines,
|
|
274
|
-
contextMap: true,
|
|
275
|
-
language: context.language !== "plain" ? context.language : undefined,
|
|
276
|
-
symbols: context.symbols.length > 0 ? context.symbols : undefined,
|
|
277
|
-
symbolsTruncated: context.symbolsTruncated,
|
|
278
|
-
});
|
|
279
|
-
counts.read++;
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
let effectiveLimit = op.l;
|
|
284
|
-
let safetyTruncated = false;
|
|
285
|
-
let safetyWarning: string | undefined;
|
|
286
|
-
if (isBatchRead(options) && !isFullFileRead(op, totalFileLines)) {
|
|
287
|
-
if (effectiveLimit === undefined || effectiveLimit > TARGETED_READ_LINE_LIMIT) {
|
|
288
|
-
effectiveLimit = TARGETED_READ_LINE_LIMIT;
|
|
289
|
-
safetyTruncated = true;
|
|
290
|
-
safetyWarning = buildBatchReadSafetyWarning();
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const { content: readContent, truncated, nextOffset, linesRead } =
|
|
295
|
-
readWithOffsetLimit(text, op.s, effectiveLimit, op.p, options.readOptions);
|
|
296
|
-
const finalContent = safetyWarning
|
|
297
|
-
? `${readContent}\n\n${safetyWarning}`
|
|
298
|
-
: readContent;
|
|
299
|
-
const finalTruncated = truncated || safetyTruncated;
|
|
300
|
-
|
|
301
|
-
if (finalTruncated || (includeLimitWarnings && effectiveLimit !== undefined && (op.s ?? 1) - 1 + effectiveLimit < totalFileLines)) {
|
|
302
|
-
const shownLines = finalTruncated ? linesRead : effectiveLimit!;
|
|
303
|
-
truncatedFiles.push({
|
|
304
|
-
path: op.p,
|
|
305
|
-
shown: shownLines,
|
|
306
|
-
total: totalFileLines,
|
|
307
|
-
nextOffset,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
aggregateLinesRead += linesRead;
|
|
312
|
-
|
|
313
|
-
results.push({
|
|
314
|
-
op: "read",
|
|
315
|
-
path: op.p,
|
|
316
|
-
status: "ok",
|
|
317
|
-
content: finalContent,
|
|
318
|
-
totalLines: totalFileLines,
|
|
319
|
-
warning: safetyWarning,
|
|
320
|
-
truncated: finalTruncated || undefined,
|
|
321
|
-
nextOffset,
|
|
322
|
-
});
|
|
323
|
-
counts.read++;
|
|
324
|
-
break;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
case "write": {
|
|
328
|
-
if (!op.c && op.c !== "") {
|
|
329
|
-
throw new Error("c (content) is required for write operations.");
|
|
330
|
-
}
|
|
331
|
-
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
332
|
-
await fs.writeFile(resolvedPath, op.c!, "utf-8");
|
|
333
|
-
results.push({
|
|
334
|
-
op: "write",
|
|
335
|
-
path: op.p,
|
|
336
|
-
status: "ok",
|
|
337
|
-
bytes: Buffer.byteLength(op.c!, "utf-8"),
|
|
338
|
-
});
|
|
339
|
-
counts.write++;
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
case "edit": {
|
|
344
|
-
if (!op.e || op.e.length === 0) {
|
|
345
|
-
throw new Error("e (edits) array is required for edit operations.");
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const rawContent = await fs.readFile(resolvedPath, "utf-8");
|
|
349
|
-
const { bom, text: contentWithoutBom } = stripBom(rawContent);
|
|
350
|
-
const originalEnding = detectLineEnding(contentWithoutBom);
|
|
351
|
-
const normalizedContent = normalizeToLF(contentWithoutBom);
|
|
352
|
-
|
|
353
|
-
const { newContent, blocksChanged } = applyEdits(
|
|
354
|
-
normalizedContent,
|
|
355
|
-
op.e,
|
|
356
|
-
op.p,
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
360
|
-
await fs.writeFile(resolvedPath, finalContent, "utf-8");
|
|
361
|
-
|
|
362
|
-
results.push({
|
|
363
|
-
op: "edit",
|
|
364
|
-
path: op.p,
|
|
365
|
-
status: "ok",
|
|
366
|
-
blocksChanged,
|
|
367
|
-
});
|
|
368
|
-
counts.edit++;
|
|
369
|
-
break;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
case "delete": {
|
|
373
|
-
let stat;
|
|
374
|
-
try {
|
|
375
|
-
stat = await fs.lstat(resolvedPath);
|
|
376
|
-
} catch (err: any) {
|
|
377
|
-
if (err.code === "ENOENT") {
|
|
378
|
-
throw new Error(`File not found: ${op.p}`);
|
|
379
|
-
}
|
|
380
|
-
throw err;
|
|
381
|
-
}
|
|
382
|
-
if (stat.isDirectory()) {
|
|
383
|
-
throw new Error(`Cannot delete directory: ${op.p}. Use a recursive removal tool or delete files individually.`);
|
|
384
|
-
}
|
|
385
|
-
await fs.unlink(resolvedPath);
|
|
386
|
-
results.push({ op: "delete", path: op.p, status: "ok" });
|
|
387
|
-
counts.delete++;
|
|
388
|
-
break;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
default:
|
|
392
|
-
throw new Error(`Unknown operation type: ${op.o}`);
|
|
393
|
-
}
|
|
394
|
-
} catch (err) {
|
|
395
|
-
failed = true;
|
|
396
|
-
counts.error++;
|
|
397
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
398
|
-
|
|
399
|
-
// Enrich file-not-found errors with fuzzy filename suggestions
|
|
400
|
-
let hint = getErrorHint(message);
|
|
401
|
-
if (
|
|
402
|
-
message.includes("File not found") ||
|
|
403
|
-
message.includes("file not found") ||
|
|
404
|
-
message.includes("ENOENT") ||
|
|
405
|
-
message.includes("no such file")
|
|
406
|
-
) {
|
|
407
|
-
const suggestions = await suggestSimilarFiles(op.p, cwd);
|
|
408
|
-
if (suggestions.length > 0) {
|
|
409
|
-
hint += ` Did you mean: ${suggestions.join(", ")}?`;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
errors.push({ path: op.p, op: op.o, message, hint });
|
|
414
|
-
results.push({
|
|
415
|
-
op: op.o,
|
|
416
|
-
path: op.p,
|
|
417
|
-
status: "error",
|
|
418
|
-
error: message,
|
|
419
|
-
hint,
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
// Build the enhanced summary and content text
|
|
424
|
-
const summary = buildSummary(counts, errors, truncatedFiles, aggregateLimitSkipped);
|
|
425
|
-
const contentText = buildContentText(summary, results);
|
|
426
|
-
|
|
427
|
-
return { summary, contentText, results };
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// ---------------------------------------------------------------------------
|
|
431
|
-
// Summary / content rendering
|
|
432
|
-
// ---------------------------------------------------------------------------
|
|
433
|
-
|
|
434
|
-
function buildSummary(
|
|
435
|
-
counts: { read: number; write: number; edit: number; delete: number; error: number; skipped: number },
|
|
436
|
-
errors: { path: string; op: string; message: string; hint?: string }[],
|
|
437
|
-
truncatedFiles: { path: string; shown: number; total: number; nextOffset?: number }[],
|
|
438
|
-
aggregateLimitSkipped: { path: string }[] = [],
|
|
439
|
-
): string {
|
|
440
|
-
const totalSuccess =
|
|
441
|
-
counts.read + counts.write + counts.edit + counts.delete;
|
|
442
|
-
const totalOps = totalSuccess + counts.error + counts.skipped;
|
|
443
|
-
|
|
444
|
-
const parts: string[] = [];
|
|
445
|
-
|
|
446
|
-
// Build the success breakdown
|
|
447
|
-
const successParts: string[] = [];
|
|
448
|
-
if (counts.read > 0)
|
|
449
|
-
successParts.push(
|
|
450
|
-
`${counts.read} read${counts.read > 1 ? "s" : ""}`,
|
|
451
|
-
);
|
|
452
|
-
if (counts.write > 0)
|
|
453
|
-
successParts.push(
|
|
454
|
-
`${counts.write} write${counts.write > 1 ? "s" : ""}`,
|
|
455
|
-
);
|
|
456
|
-
if (counts.edit > 0)
|
|
457
|
-
successParts.push(
|
|
458
|
-
`${counts.edit} edit${counts.edit > 1 ? "s" : ""}`,
|
|
459
|
-
);
|
|
460
|
-
if (counts.delete > 0)
|
|
461
|
-
successParts.push(
|
|
462
|
-
`${counts.delete} delete${counts.delete > 1 ? "s" : ""}`,
|
|
463
|
-
);
|
|
464
|
-
|
|
465
|
-
if (counts.error === 0) {
|
|
466
|
-
// All success
|
|
467
|
-
parts.push(`✓ ${totalOps} operations: ${successParts.join(", ")}`);
|
|
468
|
-
} else {
|
|
469
|
-
// Mixed success/failure
|
|
470
|
-
parts.push(
|
|
471
|
-
`✗ ${counts.error} failed${counts.skipped > 0 ? `, ${counts.skipped} skipped` : ""}`,
|
|
472
|
-
);
|
|
473
|
-
if (totalSuccess > 0) {
|
|
474
|
-
parts.push(` ✓ ${successParts.join(", ")} ok`);
|
|
475
|
-
}
|
|
476
|
-
for (const err of errors) {
|
|
477
|
-
const hint = err.hint ?? "";
|
|
478
|
-
const hintSuffix = hint ? ` — ${hint}` : "";
|
|
479
|
-
parts.push(` ✗ ${err.op} ${err.path}: ${err.message}${hintSuffix}`);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Truncation warnings
|
|
484
|
-
for (const tf of truncatedFiles) {
|
|
485
|
-
if (tf.nextOffset) {
|
|
486
|
-
parts.push(
|
|
487
|
-
` ⚠ ${tf.path} truncated (${tf.shown}/${tf.total} lines) — use s=${tf.nextOffset}`,
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Aggregate line limit warnings
|
|
493
|
-
if (aggregateLimitSkipped.length > 0) {
|
|
494
|
-
parts.push(
|
|
495
|
-
` ⚠ Aggregate line limit (${MAX_TOTAL_RESULT_LINES}) reached — skipped ${aggregateLimitSkipped.length} read${aggregateLimitSkipped.length > 1 ? "s" : ""}: ${aggregateLimitSkipped.map((s) => s.path).join(", ")}`,
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
return parts.join("\n");
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
export function buildContextMapText(result: OpResult): string {
|
|
503
|
-
const title = result.language || result.symbols ? "context map" : "file summary";
|
|
504
|
-
const lines: string[] = [`\n--- ${result.path} ${title} ---`];
|
|
505
|
-
lines.push(`Total lines: ${result.totalLines ?? 0}`);
|
|
506
|
-
if (result.language) lines.push(`Language: ${result.language}`);
|
|
507
|
-
lines.push("");
|
|
508
|
-
lines.push(`Full-file content omitted because file exceeds SAFE_FULL_READ_LIMIT=${SAFE_FULL_READ_LIMIT} lines.`);
|
|
509
|
-
lines.push("Use targeted reads with s/l, for example:");
|
|
510
|
-
lines.push(`{ "o": "read", "p": "${result.path}", "s": <startLine>, "l": <lineCount> }`);
|
|
511
|
-
|
|
512
|
-
if (result.symbols && result.symbols.length > 0) {
|
|
513
|
-
lines.push("");
|
|
514
|
-
lines.push("Context map:");
|
|
515
|
-
for (const entry of result.symbols) {
|
|
516
|
-
lines.push(`- ${entry.kind} ${entry.name} ${entry.startLine}-${entry.endLine}`);
|
|
517
|
-
}
|
|
518
|
-
if (result.symbolsTruncated) {
|
|
519
|
-
lines.push(`... [Context map truncated. Over ${100} entries detected. Use targeted reads to explore further.]`);
|
|
520
|
-
}
|
|
521
|
-
} else if (result.language) {
|
|
522
|
-
lines.push("");
|
|
523
|
-
lines.push("No context map entries detected for this structured file.");
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
return lines.join("\n");
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
function buildContentText(summary: string, results: OpResult[]): string {
|
|
530
|
-
const sections: string[] = [summary];
|
|
531
|
-
|
|
532
|
-
for (const r of results) {
|
|
533
|
-
if (r.op === "read" && r.status === "ok" && r.contextMap) {
|
|
534
|
-
sections.push(buildContextMapText(r));
|
|
535
|
-
} else if (r.op === "read" && r.status === "ok" && r.content) {
|
|
536
|
-
const lineInfo = r.totalLines !== undefined ? ` (${r.totalLines} lines)` : "";
|
|
537
|
-
sections.push(`\n--- ${r.path}${lineInfo} ---\n${r.content}`);
|
|
538
|
-
} else if (r.op === "edit" && r.status === "ok") {
|
|
539
|
-
const blockInfo = r.blocksChanged !== undefined ? `${r.blocksChanged} block${r.blocksChanged > 1 ? "s" : ""}` : "";
|
|
540
|
-
sections.push(`\n--- edit: ${r.path} (${blockInfo}) ---`);
|
|
541
|
-
} else if (r.op === "write" && r.status === "ok") {
|
|
542
|
-
sections.push(`\n--- write: ${r.path} (${r.bytes ?? 0} bytes) ---`);
|
|
543
|
-
} else if (r.op === "delete" && r.status === "ok") {
|
|
544
|
-
sections.push(`\n--- delete: ${r.path} ---`);
|
|
545
|
-
} else if (r.status === "error") {
|
|
546
|
-
sections.push(`\n--- ${r.op}: ${r.path} ---\nError: ${r.error}`);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return sections.join("");
|
|
551
|
-
}
|