pi-agent-flow 2.0.0 → 2.0.2
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 +126 -489
- package/agents/audit.md +7 -2
- package/agents/build.md +6 -2
- package/agents/craft.md +6 -3
- package/agents/debug.md +7 -3
- package/agents/ideas.md +8 -4
- package/agents/scout.md +8 -1
- package/dist/batch/apply-patch.d.ts +60 -0
- package/dist/batch/apply-patch.d.ts.map +1 -0
- package/dist/batch/apply-patch.js +477 -0
- package/dist/batch/apply-patch.js.map +1 -0
- package/dist/batch/batch-bash.d.ts +0 -6
- package/dist/batch/batch-bash.d.ts.map +1 -1
- package/dist/batch/batch-bash.js +52 -14
- package/dist/batch/batch-bash.js.map +1 -1
- package/dist/batch/constants.d.ts +45 -4
- package/dist/batch/constants.d.ts.map +1 -1
- package/dist/batch/constants.js +72 -4
- package/dist/batch/constants.js.map +1 -1
- package/dist/batch/execute.d.ts +8 -2
- package/dist/batch/execute.d.ts.map +1 -1
- package/dist/batch/execute.js +338 -70
- package/dist/batch/execute.js.map +1 -1
- package/dist/batch/fuzzy-edit.d.ts +4 -1
- package/dist/batch/fuzzy-edit.d.ts.map +1 -1
- package/dist/batch/fuzzy-edit.js +7 -2
- package/dist/batch/fuzzy-edit.js.map +1 -1
- package/dist/batch/index.d.ts +3 -15
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +48 -78
- package/dist/batch/index.js.map +1 -1
- package/dist/batch/render.d.ts.map +1 -1
- package/dist/batch/render.js +30 -7
- package/dist/batch/render.js.map +1 -1
- package/dist/batch/shell-compress.d.ts +25 -0
- package/dist/batch/shell-compress.d.ts.map +1 -0
- package/dist/batch/shell-compress.js +602 -0
- package/dist/batch/shell-compress.js.map +1 -0
- package/dist/batch/summary.d.ts.map +1 -1
- package/dist/batch/summary.js +4 -0
- package/dist/batch/summary.js.map +1 -1
- package/dist/batch/symbols.d.ts.map +1 -1
- package/dist/batch/symbols.js +12 -7
- package/dist/batch/symbols.js.map +1 -1
- package/dist/config/config.d.ts +5 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +63 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/models.d.ts +2 -0
- package/dist/config/models.d.ts.map +1 -0
- package/dist/config/models.js +49 -0
- package/dist/config/models.js.map +1 -0
- package/dist/config/settings-resolver.js +2 -2
- package/dist/config/settings-resolver.js.map +1 -1
- package/dist/core/agents.js +2 -2
- package/dist/core/agents.js.map +1 -1
- package/dist/core/depth.d.ts +3 -3
- package/dist/core/depth.d.ts.map +1 -1
- package/dist/core/depth.js +5 -5
- package/dist/core/depth.js.map +1 -1
- package/dist/core/executor.d.ts +10 -3
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +7 -3
- package/dist/core/executor.js.map +1 -1
- package/dist/core/flow.d.ts +19 -3
- package/dist/core/flow.d.ts.map +1 -1
- package/dist/core/flow.js +97 -58
- package/dist/core/flow.js.map +1 -1
- package/dist/core/session-mode.d.ts +1 -1
- package/dist/core/session-mode.d.ts.map +1 -1
- package/dist/core/session-mode.js +2 -1
- package/dist/core/session-mode.js.map +1 -1
- package/dist/core/{delegation.d.ts → transition.d.ts} +9 -9
- package/dist/core/transition.d.ts.map +1 -0
- package/dist/core/{delegation.js → transition.js} +17 -24
- package/dist/core/transition.js.map +1 -0
- package/dist/flow/auto-warp.d.ts +12 -0
- package/dist/flow/auto-warp.d.ts.map +1 -0
- package/dist/flow/auto-warp.js +29 -0
- package/dist/flow/auto-warp.js.map +1 -0
- package/dist/flow/command.d.ts.map +1 -1
- package/dist/flow/command.js +7 -2
- package/dist/flow/command.js.map +1 -1
- package/dist/flow/continuation.d.ts.map +1 -1
- package/dist/flow/continuation.js +52 -15
- package/dist/flow/continuation.js.map +1 -1
- package/dist/flow/index.d.ts +5 -2
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +6 -3
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/loop-command.d.ts +8 -0
- package/dist/flow/loop-command.d.ts.map +1 -0
- package/dist/flow/loop-command.js +102 -0
- package/dist/flow/loop-command.js.map +1 -0
- package/dist/flow/loop-templates.d.ts +7 -0
- package/dist/flow/loop-templates.d.ts.map +1 -0
- package/dist/flow/loop-templates.js +38 -0
- package/dist/flow/loop-templates.js.map +1 -0
- package/dist/flow/loop.d.ts +19 -0
- package/dist/flow/loop.d.ts.map +1 -0
- package/dist/flow/loop.js +95 -0
- package/dist/flow/loop.js.map +1 -0
- package/dist/flow/settings-command.d.ts.map +1 -1
- package/dist/flow/settings-command.js +93 -4
- package/dist/flow/settings-command.js.map +1 -1
- package/dist/flow/store.d.ts +3 -3
- package/dist/flow/store.d.ts.map +1 -1
- package/dist/flow/store.js +24 -16
- package/dist/flow/store.js.map +1 -1
- package/dist/flow/template-shared.d.ts +9 -0
- package/dist/flow/template-shared.d.ts.map +1 -0
- package/dist/flow/template-shared.js +13 -0
- package/dist/flow/template-shared.js.map +1 -0
- package/dist/flow/template-strings.d.ts.map +1 -1
- package/dist/flow/template-strings.js +2 -5
- package/dist/flow/template-strings.js.map +1 -1
- package/dist/flow/types.d.ts +15 -9
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/flow/warp.d.ts +15 -0
- package/dist/flow/warp.d.ts.map +1 -0
- package/dist/flow/warp.js +207 -0
- package/dist/flow/warp.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +185 -28
- package/dist/index.js.map +1 -1
- package/dist/snapshot/cli-args.d.ts +1 -0
- package/dist/snapshot/cli-args.d.ts.map +1 -1
- package/dist/snapshot/cli-args.js +12 -0
- package/dist/snapshot/cli-args.js.map +1 -1
- package/dist/snapshot/runner-events.d.ts +5 -0
- package/dist/snapshot/runner-events.d.ts.map +1 -1
- package/dist/snapshot/runner-events.js +89 -15
- package/dist/snapshot/runner-events.js.map +1 -1
- package/dist/snapshot/snapshot.d.ts +22 -19
- package/dist/snapshot/snapshot.d.ts.map +1 -1
- package/dist/snapshot/snapshot.js +1063 -167
- package/dist/snapshot/snapshot.js.map +1 -1
- package/dist/steering/flow-prompt.d.ts +2 -2
- package/dist/steering/flow-prompt.d.ts.map +1 -1
- package/dist/steering/flow-prompt.js +4 -4
- package/dist/steering/flow-prompt.js.map +1 -1
- package/dist/steering/sliding-prompt.d.ts.map +1 -1
- package/dist/steering/sliding-prompt.js +9 -6
- package/dist/steering/sliding-prompt.js.map +1 -1
- package/dist/steering/tool-utils.d.ts +31 -8
- package/dist/steering/tool-utils.d.ts.map +1 -1
- package/dist/steering/tool-utils.js +63 -30
- package/dist/steering/tool-utils.js.map +1 -1
- package/dist/tools/ask-user.d.ts +0 -17
- package/dist/tools/ask-user.d.ts.map +1 -1
- package/dist/tools/ask-user.js +13 -37
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/timed-bash.d.ts +1 -1
- package/dist/tools/timed-bash.d.ts.map +1 -1
- package/dist/tools/timed-bash.js +19 -8
- package/dist/tools/timed-bash.js.map +1 -1
- package/dist/tools/web-tool.d.ts.map +1 -1
- package/dist/tools/web-tool.js +11 -13
- package/dist/tools/web-tool.js.map +1 -1
- package/dist/tui/render-utils.d.ts +5 -1
- package/dist/tui/render-utils.d.ts.map +1 -1
- package/dist/tui/render-utils.js +36 -10
- package/dist/tui/render-utils.js.map +1 -1
- package/dist/tui/render.d.ts +9 -0
- package/dist/tui/render.d.ts.map +1 -1
- package/dist/tui/render.js +112 -100
- package/dist/tui/render.js.map +1 -1
- package/dist/tui/scramble/constants.d.ts +8 -8
- package/dist/tui/scramble/constants.d.ts.map +1 -1
- package/dist/tui/scramble/constants.js +7 -7
- package/dist/tui/scramble/constants.js.map +1 -1
- package/dist/tui/scramble/index.d.ts +1 -1
- package/dist/tui/scramble/index.d.ts.map +1 -1
- package/dist/tui/scramble/index.js +1 -1
- package/dist/tui/scramble/index.js.map +1 -1
- package/dist/tui/scramble/manager.d.ts +1 -5
- package/dist/tui/scramble/manager.d.ts.map +1 -1
- package/dist/tui/scramble/manager.js +16 -64
- package/dist/tui/scramble/manager.js.map +1 -1
- package/dist/tui/scramble/utils.js +1 -1
- package/dist/tui/scramble/utils.js.map +1 -1
- package/dist/types/flow.d.ts +2 -0
- package/dist/types/flow.d.ts.map +1 -1
- package/dist/types/flow.js +11 -2
- package/dist/types/flow.js.map +1 -1
- package/dist/types/output.d.ts +6 -0
- package/dist/types/output.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/core/delegation.d.ts.map +0 -1
- package/dist/core/delegation.js.map +0 -1
- package/dist/flow/warp-command.d.ts +0 -9
- package/dist/flow/warp-command.d.ts.map +0 -1
- package/dist/flow/warp-command.js +0 -405
- package/dist/flow/warp-command.js.map +0 -1
package/dist/batch/execute.js
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
import * as fs from "node:fs/promises";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { execFile } from "node:child_process";
|
|
10
|
-
import {
|
|
10
|
+
import { withFileMutationQueue } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { MAX_LINES, MAX_BYTES, SAFE_FULL_READ_LIMIT, TARGETED_READ_LINE_LIMIT, MAX_TOTAL_RESULT_LINES, BATCH_READ_MAX_TOTAL_BYTES, RG_SIGNATURES_MAX_FILES, } from "./constants.js";
|
|
11
12
|
import { normalizeToLF, restoreLineEndings, detectLineEnding, stripBom, applyEdits, levenshtein, validatePath, } from "./fuzzy-edit.js";
|
|
13
|
+
import { applyPatch } from "./apply-patch.js";
|
|
12
14
|
import { buildFileContextMap } from "./symbols.js";
|
|
13
15
|
// ---------------------------------------------------------------------------
|
|
14
16
|
// Read helpers
|
|
@@ -123,9 +125,6 @@ export async function suggestSimilarFiles(inputPath, cwd) {
|
|
|
123
125
|
return [];
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
// Error hints
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
128
|
function getErrorHint(error) {
|
|
130
129
|
if (error.includes("File not found") || error.includes("file not found"))
|
|
131
130
|
return "Verify the path exists.";
|
|
@@ -143,15 +142,27 @@ function getErrorHint(error) {
|
|
|
143
142
|
return "Verify the path exists.";
|
|
144
143
|
if (error.includes("is beyond end of file"))
|
|
145
144
|
return "Use a smaller offset within the file length.";
|
|
145
|
+
if (error.includes("ripgrep failed"))
|
|
146
|
+
return "Ripgrep crashed or was killed. Try narrowing the search path or adding max-count to limit output.";
|
|
146
147
|
return "";
|
|
147
148
|
}
|
|
149
|
+
function isRetryable(error) {
|
|
150
|
+
const transient = [
|
|
151
|
+
"File not found",
|
|
152
|
+
"file not found",
|
|
153
|
+
"ENOENT",
|
|
154
|
+
"no such file",
|
|
155
|
+
"Could not find",
|
|
156
|
+
"occurrences",
|
|
157
|
+
];
|
|
158
|
+
return transient.some((p) => error.includes(p));
|
|
159
|
+
}
|
|
148
160
|
// ---------------------------------------------------------------------------
|
|
149
161
|
// Main execute function
|
|
150
162
|
// ---------------------------------------------------------------------------
|
|
151
|
-
export async function executeOperations(operations, cwd, signal, options = {}) {
|
|
163
|
+
export async function executeOperations(operations, cwd, signal, options = {}, onUpdate) {
|
|
152
164
|
const results = [];
|
|
153
|
-
|
|
154
|
-
const counts = { read: 0, write: 0, edit: 0, delete: 0, rg: 0, error: 0, skipped: 0 };
|
|
165
|
+
const counts = { read: 0, write: 0, edit: 0, delete: 0, rg: 0, patch: 0, bash: 0, error: 0, skipped: 0 };
|
|
155
166
|
const errors = [];
|
|
156
167
|
const truncatedFiles = [];
|
|
157
168
|
const aggregateLimitSkipped = [];
|
|
@@ -159,38 +170,72 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
|
|
|
159
170
|
let aggregateLinesRead = 0;
|
|
160
171
|
let aggregateBytesRead = 0;
|
|
161
172
|
const includeLimitWarnings = options.includeLimitWarnings ?? true;
|
|
162
|
-
|
|
173
|
+
let lastUpdateTime = 0;
|
|
174
|
+
let finalUpdateEmitted = false;
|
|
175
|
+
function emitPartialUpdate() {
|
|
176
|
+
if (!onUpdate)
|
|
177
|
+
return;
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
const isFinal = results.length === operations.length;
|
|
180
|
+
if (!isFinal && now - lastUpdateTime < 100)
|
|
181
|
+
return;
|
|
182
|
+
if (isFinal && finalUpdateEmitted)
|
|
183
|
+
return;
|
|
184
|
+
finalUpdateEmitted = isFinal;
|
|
185
|
+
lastUpdateTime = now;
|
|
186
|
+
const partialSummary = buildSummary(counts, errors, truncatedFiles, aggregateLimitSkipped, aggregateByteLimitSkipped);
|
|
187
|
+
const partialContentText = buildContentText(partialSummary, results);
|
|
188
|
+
onUpdate({
|
|
189
|
+
content: [{ type: "text", text: partialContentText }],
|
|
190
|
+
details: { results: [...results] },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
for (let i = 0; i < operations.length; i++) {
|
|
194
|
+
const op = operations[i];
|
|
163
195
|
if (signal?.aborted) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
continue;
|
|
196
|
+
for (let j = i; j < operations.length; j++) {
|
|
197
|
+
const r = operations[j];
|
|
198
|
+
results.push({ op: r.o, path: r.p, status: "skipped", error: "Operation aborted.", s: r.s, l: r.l, q: r.q });
|
|
199
|
+
counts.skipped++;
|
|
200
|
+
}
|
|
201
|
+
emitPartialUpdate();
|
|
202
|
+
break;
|
|
172
203
|
}
|
|
173
204
|
try {
|
|
174
|
-
const resolvedPath = await validatePath(op.p, cwd);
|
|
205
|
+
const { path: resolvedPath, warning: pathWarning } = await validatePath(op.p, cwd);
|
|
175
206
|
switch (op.o) {
|
|
176
207
|
case "read": {
|
|
177
208
|
if (aggregateLinesRead >= MAX_TOTAL_RESULT_LINES) {
|
|
209
|
+
const remainingOps = operations.length - i - 1;
|
|
178
210
|
results.push({
|
|
179
211
|
op: "read",
|
|
180
212
|
path: op.p,
|
|
181
213
|
status: "skipped",
|
|
182
|
-
|
|
214
|
+
skipped: true,
|
|
215
|
+
reason: "aggregate_line_limit",
|
|
216
|
+
consumed: { lines: aggregateLinesRead, bytes: aggregateBytesRead },
|
|
217
|
+
remainingOps,
|
|
218
|
+
error: `Skipped: aggregate line limit of ${MAX_TOTAL_RESULT_LINES} reached (${aggregateLinesRead} lines consumed). ${remainingOps} remaining operation(s) will still execute. Use separate batch/batch_read calls.`,
|
|
219
|
+
s: op.s,
|
|
220
|
+
l: op.l,
|
|
183
221
|
});
|
|
184
222
|
counts.skipped++;
|
|
185
223
|
aggregateLimitSkipped.push({ path: op.p });
|
|
186
224
|
break;
|
|
187
225
|
}
|
|
188
226
|
if (aggregateBytesRead >= BATCH_READ_MAX_TOTAL_BYTES) {
|
|
227
|
+
const remainingOps = operations.length - i - 1;
|
|
189
228
|
results.push({
|
|
190
229
|
op: "read",
|
|
191
230
|
path: op.p,
|
|
192
231
|
status: "skipped",
|
|
193
|
-
|
|
232
|
+
skipped: true,
|
|
233
|
+
reason: "aggregate_byte_limit",
|
|
234
|
+
consumed: { lines: aggregateLinesRead, bytes: aggregateBytesRead },
|
|
235
|
+
remainingOps,
|
|
236
|
+
error: `Skipped: aggregate byte limit of ${BATCH_READ_MAX_TOTAL_BYTES} reached (${aggregateBytesRead} bytes consumed). ${remainingOps} remaining operation(s) will still execute. Use separate batch/batch_read calls.`,
|
|
237
|
+
s: op.s,
|
|
238
|
+
l: op.l,
|
|
194
239
|
});
|
|
195
240
|
counts.skipped++;
|
|
196
241
|
aggregateByteLimitSkipped.push({ path: op.p });
|
|
@@ -260,9 +305,11 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
|
|
|
260
305
|
status: "ok",
|
|
261
306
|
content: finalContent,
|
|
262
307
|
totalLines: totalFileLines,
|
|
263
|
-
warning: safetyWarning,
|
|
308
|
+
warning: [pathWarning, safetyWarning].filter(Boolean).join("\n") || undefined,
|
|
264
309
|
truncated: finalTruncated || undefined,
|
|
265
310
|
nextOffset,
|
|
311
|
+
s: op.s,
|
|
312
|
+
l: op.l,
|
|
266
313
|
});
|
|
267
314
|
counts.read++;
|
|
268
315
|
break;
|
|
@@ -271,13 +318,16 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
|
|
|
271
318
|
if (!op.c && op.c !== "") {
|
|
272
319
|
throw new Error("c (content) is required for write operations.");
|
|
273
320
|
}
|
|
274
|
-
await
|
|
275
|
-
|
|
321
|
+
await withFileMutationQueue(resolvedPath, async () => {
|
|
322
|
+
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
323
|
+
await fs.writeFile(resolvedPath, op.c, "utf-8");
|
|
324
|
+
});
|
|
276
325
|
results.push({
|
|
277
326
|
op: "write",
|
|
278
327
|
path: op.p,
|
|
279
328
|
status: "ok",
|
|
280
329
|
bytes: Buffer.byteLength(op.c, "utf-8"),
|
|
330
|
+
warning: pathWarning,
|
|
281
331
|
});
|
|
282
332
|
counts.write++;
|
|
283
333
|
break;
|
|
@@ -286,38 +336,45 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
|
|
|
286
336
|
if (!op.e || op.e.length === 0) {
|
|
287
337
|
throw new Error("e (edits) array is required for edit operations.");
|
|
288
338
|
}
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
339
|
+
const edits = op.e;
|
|
340
|
+
const blocksChanged = await withFileMutationQueue(resolvedPath, async () => {
|
|
341
|
+
const rawContent = await fs.readFile(resolvedPath, "utf-8");
|
|
342
|
+
const { bom, text: contentWithoutBom } = stripBom(rawContent);
|
|
343
|
+
const originalEnding = detectLineEnding(contentWithoutBom);
|
|
344
|
+
const normalizedContent = normalizeToLF(contentWithoutBom);
|
|
345
|
+
const { newContent, blocksChanged: changed } = applyEdits(normalizedContent, edits, op.p);
|
|
346
|
+
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
347
|
+
await fs.writeFile(resolvedPath, finalContent, "utf-8");
|
|
348
|
+
return changed;
|
|
349
|
+
});
|
|
296
350
|
results.push({
|
|
297
351
|
op: "edit",
|
|
298
352
|
path: op.p,
|
|
299
353
|
status: "ok",
|
|
300
354
|
blocksChanged,
|
|
355
|
+
warning: pathWarning,
|
|
301
356
|
});
|
|
302
357
|
counts.edit++;
|
|
303
358
|
break;
|
|
304
359
|
}
|
|
305
360
|
case "delete": {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
catch (err) {
|
|
311
|
-
if (err.code === "ENOENT") {
|
|
312
|
-
throw new Error(`File not found: ${op.p}`);
|
|
361
|
+
await withFileMutationQueue(resolvedPath, async () => {
|
|
362
|
+
let stat;
|
|
363
|
+
try {
|
|
364
|
+
stat = await fs.lstat(resolvedPath);
|
|
313
365
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
366
|
+
catch (err) {
|
|
367
|
+
if (err.code === "ENOENT") {
|
|
368
|
+
throw new Error(`File not found: ${op.p}`);
|
|
369
|
+
}
|
|
370
|
+
throw err;
|
|
371
|
+
}
|
|
372
|
+
if (stat.isDirectory()) {
|
|
373
|
+
throw new Error(`Cannot delete directory: ${op.p}. Use a recursive removal tool or delete files individually.`);
|
|
374
|
+
}
|
|
375
|
+
await fs.unlink(resolvedPath);
|
|
376
|
+
});
|
|
377
|
+
results.push({ op: "delete", path: op.p, status: "ok", warning: pathWarning });
|
|
321
378
|
counts.delete++;
|
|
322
379
|
break;
|
|
323
380
|
}
|
|
@@ -328,26 +385,61 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
|
|
|
328
385
|
}
|
|
329
386
|
const searchPath = (rgOp.p.startsWith("~") || path.isAbsolute(rgOp.p)) ? resolvedPath : rgOp.p;
|
|
330
387
|
const args = buildRgArgs({ ...rgOp, p: searchPath });
|
|
331
|
-
const matches = await execRg(args, cwd);
|
|
388
|
+
const matches = await execRg(args, cwd, signal);
|
|
332
389
|
const content = matches.join("\n");
|
|
390
|
+
// Try to attach enclosing signatures (only when we have line numbers)
|
|
391
|
+
let enclosingSignatures;
|
|
392
|
+
const uniqueFiles = extractUniqueFilesFromRg(matches);
|
|
393
|
+
if (uniqueFiles.size > 0 && uniqueFiles.size <= RG_SIGNATURES_MAX_FILES && !isFilesOnlyRg(matches)) {
|
|
394
|
+
enclosingSignatures = await buildEnclosingSignatures(uniqueFiles, matches, cwd);
|
|
395
|
+
}
|
|
333
396
|
results.push({
|
|
334
397
|
op: "rg",
|
|
335
398
|
path: rgOp.p,
|
|
336
399
|
status: "ok",
|
|
337
400
|
content,
|
|
338
401
|
totalLines: matches.length,
|
|
402
|
+
enclosingSignatures,
|
|
403
|
+
warning: pathWarning,
|
|
404
|
+
q: rgOp.q,
|
|
339
405
|
});
|
|
340
406
|
counts.rg++;
|
|
341
407
|
break;
|
|
342
408
|
}
|
|
409
|
+
case "patch": {
|
|
410
|
+
if (!op.c && op.c !== "") {
|
|
411
|
+
throw new Error("c (patch text) is required for patch operations.");
|
|
412
|
+
}
|
|
413
|
+
const { affected, exact } = await applyPatch(op.c, cwd);
|
|
414
|
+
results.push({
|
|
415
|
+
op: "patch",
|
|
416
|
+
path: op.p,
|
|
417
|
+
status: "ok",
|
|
418
|
+
affected,
|
|
419
|
+
exact,
|
|
420
|
+
warning: pathWarning,
|
|
421
|
+
});
|
|
422
|
+
counts.patch++;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
343
425
|
default:
|
|
344
426
|
throw new Error(`Unknown operation type: ${op.o}`);
|
|
345
427
|
}
|
|
346
428
|
}
|
|
347
429
|
catch (err) {
|
|
348
|
-
failed = true;
|
|
349
|
-
counts.error++;
|
|
350
430
|
const message = err instanceof Error ? err.message : String(err);
|
|
431
|
+
// Treat mid-flight rg abort as skipped rather than error
|
|
432
|
+
if (message === "Aborted" && signal?.aborted) {
|
|
433
|
+
counts.skipped++;
|
|
434
|
+
results.push({
|
|
435
|
+
op: op.o,
|
|
436
|
+
path: op.p,
|
|
437
|
+
status: "skipped",
|
|
438
|
+
error: "Operation aborted.",
|
|
439
|
+
});
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
counts.error++;
|
|
351
443
|
// Enrich file-not-found errors with fuzzy filename suggestions
|
|
352
444
|
let hint = getErrorHint(message);
|
|
353
445
|
if (message.includes("File not found") ||
|
|
@@ -359,6 +451,8 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
|
|
|
359
451
|
hint += ` Did you mean: ${suggestions.join(", ")}?`;
|
|
360
452
|
}
|
|
361
453
|
}
|
|
454
|
+
const retryable = isRetryable(message);
|
|
455
|
+
const suggestedFix = hint || undefined;
|
|
362
456
|
errors.push({ path: op.p, op: op.o, message, hint });
|
|
363
457
|
results.push({
|
|
364
458
|
op: op.o,
|
|
@@ -366,9 +460,16 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
|
|
|
366
460
|
status: "error",
|
|
367
461
|
error: message,
|
|
368
462
|
hint,
|
|
463
|
+
retryable,
|
|
464
|
+
suggestedFix,
|
|
465
|
+
s: op.s,
|
|
466
|
+
l: op.l,
|
|
467
|
+
q: op.q,
|
|
369
468
|
});
|
|
370
469
|
}
|
|
470
|
+
emitPartialUpdate();
|
|
371
471
|
}
|
|
472
|
+
emitPartialUpdate();
|
|
372
473
|
// Build the enhanced summary and content text
|
|
373
474
|
const summary = buildSummary(counts, errors, truncatedFiles, aggregateLimitSkipped, aggregateByteLimitSkipped);
|
|
374
475
|
const contentText = buildContentText(summary, results);
|
|
@@ -378,36 +479,70 @@ export async function executeOperations(operations, cwd, signal, options = {}) {
|
|
|
378
479
|
// Summary / content rendering
|
|
379
480
|
// ---------------------------------------------------------------------------
|
|
380
481
|
function buildSummary(counts, errors, truncatedFiles, aggregateLimitSkipped = [], aggregateByteLimitSkipped = []) {
|
|
381
|
-
const totalSuccess = counts.read + counts.write + counts.edit + counts.delete + counts.rg;
|
|
382
|
-
const totalOps = totalSuccess + counts.error + counts.skipped;
|
|
383
482
|
const parts = [];
|
|
384
|
-
// Build
|
|
483
|
+
// Build success parts from counts
|
|
385
484
|
const successParts = [];
|
|
386
485
|
if (counts.read > 0)
|
|
387
|
-
successParts.push(`${counts.read} read
|
|
486
|
+
successParts.push(`${counts.read} read`);
|
|
388
487
|
if (counts.write > 0)
|
|
389
|
-
successParts.push(`${counts.write} write
|
|
488
|
+
successParts.push(`${counts.write} write`);
|
|
390
489
|
if (counts.edit > 0)
|
|
391
|
-
successParts.push(`${counts.edit} edit
|
|
490
|
+
successParts.push(`${counts.edit} edit`);
|
|
392
491
|
if (counts.delete > 0)
|
|
393
|
-
successParts.push(`${counts.delete} delete
|
|
492
|
+
successParts.push(`${counts.delete} delete`);
|
|
394
493
|
if (counts.rg > 0)
|
|
395
|
-
successParts.push(`${counts.rg} rg
|
|
396
|
-
if (counts.
|
|
397
|
-
|
|
398
|
-
|
|
494
|
+
successParts.push(`${counts.rg} rg`);
|
|
495
|
+
if (counts.patch > 0)
|
|
496
|
+
successParts.push(`${counts.patch} patch`);
|
|
497
|
+
if (counts.bash > 0)
|
|
498
|
+
successParts.push(`${counts.bash} bash`);
|
|
499
|
+
// Build failure parts from errors
|
|
500
|
+
const failedCounts = {};
|
|
501
|
+
for (const err of errors) {
|
|
502
|
+
failedCounts[err.op] = (failedCounts[err.op] || 0) + 1;
|
|
503
|
+
}
|
|
504
|
+
const failedParts = [];
|
|
505
|
+
if (failedCounts.read > 0)
|
|
506
|
+
failedParts.push(`${failedCounts.read} read`);
|
|
507
|
+
if (failedCounts.write > 0)
|
|
508
|
+
failedParts.push(`${failedCounts.write} write`);
|
|
509
|
+
if (failedCounts.edit > 0)
|
|
510
|
+
failedParts.push(`${failedCounts.edit} edit`);
|
|
511
|
+
if (failedCounts.delete > 0)
|
|
512
|
+
failedParts.push(`${failedCounts.delete} delete`);
|
|
513
|
+
if (failedCounts.rg > 0)
|
|
514
|
+
failedParts.push(`${failedCounts.rg} rg`);
|
|
515
|
+
if (failedCounts.patch > 0)
|
|
516
|
+
failedParts.push(`${failedCounts.patch} patch`);
|
|
517
|
+
if (failedCounts.bash > 0)
|
|
518
|
+
failedParts.push(`${failedCounts.bash} bash`);
|
|
519
|
+
const hasSuccess = successParts.length > 0;
|
|
520
|
+
const hasFailure = failedParts.length > 0;
|
|
521
|
+
const hasSkipped = counts.skipped > 0;
|
|
522
|
+
if (!hasFailure) {
|
|
523
|
+
// All success (or skipped)
|
|
524
|
+
const summaryParts = [...successParts];
|
|
525
|
+
if (hasSkipped)
|
|
526
|
+
summaryParts.push(`${counts.skipped} skipped`);
|
|
527
|
+
parts.push(`✔ ${summaryParts.join(", ")}`);
|
|
399
528
|
}
|
|
400
529
|
else {
|
|
401
|
-
// Mixed
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
parts.push(` ${successParts.join(", ")} ok`);
|
|
530
|
+
// Mixed or all failed
|
|
531
|
+
if (hasSuccess) {
|
|
532
|
+
parts.push(`✔ ${successParts.join(", ")} | ✗ ${failedParts.join(", ")}`);
|
|
405
533
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const hintSuffix = hint ? ` — ${hint}` : "";
|
|
409
|
-
parts.push(` ${err.op} ${err.path}: ${err.message}${hintSuffix}`);
|
|
534
|
+
else {
|
|
535
|
+
parts.push(`✗ ${failedParts.join(", ")}`);
|
|
410
536
|
}
|
|
537
|
+
if (hasSkipped) {
|
|
538
|
+
parts.push(`${counts.skipped} skipped`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Error details
|
|
542
|
+
for (const err of errors) {
|
|
543
|
+
const hint = err.hint ?? "";
|
|
544
|
+
const hintSuffix = hint ? ` — ${hint}` : "";
|
|
545
|
+
parts.push(` ${err.op} ${err.path}: ${err.message}${hintSuffix}`);
|
|
411
546
|
}
|
|
412
547
|
// Truncation warnings
|
|
413
548
|
for (const tf of truncatedFiles) {
|
|
@@ -472,11 +607,30 @@ function buildContentText(summary, results) {
|
|
|
472
607
|
sections.push(`\n--- delete: ${r.path} ---`);
|
|
473
608
|
}
|
|
474
609
|
else if (r.op === "rg" && r.status === "ok") {
|
|
475
|
-
|
|
610
|
+
if (r.enclosingSignatures && Object.keys(r.enclosingSignatures).length > 0) {
|
|
611
|
+
const grouped = groupRgMatchesByFile(r.content ?? "", r.enclosingSignatures);
|
|
612
|
+
sections.push(`\n--- rg: ${r.path} ---\n${grouped}`);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
sections.push(`\n--- rg: ${r.path} ---\n${r.content}`);
|
|
616
|
+
}
|
|
476
617
|
}
|
|
477
618
|
else if (r.status === "error") {
|
|
478
619
|
sections.push(`\n--- ${r.op}: ${r.path} ---\nError: ${r.error}`);
|
|
479
620
|
}
|
|
621
|
+
else if (r.op === "patch" && r.status === "ok") {
|
|
622
|
+
const parts = [];
|
|
623
|
+
if (r.affected?.added.length)
|
|
624
|
+
parts.push(`A ${r.affected.added.join(', ')}`);
|
|
625
|
+
if (r.affected?.modified.length)
|
|
626
|
+
parts.push(`M ${r.affected.modified.join(', ')}`);
|
|
627
|
+
if (r.affected?.deleted.length)
|
|
628
|
+
parts.push(`D ${r.affected.deleted.join(', ')}`);
|
|
629
|
+
sections.push(`\n--- patch: ${r.path} ---\n${parts.join('\n')}`);
|
|
630
|
+
}
|
|
631
|
+
else if (r.status === "skipped") {
|
|
632
|
+
sections.push(`\n--- ${r.op}: ${r.path} ---\n${r.error ?? "Skipped"}`);
|
|
633
|
+
}
|
|
480
634
|
}
|
|
481
635
|
return sections.join("");
|
|
482
636
|
}
|
|
@@ -485,6 +639,7 @@ function buildContentText(summary, results) {
|
|
|
485
639
|
// ---------------------------------------------------------------------------
|
|
486
640
|
function buildRgArgs(op) {
|
|
487
641
|
const args = [];
|
|
642
|
+
args.push("-n");
|
|
488
643
|
if (op.l === true || op.l === undefined)
|
|
489
644
|
args.push("-l");
|
|
490
645
|
if (op.i === true)
|
|
@@ -502,16 +657,111 @@ function buildRgArgs(op) {
|
|
|
502
657
|
args.push(op.p);
|
|
503
658
|
return args;
|
|
504
659
|
}
|
|
505
|
-
function
|
|
660
|
+
function isFilesOnlyRg(matches) {
|
|
661
|
+
// When rg runs with -l, matches are just filenames with no line numbers
|
|
662
|
+
return matches.length > 0 && !matches.some((m) => m.includes(":"));
|
|
663
|
+
}
|
|
664
|
+
function extractUniqueFilesFromRg(matches) {
|
|
665
|
+
const files = new Set();
|
|
666
|
+
for (const m of matches) {
|
|
667
|
+
const colonIdx = m.indexOf(":");
|
|
668
|
+
if (colonIdx > 0)
|
|
669
|
+
files.add(m.substring(0, colonIdx));
|
|
670
|
+
}
|
|
671
|
+
return files;
|
|
672
|
+
}
|
|
673
|
+
function parseRgLineNumber(match) {
|
|
674
|
+
// format: path:line:content
|
|
675
|
+
const parts = match.split(":");
|
|
676
|
+
if (parts.length < 3)
|
|
677
|
+
return null;
|
|
678
|
+
const lineNum = parseInt(parts[1], 10);
|
|
679
|
+
return Number.isFinite(lineNum) ? lineNum : null;
|
|
680
|
+
}
|
|
681
|
+
function parseRgFilePath(match) {
|
|
682
|
+
const colonIdx = match.indexOf(":");
|
|
683
|
+
return colonIdx > 0 ? match.substring(0, colonIdx) : null;
|
|
684
|
+
}
|
|
685
|
+
async function buildEnclosingSignatures(files, matches, cwd) {
|
|
686
|
+
const sigMap = {};
|
|
687
|
+
for (const filePath of files) {
|
|
688
|
+
try {
|
|
689
|
+
const abs = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
690
|
+
const raw = await fs.readFile(abs, "utf-8");
|
|
691
|
+
const lines = raw.split("\n");
|
|
692
|
+
const ctxMap = buildFileContextMap(filePath, lines);
|
|
693
|
+
if (!ctxMap.symbols || ctxMap.symbols.length === 0)
|
|
694
|
+
continue;
|
|
695
|
+
// For each match line in this file, find enclosing symbol
|
|
696
|
+
for (const match of matches) {
|
|
697
|
+
const lineNum = parseRgLineNumber(match);
|
|
698
|
+
if (lineNum === null)
|
|
699
|
+
continue;
|
|
700
|
+
const matchPath = parseRgFilePath(match);
|
|
701
|
+
if (matchPath !== filePath)
|
|
702
|
+
continue;
|
|
703
|
+
const enclosing = ctxMap.symbols
|
|
704
|
+
.filter((s) => lineNum >= s.startLine && lineNum <= s.endLine)
|
|
705
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
706
|
+
if (enclosing?.signature) {
|
|
707
|
+
sigMap[match] = enclosing.signature;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
// File not readable, skip
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return sigMap;
|
|
716
|
+
}
|
|
717
|
+
function groupRgMatchesByFile(content, sigMap) {
|
|
718
|
+
// Group matches by file, deduplicate signatures per file
|
|
719
|
+
const fileGroups = new Map();
|
|
720
|
+
for (const match of content.split("\n").filter(Boolean)) {
|
|
721
|
+
const filePath = parseRgFilePath(match);
|
|
722
|
+
if (!filePath) {
|
|
723
|
+
// Fallback: keep bare match as-is
|
|
724
|
+
const fallbackKey = "";
|
|
725
|
+
const group = fileGroups.get(fallbackKey) ?? { sigs: new Set(), lines: [] };
|
|
726
|
+
group.lines.push(match);
|
|
727
|
+
fileGroups.set(fallbackKey, group);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const group = fileGroups.get(filePath) ?? { sigs: new Set(), lines: [] };
|
|
731
|
+
const sig = sigMap[match];
|
|
732
|
+
if (sig)
|
|
733
|
+
group.sigs.add(sig);
|
|
734
|
+
group.lines.push(match);
|
|
735
|
+
fileGroups.set(filePath, group);
|
|
736
|
+
}
|
|
737
|
+
const out = [];
|
|
738
|
+
for (const [filePath, { sigs, lines }] of fileGroups) {
|
|
739
|
+
if (!filePath) {
|
|
740
|
+
out.push(...lines);
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
out.push(filePath);
|
|
744
|
+
for (const sig of sigs) {
|
|
745
|
+
out.push(` ${sig}`);
|
|
746
|
+
}
|
|
747
|
+
for (const line of lines) {
|
|
748
|
+
const colonIdx = line.indexOf(":");
|
|
749
|
+
const afterPath = colonIdx > 0 ? line.substring(colonIdx + 1) : line;
|
|
750
|
+
out.push(` → ${afterPath}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return out.join("\n");
|
|
754
|
+
}
|
|
755
|
+
function execRg(args, cwd, signal) {
|
|
506
756
|
return new Promise((resolve, reject) => {
|
|
507
|
-
execFile("rg", args, { cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
757
|
+
const child = execFile("rg", args, { cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
508
758
|
if (err) {
|
|
509
759
|
// ripgrep exits with code 1 when no matches are found
|
|
510
760
|
if (err.code === 1) {
|
|
511
761
|
resolve([]);
|
|
512
762
|
return;
|
|
513
763
|
}
|
|
514
|
-
if (err.code === "ENOBUFS") {
|
|
764
|
+
if (err.code === "ENOBUFS" || err.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
|
|
515
765
|
reject(new Error("ripgrep output exceeded 10MB buffer limit. Use a more specific pattern or add max-count."));
|
|
516
766
|
return;
|
|
517
767
|
}
|
|
@@ -520,12 +770,30 @@ function execRg(args, cwd) {
|
|
|
520
770
|
return;
|
|
521
771
|
}
|
|
522
772
|
const stderrMsg = stderr?.trim() ? ` — ${stderr.trim()}` : "";
|
|
523
|
-
|
|
773
|
+
const codeInfo = err.code ? ` (code: ${err.code})` : "";
|
|
774
|
+
const msgInfo = err.message ? `: ${err.message}` : "";
|
|
775
|
+
reject(new Error(`ripgrep failed${codeInfo}${msgInfo}${stderrMsg}`));
|
|
524
776
|
return;
|
|
525
777
|
}
|
|
526
778
|
const lines = stdout.split("\n").filter((line) => line.length > 0);
|
|
527
779
|
resolve(lines);
|
|
528
780
|
});
|
|
781
|
+
if (signal) {
|
|
782
|
+
const onAbort = () => {
|
|
783
|
+
try {
|
|
784
|
+
child.kill("SIGTERM");
|
|
785
|
+
}
|
|
786
|
+
catch { /* already dead */ }
|
|
787
|
+
reject(new Error("Aborted"));
|
|
788
|
+
};
|
|
789
|
+
if (signal.aborted) {
|
|
790
|
+
onAbort();
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
794
|
+
child.on("close", () => signal.removeEventListener("abort", onAbort));
|
|
795
|
+
}
|
|
796
|
+
}
|
|
529
797
|
});
|
|
530
798
|
}
|
|
531
799
|
//# sourceMappingURL=execute.js.map
|