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.
Files changed (154) hide show
  1. package/README.md +4 -30
  2. package/agents/audit.md +1 -2
  3. package/agents/build.md +1 -0
  4. package/agents/craft.md +12 -8
  5. package/agents/debug.md +2 -2
  6. package/agents/ideas.md +1 -0
  7. package/agents/scout.md +1 -0
  8. package/dist/agents.d.ts +41 -0
  9. package/dist/agents.d.ts.map +1 -0
  10. package/dist/agents.js +283 -0
  11. package/dist/agents.js.map +1 -0
  12. package/dist/batch/batch-bash.d.ts +87 -0
  13. package/dist/batch/batch-bash.d.ts.map +1 -0
  14. package/dist/batch/batch-bash.js +369 -0
  15. package/dist/batch/batch-bash.js.map +1 -0
  16. package/dist/batch/constants.d.ts +100 -0
  17. package/dist/batch/constants.d.ts.map +1 -0
  18. package/dist/batch/constants.js +15 -0
  19. package/dist/batch/constants.js.map +1 -0
  20. package/dist/batch/execute.d.ts +21 -0
  21. package/dist/batch/execute.d.ts.map +1 -0
  22. package/dist/batch/execute.js +440 -0
  23. package/dist/batch/execute.js.map +1 -0
  24. package/dist/batch/fuzzy-edit.d.ts +29 -0
  25. package/dist/batch/fuzzy-edit.d.ts.map +1 -0
  26. package/dist/batch/fuzzy-edit.js +257 -0
  27. package/dist/batch/fuzzy-edit.js.map +1 -0
  28. package/dist/batch/index.d.ts +85 -0
  29. package/dist/batch/index.d.ts.map +1 -0
  30. package/dist/batch/index.js +422 -0
  31. package/dist/batch/index.js.map +1 -0
  32. package/dist/batch/render.d.ts +14 -0
  33. package/dist/batch/render.d.ts.map +1 -0
  34. package/dist/batch/render.js +74 -0
  35. package/dist/batch/render.js.map +1 -0
  36. package/dist/batch/symbols.d.ts +9 -0
  37. package/dist/batch/symbols.d.ts.map +1 -0
  38. package/dist/batch/symbols.js +310 -0
  39. package/dist/batch/symbols.js.map +1 -0
  40. package/dist/batch.d.ts +12 -0
  41. package/dist/batch.d.ts.map +1 -0
  42. package/dist/batch.js +11 -0
  43. package/dist/batch.js.map +1 -0
  44. package/dist/cli-args.d.ts +27 -0
  45. package/dist/cli-args.d.ts.map +1 -0
  46. package/dist/cli-args.js +265 -0
  47. package/dist/cli-args.js.map +1 -0
  48. package/dist/config.d.ts +58 -0
  49. package/dist/config.d.ts.map +1 -0
  50. package/dist/config.js +296 -0
  51. package/dist/config.js.map +1 -0
  52. package/dist/depth.d.ts +25 -0
  53. package/dist/depth.d.ts.map +1 -0
  54. package/dist/depth.js +160 -0
  55. package/dist/depth.js.map +1 -0
  56. package/dist/executor.d.ts +87 -0
  57. package/dist/executor.d.ts.map +1 -0
  58. package/dist/executor.js +295 -0
  59. package/dist/executor.js.map +1 -0
  60. package/dist/flow-prompt.d.ts +23 -0
  61. package/dist/flow-prompt.d.ts.map +1 -0
  62. package/dist/flow-prompt.js +99 -0
  63. package/dist/flow-prompt.js.map +1 -0
  64. package/dist/flow.d.ts +76 -0
  65. package/dist/flow.d.ts.map +1 -0
  66. package/dist/flow.js +704 -0
  67. package/dist/flow.js.map +1 -0
  68. package/dist/index.d.ts +10 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +327 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/reasoning-strip.d.ts +26 -0
  73. package/dist/reasoning-strip.d.ts.map +1 -0
  74. package/dist/reasoning-strip.js +58 -0
  75. package/dist/reasoning-strip.js.map +1 -0
  76. package/dist/render-utils.d.ts +42 -0
  77. package/dist/render-utils.d.ts.map +1 -0
  78. package/dist/render-utils.js +182 -0
  79. package/dist/render-utils.js.map +1 -0
  80. package/dist/render.d.ts +24 -0
  81. package/dist/render.d.ts.map +1 -0
  82. package/dist/render.js +409 -0
  83. package/dist/render.js.map +1 -0
  84. package/dist/runner-events.d.ts +59 -0
  85. package/dist/runner-events.d.ts.map +1 -0
  86. package/dist/runner-events.js +539 -0
  87. package/dist/runner-events.js.map +1 -0
  88. package/dist/session-mode.d.ts +10 -0
  89. package/dist/session-mode.d.ts.map +1 -0
  90. package/dist/session-mode.js +25 -0
  91. package/dist/session-mode.js.map +1 -0
  92. package/dist/settings-resolver.d.ts +28 -0
  93. package/dist/settings-resolver.d.ts.map +1 -0
  94. package/dist/settings-resolver.js +148 -0
  95. package/dist/settings-resolver.js.map +1 -0
  96. package/dist/sliding-prompt.d.ts +40 -0
  97. package/dist/sliding-prompt.d.ts.map +1 -0
  98. package/dist/sliding-prompt.js +121 -0
  99. package/dist/sliding-prompt.js.map +1 -0
  100. package/dist/snapshot.d.ts +29 -0
  101. package/dist/snapshot.d.ts.map +1 -0
  102. package/dist/snapshot.js +199 -0
  103. package/dist/snapshot.js.map +1 -0
  104. package/dist/structured-output.d.ts +36 -0
  105. package/dist/structured-output.d.ts.map +1 -0
  106. package/dist/structured-output.js +244 -0
  107. package/dist/structured-output.js.map +1 -0
  108. package/dist/timed-bash.d.ts +45 -0
  109. package/dist/timed-bash.d.ts.map +1 -0
  110. package/dist/timed-bash.js +219 -0
  111. package/dist/timed-bash.js.map +1 -0
  112. package/dist/tool-utils.d.ts +20 -0
  113. package/dist/tool-utils.d.ts.map +1 -0
  114. package/dist/tool-utils.js +38 -0
  115. package/dist/tool-utils.js.map +1 -0
  116. package/dist/transitions.d.ts +39 -0
  117. package/dist/transitions.d.ts.map +1 -0
  118. package/dist/transitions.js +59 -0
  119. package/dist/transitions.js.map +1 -0
  120. package/dist/types.d.ts +207 -0
  121. package/dist/types.d.ts.map +1 -0
  122. package/dist/types.js +143 -0
  123. package/dist/types.js.map +1 -0
  124. package/dist/web-tool.d.ts +35 -0
  125. package/dist/web-tool.d.ts.map +1 -0
  126. package/dist/web-tool.js +545 -0
  127. package/dist/web-tool.js.map +1 -0
  128. package/package.json +7 -5
  129. package/src/agents.ts +0 -299
  130. package/src/ambient.d.ts +0 -107
  131. package/src/batch/batch-bash.ts +0 -443
  132. package/src/batch/constants.ts +0 -128
  133. package/src/batch/execute.ts +0 -551
  134. package/src/batch/fuzzy-edit.ts +0 -323
  135. package/src/batch/index.ts +0 -494
  136. package/src/batch/render.ts +0 -81
  137. package/src/batch/symbols.ts +0 -341
  138. package/src/batch.ts +0 -28
  139. package/src/cli-args.ts +0 -315
  140. package/src/config.ts +0 -391
  141. package/src/executor.ts +0 -445
  142. package/src/flow.ts +0 -834
  143. package/src/hooks.ts +0 -294
  144. package/src/index.ts +0 -1132
  145. package/src/render-utils.ts +0 -205
  146. package/src/render.ts +0 -524
  147. package/src/runner-events.ts +0 -692
  148. package/src/session-mode.ts +0 -33
  149. package/src/sliding-prompt.ts +0 -144
  150. package/src/structured-output.ts +0 -195
  151. package/src/timed-bash.ts +0 -270
  152. package/src/transitions.ts +0 -86
  153. package/src/types.ts +0 -386
  154. package/src/web-tool.ts +0 -663
@@ -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
- }