@oh-my-pi/pi-coding-agent 13.7.4 → 13.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/package.json +7 -7
- package/src/prompts/tools/hashline.md +21 -22
- package/src/tools/ast-edit.ts +9 -6
- package/src/tools/ast-grep.ts +11 -5
- package/src/tools/render-utils.ts +18 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.7.6] - 2026-03-04
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Exported `dedupeParseErrors` utility function to deduplicate parse error messages while preserving order
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Reduced duplicate parse error messages when multiple patterns fail on the same file
|
|
13
|
+
- Normalized parse error output in ast-grep to remove pattern-specific prefixes and show only file-level errors
|
|
14
|
+
|
|
5
15
|
## [13.7.4] - 2026-03-04
|
|
6
16
|
### Added
|
|
7
17
|
- Added `fetch.useKagiSummarizer` setting to toggle Kagi Universal Summarizer usage in the fetch tool.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.7.
|
|
4
|
+
"version": "13.7.6",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.7.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.7.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.7.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.7.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.7.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.7.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.7.6",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.7.6",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.7.6",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.7.6",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.7.6",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.7.6",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -1,41 +1,40 @@
|
|
|
1
|
-
Applies precise file edits
|
|
1
|
+
Applies precise, surgical file edits by referencing `LINE#ID` tags from `read` output. Each tag uniquely identifies a line, so edits remain stable even when lines shift.
|
|
2
2
|
|
|
3
3
|
<workflow>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Follow these steps in order for every edit:
|
|
5
|
+
1. You **SHOULD** issue a `read` call before editing to get fresh `LINE#ID` tags. Editing without current tags causes mismatches because other edits or external changes may have shifted line numbers since your last read.
|
|
6
|
+
2. You **MUST** submit one `edit` call per file with all operations. Multiple calls to the same file require re-reading between each one (tags shift after each edit), so batching avoids wasted round-trips. Think your changes through before submitting.
|
|
7
|
+
3. You **MUST** pick the smallest operation per change site. Each operation should be one logical mutation — a single replace, insert, or delete. Combining unrelated changes into one operation makes errors harder to diagnose and recover from.
|
|
7
8
|
</workflow>
|
|
8
9
|
|
|
9
10
|
<operations>
|
|
10
11
|
**`path`** — the path to the file to edit.
|
|
11
12
|
**`move`** — if set, move the file to the given path.
|
|
12
13
|
**`delete`** — if true, delete the file.
|
|
13
|
-
**`edits
|
|
14
|
+
**`edits[n].pos`** — the anchor line. Meaning depends on `op`:
|
|
14
15
|
- `replace`: start of range (or the single line to replace)
|
|
15
16
|
- `prepend`: insert new lines **before** this line; omit for beginning of file
|
|
16
17
|
- `append`: insert new lines **after** this line; omit for end of file
|
|
17
|
-
**`edits
|
|
18
|
-
**`edits
|
|
18
|
+
**`edits[n].end`** — range replace only. The last line of the range (inclusive). Omit for single-line replace.
|
|
19
|
+
**`edits[n].lines`** — the replacement content:
|
|
19
20
|
- `["line1", "line2"]` — replace with these lines (array of strings)
|
|
20
21
|
- `"line1"` — shorthand for `["line1"]` (single-line replace)
|
|
21
22
|
- `[""]` — replace content with a blank line (line preserved, content cleared)
|
|
22
23
|
- `null` or `[]` — **delete** the line(s) entirely
|
|
23
24
|
|
|
24
|
-
Tags
|
|
25
|
-
Edits are applied bottom-up, so earlier tags stay valid even when later ops add or remove lines.
|
|
25
|
+
Tags are applied bottom-up: later edits (by position) are applied first, so earlier tags remain valid even when subsequent ops add or remove lines. Tags **MUST** be referenced from the most recent `read` output.
|
|
26
26
|
</operations>
|
|
27
27
|
|
|
28
28
|
<rules>
|
|
29
|
-
1. **
|
|
30
|
-
2. **Prefer insertion over neighbor rewrites
|
|
31
|
-
3. **
|
|
32
|
-
- If `lines` includes a closing boundary token (`}`, `]`, `)`, `);`, `},`), `end` **MUST** include the original boundary line.
|
|
33
|
-
- You **MUST NOT** set `end` to an interior line and then re-add the boundary token in `lines`; that duplicates the next surviving line.
|
|
29
|
+
1. **Anchor on unique, structural lines.** You **SHOULD** choose anchors like function signatures, class declarations, or distinct statements — lines that appear exactly once. Blank lines, `}`, and `return null;` repeat throughout a file; anchoring on them risks matching the wrong location. When inserting between blocks, anchor on the nearest unique declaration using `prepend` or `append`.
|
|
30
|
+
2. **Prefer insertion over neighbor rewrites.** You **SHOULD** use `prepend`/`append` anchored on a structural boundary (`}`, `]`, `},`) rather than replacing adjacent lines when adding code near existing code. This keeps the edit minimal and avoids accidentally rewriting lines that should stay.
|
|
31
|
+
3. **Include boundary lines in the replaced range.** `end` is inclusive and **MUST** point to the final line being replaced. If your replacement `lines` include a closing token (`}`, `]`, `)`, `);`, `},`), `end` **MUST** include the original closing line. Otherwise the original closer survives and you get a duplicate.
|
|
34
32
|
</rules>
|
|
35
33
|
|
|
36
34
|
<recovery>
|
|
37
|
-
|
|
38
|
-
**
|
|
35
|
+
Edits can fail in two ways. Here is exactly what to do for each:
|
|
36
|
+
1. **Tag mismatch (`>>>`):** The file changed since your last read, so the tag no longer matches. You **MUST** retry using the fresh tags from the error snippet. If the snippet lacks enough context, or if you fail repeatedly, you **MUST** re-read the entire file and submit a simpler, single-op edit.
|
|
37
|
+
2. **No-op (`identical`):** Your replacement is identical to the existing content — nothing changed. You **MUST NOT** resubmit the same edit. Re-read the target lines to understand what is actually there, then adjust your edit.
|
|
39
38
|
</recovery>
|
|
40
39
|
|
|
41
40
|
<example name="single-line replace">
|
|
@@ -121,6 +120,7 @@ Range — add `end`:
|
|
|
121
120
|
</example>
|
|
122
121
|
|
|
123
122
|
<example name="inclusive end avoids duplicate boundary">
|
|
123
|
+
This example demonstrates why `end` must include the original closing line when your replacement also contains that closer.
|
|
124
124
|
```ts
|
|
125
125
|
{{hlinefull 70 "if (ok) {"}}
|
|
126
126
|
{{hlinefull 71 " run();"}}
|
|
@@ -159,7 +159,6 @@ Good — include original `}` in the replaced range when replacement keeps `}`:
|
|
|
159
159
|
}]
|
|
160
160
|
}
|
|
161
161
|
```
|
|
162
|
-
Also apply the same rule to `);`, `],`, and `},` closers: if replacement includes the closer token, `end` must include the original closer line.
|
|
163
162
|
</example>
|
|
164
163
|
|
|
165
164
|
<example name="insert between sibling declarations">
|
|
@@ -191,7 +190,7 @@ Use a trailing `""` to preserve the blank line between top-level sibling declara
|
|
|
191
190
|
</example>
|
|
192
191
|
|
|
193
192
|
<example name="disambiguate anchors">
|
|
194
|
-
Blank lines and repeated patterns (`}`, `return null;`) appear many times
|
|
193
|
+
Blank lines and repeated patterns (`}`, `return null;`) appear many times. Always anchor on a unique line nearby instead.
|
|
195
194
|
```ts
|
|
196
195
|
{{hlinefull 101 "}"}}
|
|
197
196
|
{{hlinefull 102 ""}}
|
|
@@ -233,8 +232,8 @@ Good — anchor on the unique declaration line:
|
|
|
233
232
|
|
|
234
233
|
<critical>
|
|
235
234
|
- Edit payload: `{ path, edits[] }`. Each entry: `op`, `lines`, optional `pos`/`end`. No extra keys.
|
|
236
|
-
- Every tag **MUST** be copied exactly from
|
|
237
|
-
- You **MUST** re-read after each edit call before issuing another on same file.
|
|
238
|
-
-
|
|
239
|
-
- `lines` entries **MUST** be literal file content with indentation copied exactly from the `read` output. If the file uses tabs, use `\t` in JSON (a real tab character)
|
|
235
|
+
- Every tag **MUST** be copied exactly from your most recent `read` output as `N#ID`. Stale or mistyped tags cause mismatches.
|
|
236
|
+
- You **MUST** re-read the file after each edit call before issuing another on the same file. Tags shift after every edit, so reusing old tags produces mismatches.
|
|
237
|
+
- You **MUST NOT** use this tool to reformat, reindent, or adjust whitespace — run the project's formatter instead. If the only difference is whitespace, it is formatting; leave it alone.
|
|
238
|
+
- `lines` entries **MUST** be literal file content with indentation copied exactly from the `read` output. If the file uses tabs, use `\t` in JSON (a real tab character). Using `\\t` (backslash + t) writes the literal two-character string `\t` into the file.
|
|
240
239
|
</critical>
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { ToolSession } from ".";
|
|
|
16
16
|
import type { OutputMeta } from "./output-meta";
|
|
17
17
|
import { hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
|
|
18
18
|
import {
|
|
19
|
+
dedupeParseErrors,
|
|
19
20
|
formatCount,
|
|
20
21
|
formatEmptyMessage,
|
|
21
22
|
formatErrorMessage,
|
|
@@ -140,6 +141,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
140
141
|
signal,
|
|
141
142
|
});
|
|
142
143
|
|
|
144
|
+
const dedupedParseErrors = dedupeParseErrors(result.parseErrors);
|
|
143
145
|
const formatPath = (filePath: string): string => {
|
|
144
146
|
const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
|
145
147
|
if (isDirectory) {
|
|
@@ -178,15 +180,15 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
178
180
|
filesSearched: result.filesSearched,
|
|
179
181
|
applied: result.applied,
|
|
180
182
|
limitReached: result.limitReached,
|
|
181
|
-
parseErrors:
|
|
183
|
+
parseErrors: dedupedParseErrors,
|
|
182
184
|
scopePath,
|
|
183
185
|
files: fileList,
|
|
184
186
|
fileReplacements: [],
|
|
185
187
|
};
|
|
186
188
|
|
|
187
189
|
if (result.totalReplacements === 0) {
|
|
188
|
-
const parseMessage =
|
|
189
|
-
? `\n${formatParseErrors(
|
|
190
|
+
const parseMessage = dedupedParseErrors.length
|
|
191
|
+
? `\n${formatParseErrors(dedupedParseErrors).join("\n")}`
|
|
190
192
|
: "";
|
|
191
193
|
return toolResult(baseDetails).text(`No replacements made${parseMessage}`).done();
|
|
192
194
|
}
|
|
@@ -258,8 +260,8 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
258
260
|
if (result.limitReached) {
|
|
259
261
|
outputLines.push("", "Limit reached; narrow path or increase limit.");
|
|
260
262
|
}
|
|
261
|
-
if (
|
|
262
|
-
outputLines.push("", ...formatParseErrors(
|
|
263
|
+
if (dedupedParseErrors.length) {
|
|
264
|
+
outputLines.push("", ...formatParseErrors(dedupedParseErrors));
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
// Register pending action so `resolve` can apply or discard these previewed changes
|
|
@@ -281,13 +283,14 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
281
283
|
maxFiles,
|
|
282
284
|
failOnParseError: false,
|
|
283
285
|
});
|
|
286
|
+
const dedupedApplyParseErrors = dedupeParseErrors(applyResult.parseErrors);
|
|
284
287
|
const appliedDetails: AstEditToolDetails = {
|
|
285
288
|
totalReplacements: applyResult.totalReplacements,
|
|
286
289
|
filesTouched: applyResult.filesTouched,
|
|
287
290
|
filesSearched: applyResult.filesSearched,
|
|
288
291
|
applied: applyResult.applied,
|
|
289
292
|
limitReached: applyResult.limitReached,
|
|
290
|
-
parseErrors:
|
|
293
|
+
parseErrors: dedupedApplyParseErrors,
|
|
291
294
|
scopePath,
|
|
292
295
|
files: fileList,
|
|
293
296
|
fileReplacements,
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { ToolSession } from ".";
|
|
|
16
16
|
import type { OutputMeta } from "./output-meta";
|
|
17
17
|
import { hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
|
|
18
18
|
import {
|
|
19
|
+
dedupeParseErrors,
|
|
19
20
|
formatCount,
|
|
20
21
|
formatEmptyMessage,
|
|
21
22
|
formatErrorMessage,
|
|
@@ -133,6 +134,11 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
133
134
|
signal,
|
|
134
135
|
});
|
|
135
136
|
|
|
137
|
+
const normalizedParseErrors = (result.parseErrors ?? []).map(error => {
|
|
138
|
+
const parseError = error.match(/^.+: (.+: parse error \(syntax tree contains error nodes\))$/);
|
|
139
|
+
return parseError?.[1] ?? error;
|
|
140
|
+
});
|
|
141
|
+
const dedupedParseErrors = dedupeParseErrors(normalizedParseErrors);
|
|
136
142
|
const formatPath = (filePath: string): string => {
|
|
137
143
|
const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
|
138
144
|
if (isDirectory) {
|
|
@@ -165,15 +171,15 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
165
171
|
fileCount: result.filesWithMatches,
|
|
166
172
|
filesSearched: result.filesSearched,
|
|
167
173
|
limitReached: result.limitReached,
|
|
168
|
-
parseErrors:
|
|
174
|
+
parseErrors: dedupedParseErrors,
|
|
169
175
|
scopePath,
|
|
170
176
|
files: fileList,
|
|
171
177
|
fileMatches: [],
|
|
172
178
|
};
|
|
173
179
|
|
|
174
180
|
if (result.matches.length === 0) {
|
|
175
|
-
const parseMessage =
|
|
176
|
-
? `\n${formatParseErrors(
|
|
181
|
+
const parseMessage = dedupedParseErrors.length
|
|
182
|
+
? `\n${formatParseErrors(dedupedParseErrors).join("\n")}`
|
|
177
183
|
: "";
|
|
178
184
|
return toolResult(baseDetails).text(`No matches found${parseMessage}`).done();
|
|
179
185
|
}
|
|
@@ -253,8 +259,8 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
253
259
|
if (result.limitReached) {
|
|
254
260
|
outputLines.push("", "Result limit reached; narrow path pattern or increase limit.");
|
|
255
261
|
}
|
|
256
|
-
if (
|
|
257
|
-
outputLines.push("", ...formatParseErrors(
|
|
262
|
+
if (dedupedParseErrors.length) {
|
|
263
|
+
outputLines.push("", ...formatParseErrors(dedupedParseErrors));
|
|
258
264
|
}
|
|
259
265
|
|
|
260
266
|
return toolResult(details).text(outputLines.join("\n")).done();
|
|
@@ -533,10 +533,25 @@ export function wrapBrackets(text: string, theme: Theme): string {
|
|
|
533
533
|
|
|
534
534
|
export const PARSE_ERRORS_LIMIT = 20;
|
|
535
535
|
|
|
536
|
+
export function dedupeParseErrors(errors: string[] | undefined): string[] {
|
|
537
|
+
if (!errors || errors.length === 0) return [];
|
|
538
|
+
const seen = new Set<string>();
|
|
539
|
+
const deduped: string[] = [];
|
|
540
|
+
for (const error of errors) {
|
|
541
|
+
if (seen.has(error)) continue;
|
|
542
|
+
seen.add(error);
|
|
543
|
+
deduped.push(error);
|
|
544
|
+
}
|
|
545
|
+
return deduped;
|
|
546
|
+
}
|
|
547
|
+
|
|
536
548
|
export function formatParseErrors(errors: string[]): string[] {
|
|
537
|
-
|
|
538
|
-
|
|
549
|
+
const deduped = dedupeParseErrors(errors);
|
|
550
|
+
if (deduped.length === 0) return [];
|
|
551
|
+
const capped = deduped.slice(0, PARSE_ERRORS_LIMIT);
|
|
539
552
|
const header =
|
|
540
|
-
|
|
553
|
+
deduped.length > PARSE_ERRORS_LIMIT
|
|
554
|
+
? `Parse issues (${PARSE_ERRORS_LIMIT} / ${deduped.length}):`
|
|
555
|
+
: "Parse issues:";
|
|
541
556
|
return [header, ...capped.map(err => `- ${err}`)];
|
|
542
557
|
}
|