@oh-my-pi/pi-coding-agent 13.7.3 → 13.7.5

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 CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.7.4] - 2026-03-04
6
+ ### Added
7
+ - Added `fetch.useKagiSummarizer` setting to toggle Kagi Universal Summarizer usage in the fetch tool.
8
+
9
+ ### Fixed
10
+
11
+ - Fixed incorrect message history reference in session title generation that could cause missing or stale titles on first message
12
+ - Added startup check requiring Bun 1.3.7+ for JSONL session parsing (`Bun.JSONL.parseChunk`) and clear upgrade guidance so `/resume` and `--resume` do not silently report missing sessions on older Bun runtimes
13
+
5
14
  ## [13.7.3] - 2026-03-04
6
15
 
7
16
  ### Added
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.3",
4
+ "version": "13.7.5",
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.3",
45
- "@oh-my-pi/pi-agent-core": "13.7.3",
46
- "@oh-my-pi/pi-ai": "13.7.3",
47
- "@oh-my-pi/pi-natives": "13.7.3",
48
- "@oh-my-pi/pi-tui": "13.7.3",
49
- "@oh-my-pi/pi-utils": "13.7.3",
44
+ "@oh-my-pi/omp-stats": "13.7.5",
45
+ "@oh-my-pi/pi-agent-core": "13.7.5",
46
+ "@oh-my-pi/pi-ai": "13.7.5",
47
+ "@oh-my-pi/pi-natives": "13.7.5",
48
+ "@oh-my-pi/pi-tui": "13.7.5",
49
+ "@oh-my-pi/pi-utils": "13.7.5",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
package/src/cli.ts CHANGED
@@ -1,11 +1,39 @@
1
1
  #!/usr/bin/env bun
2
- import { APP_NAME, VERSION } from "@oh-my-pi/pi-utils";
2
+ import { APP_NAME, MIN_BUN_VERSION, VERSION } from "@oh-my-pi/pi-utils";
3
3
  /**
4
4
  * CLI entry point — registers all commands explicitly and delegates to the
5
5
  * lightweight CLI runner from pi-utils.
6
6
  */
7
7
  import { type CommandEntry, run } from "@oh-my-pi/pi-utils/cli";
8
8
 
9
+ function parseSemver(version: string): [number, number, number] {
10
+ function toint(value: string): number {
11
+ const int = Number.parseInt(value, 10);
12
+ if (Number.isNaN(int) || !Number.isFinite(int)) return 0;
13
+ return int;
14
+ }
15
+ const [majorRaw, minorRaw, patchRaw] = version.split(".").map(toint);
16
+ return [majorRaw, minorRaw, patchRaw];
17
+ }
18
+
19
+ function isAtLeastBunVersion(minimum: string): boolean {
20
+ const ver = parseSemver(Bun.version);
21
+ const min = parseSemver(minimum);
22
+ for (let i = 0; i < 3; i++) {
23
+ if (ver[i] !== min[i]) {
24
+ return ver[i] > min[i];
25
+ }
26
+ }
27
+ return true;
28
+ }
29
+
30
+ if (typeof Bun.JSONL?.parseChunk !== "function" || !isAtLeastBunVersion(MIN_BUN_VERSION)) {
31
+ process.stderr.write(
32
+ `error: Bun runtime must be >= ${MIN_BUN_VERSION} (found v${Bun.version}). Please update Bun: bun upgrade\n`,
33
+ );
34
+ process.exit(1);
35
+ }
36
+
9
37
  // Detect known Bun errata that cause TUI crashes (e.g. Bun.stringWidth mishandling OSC sequences).
10
38
  if (Bun.stringWidth("\x1b[0m\x1b]8;;\x07") !== 0) {
11
39
  process.stderr.write(`error: Bun runtime errata detected (v${Bun.version}). Please update Bun: bun upgrade\n`);
@@ -490,6 +490,15 @@ export const SETTINGS_SCHEMA = {
490
490
  default: true,
491
491
  ui: { tab: "tools", label: "Enable Fetch", description: "Enable the fetch tool for URL fetching" },
492
492
  },
493
+ "fetch.useKagiSummarizer": {
494
+ type: "boolean",
495
+ default: true,
496
+ ui: {
497
+ tab: "tools",
498
+ label: "Use Kagi in Fetch",
499
+ description: "Use Kagi Universal Summarizer when rendering HTML in fetch",
500
+ },
501
+ },
493
502
  "web_search.enabled": {
494
503
  type: "boolean",
495
504
  default: true,
@@ -300,7 +300,7 @@ export class InputController {
300
300
  this.ctx.flushPendingBashComponents();
301
301
 
302
302
  // Generate session title on first message
303
- const hasUserMessages = this.ctx.agent.state.messages.some((m: AgentMessage) => m.role === "user");
303
+ const hasUserMessages = this.ctx.session.messages.some((m: AgentMessage) => m.role === "user");
304
304
  if (!hasUserMessages && !this.ctx.sessionManager.getSessionName() && !$env.PI_NO_TITLE) {
305
305
  const registry = this.ctx.session.modelRegistry;
306
306
  const smolModel = this.ctx.settings.getModelRole("smol");
@@ -393,7 +393,7 @@ export class InputController {
393
393
  if (allQueued.length === 0) {
394
394
  this.ctx.updatePendingMessagesDisplay();
395
395
  if (options?.abort) {
396
- this.ctx.agent.abort();
396
+ this.ctx.session.abort();
397
397
  }
398
398
  return 0;
399
399
  }
@@ -403,7 +403,7 @@ export class InputController {
403
403
  this.ctx.editor.setText(combinedText);
404
404
  this.ctx.updatePendingMessagesDisplay();
405
405
  if (options?.abort) {
406
- this.ctx.agent.abort();
406
+ this.ctx.session.abort();
407
407
  }
408
408
  return allQueued.length;
409
409
  }
@@ -1,41 +1,40 @@
1
- Applies precise file edits using `LINE#ID` tags from `read` output.
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
- 1. You **SHOULD** issue a `read` call before editing if you have no tagged context for a file.
5
- 2. You **MUST** pick the smallest operation per change site.
6
- 3. You **MUST** submit one `edit` call per file with all operations, think your changes through before submitting.
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.[n].pos`** — the anchor line. Meaning depends on `op`:
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.[n].end`** — range replace only. The last line of the range (inclusive). Omit for single-line replace.
18
- **`edits.[n].lines`** — the replacement content:
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 should be referenced from the last `read` output.
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. **Minimize scope:** You **MUST** use one logical mutation per operation.
30
- 2. **Prefer insertion over neighbor rewrites:** You **SHOULD** anchor on structural boundaries (`}`, `]`, `},`), not interior lines.
31
- 3. **Range end tag (inclusive):** `end` is inclusive and **MUST** point to the final line being replaced.
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
- **Tag mismatch (`>>>`):** You **MUST** retry using fresh tags from the error snippet. If snippet lacks context, or if you repeatedly fail, you **MUST** re-read the file and issue less ambitious edits, i.e. single op.
38
- **No-op (`identical`):** You **MUST NOT** resubmit. Re-read target lines and adjust the edit.
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 never anchor on them when a unique line exists nearby.
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 fresh tool result as `N#ID`.
237
- - You **MUST** re-read after each edit call before issuing another on same file.
238
- - Formatting is a batch operation. You **MUST NOT** use this tool to reformat, reindent, or adjust whitespace — run the project's formatter instead. If the only change is whitespace, it is formatting; do not touch it.
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) you **MUST NOT** use `\\t` (two characters: backslash + t), which produces the literal string `\t` in the file.
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>
@@ -429,6 +429,7 @@ async function renderHtmlToText(
429
429
  url: string,
430
430
  html: string,
431
431
  timeout: number,
432
+ useKagiSummarizer: boolean,
432
433
  userSignal?: AbortSignal,
433
434
  ): Promise<{ content: string; ok: boolean; method: string }> {
434
435
  const signal = ptree.combineSignals(userSignal, timeout * 1000);
@@ -440,15 +441,17 @@ async function renderHtmlToText(
440
441
  signal,
441
442
  };
442
443
 
443
- // Try Kagi Universal Summarizer first (if KAGI_API_KEY is configured)
444
- try {
445
- const kagiSummary = await summarizeUrlWithKagi(url, { signal });
446
- if (kagiSummary && kagiSummary.length > 100 && !isLowQualityOutput(kagiSummary)) {
447
- return { content: kagiSummary, ok: true, method: "kagi" };
444
+ // Try Kagi Universal Summarizer first (if enabled and KAGI_API_KEY is configured)
445
+ if (useKagiSummarizer) {
446
+ try {
447
+ const kagiSummary = await summarizeUrlWithKagi(url, { signal });
448
+ if (kagiSummary && kagiSummary.length > 100 && !isLowQualityOutput(kagiSummary)) {
449
+ return { content: kagiSummary, ok: true, method: "kagi" };
450
+ }
451
+ } catch {
452
+ // Kagi failed, continue to next method
453
+ signal?.throwIfAborted();
448
454
  }
449
- } catch {
450
- // Kagi failed, continue to next method
451
- signal?.throwIfAborted();
452
455
  }
453
456
 
454
457
  // Try jina next (reader API)
@@ -564,7 +567,13 @@ async function handleSpecialUrls(url: string, timeout: number, signal?: AbortSig
564
567
  /**
565
568
  * Main render function implementing the full pipeline
566
569
  */
567
- async function renderUrl(url: string, timeout: number, raw: boolean, signal?: AbortSignal): Promise<RenderResult> {
570
+ async function renderUrl(
571
+ url: string,
572
+ timeout: number,
573
+ raw: boolean,
574
+ useKagiSummarizer: boolean,
575
+ signal?: AbortSignal,
576
+ ): Promise<RenderResult> {
568
577
  const notes: string[] = [];
569
578
  const fetchedAt = new Date().toISOString();
570
579
  if (signal?.aborted) {
@@ -803,7 +812,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean, signal?: Ab
803
812
  }
804
813
 
805
814
  // Step 6: Render HTML with lynx or html2text
806
- const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, signal);
815
+ const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, useKagiSummarizer, signal);
807
816
  if (!htmlResult.ok) {
808
817
  notes.push("html rendering failed (lynx/html2text unavailable)");
809
818
  const output = finalizeOutput(rawContent);
@@ -926,7 +935,8 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
926
935
  throw new ToolAbortError();
927
936
  }
928
937
 
929
- const result = await renderUrl(url, effectiveTimeout, raw, signal);
938
+ const useKagiSummarizer = this.session.settings.get("fetch.useKagiSummarizer");
939
+ const result = await renderUrl(url, effectiveTimeout, raw, useKagiSummarizer, signal);
930
940
  const truncation = truncateHead(result.content, {
931
941
  maxBytes: DEFAULT_MAX_BYTES,
932
942
  maxLines: FETCH_DEFAULT_MAX_LINES,