@oh-my-pi/pi-coding-agent 15.5.3 → 15.5.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/types/config/settings-schema.d.ts +27 -0
  3. package/dist/types/config.d.ts +31 -5
  4. package/dist/types/edit/file-snapshot-store.d.ts +18 -0
  5. package/dist/types/edit/hashline/diff.d.ts +35 -0
  6. package/dist/types/edit/hashline/execute.d.ts +28 -0
  7. package/dist/types/edit/hashline/filesystem.d.ts +57 -0
  8. package/dist/types/edit/hashline/index.d.ts +4 -0
  9. package/dist/types/edit/hashline/params.d.ts +11 -0
  10. package/dist/types/edit/index.d.ts +4 -3
  11. package/dist/types/edit/normalize.d.ts +4 -16
  12. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +23 -0
  13. package/dist/types/index.d.ts +0 -1
  14. package/dist/types/tools/fetch.d.ts +3 -0
  15. package/dist/types/tools/find.d.ts +7 -0
  16. package/dist/types/tools/index.d.ts +6 -5
  17. package/dist/types/tools/path-utils.d.ts +18 -0
  18. package/dist/types/utils/changelog.d.ts +8 -3
  19. package/package.json +8 -15
  20. package/scripts/build-binary.ts +11 -0
  21. package/src/config/settings-schema.ts +32 -0
  22. package/src/config.ts +42 -15
  23. package/src/edit/diff.ts +5 -3
  24. package/src/edit/file-snapshot-store.ts +22 -0
  25. package/src/edit/hashline/diff.ts +95 -0
  26. package/src/edit/hashline/execute.ts +181 -0
  27. package/src/edit/hashline/filesystem.ts +129 -0
  28. package/src/edit/hashline/index.ts +4 -0
  29. package/src/edit/hashline/params.ts +18 -0
  30. package/src/edit/index.ts +16 -27
  31. package/src/edit/normalize.ts +11 -41
  32. package/src/edit/renderer.ts +15 -8
  33. package/src/edit/streaming.ts +20 -134
  34. package/src/extensibility/legacy-pi-ai-shim.ts +24 -0
  35. package/src/extensibility/plugins/legacy-pi-compat.ts +47 -3
  36. package/src/index.ts +0 -1
  37. package/src/internal-urls/docs-index.generated.ts +1 -1
  38. package/src/main.ts +2 -1
  39. package/src/modes/rpc/rpc-client.ts +3 -1
  40. package/src/prompts/tools/find.md +3 -2
  41. package/src/sdk.ts +8 -1
  42. package/src/session/agent-session.ts +18 -2
  43. package/src/tools/ast-edit.ts +1 -1
  44. package/src/tools/ast-grep.ts +3 -3
  45. package/src/tools/fetch.ts +93 -50
  46. package/src/tools/find.ts +38 -6
  47. package/src/tools/index.ts +6 -5
  48. package/src/tools/path-utils.ts +81 -0
  49. package/src/tools/read.ts +71 -75
  50. package/src/tools/search.ts +136 -17
  51. package/src/tools/write.ts +3 -3
  52. package/src/utils/changelog.ts +11 -3
  53. package/src/utils/file-mentions.ts +1 -1
  54. package/dist/types/edit/file-read-cache.d.ts +0 -36
  55. package/dist/types/hashline/anchors.d.ts +0 -26
  56. package/dist/types/hashline/apply.d.ts +0 -14
  57. package/dist/types/hashline/constants.d.ts +0 -48
  58. package/dist/types/hashline/diff-preview.d.ts +0 -2
  59. package/dist/types/hashline/diff.d.ts +0 -16
  60. package/dist/types/hashline/execute.d.ts +0 -4
  61. package/dist/types/hashline/executor.d.ts +0 -56
  62. package/dist/types/hashline/hash.d.ts +0 -76
  63. package/dist/types/hashline/index.d.ts +0 -14
  64. package/dist/types/hashline/input.d.ts +0 -4
  65. package/dist/types/hashline/prefixes.d.ts +0 -7
  66. package/dist/types/hashline/recovery.d.ts +0 -21
  67. package/dist/types/hashline/stream.d.ts +0 -2
  68. package/dist/types/hashline/tokenizer.d.ts +0 -94
  69. package/dist/types/hashline/types.d.ts +0 -75
  70. package/src/edit/file-read-cache.ts +0 -138
  71. package/src/hashline/anchors.ts +0 -104
  72. package/src/hashline/apply.ts +0 -790
  73. package/src/hashline/bigrams.json +0 -649
  74. package/src/hashline/constants.ts +0 -60
  75. package/src/hashline/diff-preview.ts +0 -42
  76. package/src/hashline/diff.ts +0 -82
  77. package/src/hashline/execute.ts +0 -334
  78. package/src/hashline/executor.ts +0 -347
  79. package/src/hashline/grammar.lark +0 -22
  80. package/src/hashline/hash.ts +0 -131
  81. package/src/hashline/index.ts +0 -14
  82. package/src/hashline/input.ts +0 -137
  83. package/src/hashline/prefixes.ts +0 -111
  84. package/src/hashline/recovery.ts +0 -139
  85. package/src/hashline/stream.ts +0 -123
  86. package/src/hashline/tokenizer.ts +0 -473
  87. package/src/hashline/types.ts +0 -66
  88. package/src/prompts/tools/hashline.md +0 -83
@@ -1,51 +1,21 @@
1
1
  /**
2
2
  * Text normalization utilities for the edit tool.
3
3
  *
4
- * Handles line endings, BOM, whitespace, and Unicode normalization.
4
+ * Whitespace, Unicode, and indentation helpers. Line-ending and BOM
5
+ * primitives live in `@oh-my-pi/hashline` and are re-exported here so
6
+ * existing consumers see one stable surface.
5
7
  */
6
8
 
7
9
  import { padding } from "@oh-my-pi/pi-tui";
8
10
 
9
- // ═══════════════════════════════════════════════════════════════════════════
10
- // Line Ending Utilities
11
- // ═══════════════════════════════════════════════════════════════════════════
12
-
13
- export type LineEnding = "\r\n" | "\n";
14
-
15
- /** Detect the predominant line ending in content */
16
- export function detectLineEnding(content: string): LineEnding {
17
- const crlfIdx = content.indexOf("\r\n");
18
- const lfIdx = content.indexOf("\n");
19
- if (lfIdx === -1) return "\n";
20
- if (crlfIdx === -1) return "\n";
21
- return crlfIdx < lfIdx ? "\r\n" : "\n";
22
- }
23
-
24
- /** Normalize all line endings to LF */
25
- export function normalizeToLF(text: string): string {
26
- return text.replace(/\r\n?/g, "\n");
27
- }
28
-
29
- /** Restore line endings to the specified type */
30
- export function restoreLineEndings(text: string, ending: LineEnding): string {
31
- return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
32
- }
33
-
34
- // ═══════════════════════════════════════════════════════════════════════════
35
- // BOM Handling
36
- // ═══════════════════════════════════════════════════════════════════════════
37
-
38
- export interface BomResult {
39
- /** The BOM character if present, empty string otherwise */
40
- bom: string;
41
- /** The text without the BOM */
42
- text: string;
43
- }
44
-
45
- /** Strip UTF-8 BOM if present */
46
- export function stripBom(content: string): BomResult {
47
- return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
48
- }
11
+ export {
12
+ type BomResult,
13
+ detectLineEnding,
14
+ type LineEnding,
15
+ normalizeToLF,
16
+ restoreLineEndings,
17
+ stripBom,
18
+ } from "@oh-my-pi/hashline";
49
19
 
50
20
  // ═══════════════════════════════════════════════════════════════════════════
51
21
  // Whitespace Utilities
@@ -2,11 +2,11 @@
2
2
  * Edit tool renderer and LSP batching helpers.
3
3
  */
4
4
 
5
+ import { HL_FILE_PREFIX } from "@oh-my-pi/hashline";
5
6
  import type { Component } from "@oh-my-pi/pi-tui";
6
7
  import { Text, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
7
8
  import { sanitizeText } from "@oh-my-pi/pi-utils";
8
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
- import { HL_FILE_PREFIX } from "../hashline/hash";
10
10
  import type { FileDiagnosticsResult } from "../lsp";
11
11
  import { renderDiff as renderDiffColored } from "../modes/components/diff";
12
12
  import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
@@ -235,14 +235,21 @@ function renderPlainTextPreview(text: string, uiTheme: Theme, filePath?: string)
235
235
 
236
236
  function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, label = "streaming"): string {
237
237
  if (!diff) return "";
238
- const lines = diff.split("\n");
239
- const total = lines.length;
240
- const displayLines = lines.slice(-EDIT_STREAMING_PREVIEW_LINES);
241
- const hidden = total - displayLines.length;
238
+ // Hunk-aware truncation keeps the change rows themselves visible and
239
+ // trims surrounding context proportionally so a multi-hunk diff doesn't
240
+ // turn into just the tail of the last hunk while streaming.
241
+ const {
242
+ text: truncatedDiff,
243
+ hiddenHunks,
244
+ hiddenLines,
245
+ } = truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, EDIT_STREAMING_PREVIEW_LINES);
242
246
  let text = "\n\n";
243
- text += renderDiffColored(displayLines.join("\n"), { filePath: rawPath });
244
- if (hidden > 0) {
245
- text += uiTheme.fg("dim", `\n… (${label} +${hidden} lines)`);
247
+ text += renderDiffColored(truncatedDiff, { filePath: rawPath });
248
+ if (hiddenHunks > 0 || hiddenLines > 0) {
249
+ const remainder: string[] = [];
250
+ if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
251
+ if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
252
+ text += uiTheme.fg("dim", `\n… (${label} +${remainder.join(", ")})`);
246
253
  } else {
247
254
  text += uiTheme.fg("dim", `\n(${label})`);
248
255
  }
@@ -13,22 +13,18 @@
13
13
  * the injected `editMode` rather than probing argument shape.
14
14
  */
15
15
 
16
- import { sanitizeText } from "@oh-my-pi/pi-utils";
17
16
  import {
18
17
  ABORT_MARKER,
19
18
  BEGIN_PATCH_MARKER,
20
- computeHashlineDiff,
21
- computeHashlineSectionDiff,
22
19
  containsRecognizableHashlineOperations,
23
20
  END_PATCH_MARKER,
24
- type HashlineInputSection,
25
- HashlineTokenizer,
26
- splitHashlineInputs,
27
- } from "../hashline";
21
+ type PatchSection as HashlineInputSection,
22
+ Patch as HashlinePatch,
23
+ } from "@oh-my-pi/hashline";
28
24
  import type { Theme } from "../modes/theme/theme";
29
- import { replaceTabs, truncateToWidth } from "../tools/render-utils";
30
25
  import { type EditMode, resolveEditMode } from "../utils/edit-mode";
31
26
  import { computeEditDiff, type DiffError, type DiffResult } from "./diff";
27
+ import { computeHashlineDiff, computeHashlineSectionDiff } from "./hashline/diff";
32
28
  import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
33
29
  import { computePatchDiff, type PatchEditEntry } from "./modes/patch";
34
30
  import type { ReplaceEditEntry } from "./modes/replace";
@@ -74,48 +70,6 @@ export interface EditStreamingStrategy<Args = unknown> {
74
70
  renderStreamingFallback(args: Args, uiTheme: Theme): string;
75
71
  }
76
72
 
77
- const STREAMING_FALLBACK_LINES = 12;
78
- const STREAMING_FALLBACK_WIDTH = 80;
79
-
80
- // Streaming-preview classification reuses one tokenizer instance for the
81
- // stateless predicates and `tokenize`/`tokenizeAll` helpers; instances are
82
- // cheap, but keeping a single module-level reference matches the rest of
83
- // the hashline package.
84
- const HASHLINE_TOKENIZER = new HashlineTokenizer();
85
-
86
- function trimHashlineStreamingSyntax(lines: string[]): string[] {
87
- let index = lines.findIndex(line => line.trim().length > 0);
88
- if (index === -1) return [];
89
-
90
- if (HASHLINE_TOKENIZER.tokenize(lines[index]).kind === "envelope-begin") {
91
- index++;
92
- while (index < lines.length && lines[index].trim().length === 0) index++;
93
- }
94
- if (index < lines.length && HASHLINE_TOKENIZER.tokenize(lines[index]).kind === "header") {
95
- index++;
96
- }
97
-
98
- return lines.slice(index).filter(line => !HASHLINE_TOKENIZER.isEnvelopeMarker(line));
99
- }
100
-
101
- function renderHashlineInputFallback(input: string, uiTheme: Theme): string {
102
- const lines = trimHashlineStreamingSyntax(sanitizeText(input).split("\n"));
103
- if (!lines.some(line => line.trim().length > 0)) return "";
104
-
105
- const displayLines = lines.slice(-STREAMING_FALLBACK_LINES);
106
- const hidden = lines.length - displayLines.length;
107
- let text = "\n\n";
108
- text += displayLines
109
- .map(line => uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), STREAMING_FALLBACK_WIDTH)))
110
- .join("\n");
111
- if (hidden > 0) {
112
- text += uiTheme.fg("dim", `\n… (streaming +${hidden} lines)`);
113
- } else {
114
- text += uiTheme.fg("dim", "\n(streaming)");
115
- }
116
- return text;
117
- }
118
-
119
73
  // -----------------------------------------------------------------------------
120
74
  // Partial-JSON handling
121
75
  // -----------------------------------------------------------------------------
@@ -274,7 +228,6 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = {
274
228
 
275
229
  interface HashlineArgs {
276
230
  input?: string;
277
- path?: string;
278
231
  __partialJson?: string;
279
232
  }
280
233
 
@@ -354,75 +307,6 @@ function buildApplyPatchNaturalOrderPreviews(input: string): PerFileDiffPreview[
354
307
  return previews.length > 0 ? previews : null;
355
308
  }
356
309
 
357
- /**
358
- * Hashline equivalent: emit each payload line as a `+added` line in the
359
- * order the model typed it. We deliberately omit op headers and removal
360
- * targets from the streaming preview because their content lives in the file
361
- * and would require a costly re-apply per tick; the complete unified diff is
362
- * shown once streaming finishes.
363
- */
364
- function buildHashlineNaturalOrderPreviews(
365
- input: string,
366
- defaultPath: string | undefined,
367
- ): PerFileDiffPreview[] | null {
368
- const groups = new Map<string, string[]>();
369
- let currentPath = defaultPath ?? "";
370
- const ensure = (sectionPath: string): string[] => {
371
- let bucket = groups.get(sectionPath);
372
- if (!bucket) {
373
- bucket = [];
374
- groups.set(sectionPath, bucket);
375
- }
376
- return bucket;
377
- };
378
-
379
- // Per-call instance: the streaming preview re-runs each tick with the
380
- // cumulative input, and we need the line counter to start at 1. A
381
- // dedicated tokenizer keeps the shared HASHLINE_TOKENIZER above free
382
- // for stateless predicate use elsewhere in this module.
383
- const streamer = new HashlineTokenizer();
384
- for (const token of streamer.tokenizeAll(input)) {
385
- switch (token.kind) {
386
- case "envelope-begin":
387
- case "envelope-end":
388
- case "abort":
389
- case "op-delete":
390
- continue;
391
- case "blank":
392
- case "raw":
393
- continue;
394
- case "header":
395
- currentPath = token.path;
396
- if (currentPath) ensure(currentPath);
397
- continue;
398
- case "op-insert":
399
- case "op-replace":
400
- // Inline body on the op line itself (`N↓payload`, `A-B:payload`) is
401
- // payload content that just happens to share a line with the op
402
- // header — render it the same as a standalone payload token so
403
- // the very first character the model types after the sigil shows
404
- // up in the streaming preview. Without this, the preview is
405
- // empty until a newline arrives, and the renderer falls back to
406
- // raw input ("A-B: bla bla bla") instead of "+ bla bla bla".
407
- if (!currentPath || token.inlineBody === undefined) continue;
408
- ensure(currentPath).push(`+${token.inlineBody}`);
409
- continue;
410
- case "payload":
411
- if (!currentPath) continue;
412
- ensure(currentPath).push(`+${token.text}`);
413
- continue;
414
- }
415
- }
416
-
417
- if (groups.size === 0) return null;
418
- const previews: PerFileDiffPreview[] = [];
419
- for (const [sectionPath, body] of groups) {
420
- if (body.length === 0) continue;
421
- previews.push({ path: sectionPath, diff: body.join("\n") });
422
- }
423
- return previews.length > 0 ? previews : null;
424
- }
425
-
426
310
  const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
427
311
  extractCompleteEdits(args) {
428
312
  return args;
@@ -431,25 +315,21 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
431
315
  if (typeof args.input !== "string" || args.input.length === 0) return null;
432
316
  const input = trimTrailingPartialLine(args.input, ctx.isStreaming);
433
317
  if (input.length === 0) return null;
434
- if (ctx.isStreaming) {
435
- // Skip the costly per-tick re-apply and avoid `Diff.structuredPatch`
436
- // reordering by showing payload lines in input order.
437
- return buildHashlineNaturalOrderPreviews(input, args.path);
438
- }
439
318
  ctx.signal.throwIfAborted();
440
319
 
441
- let sections: HashlineInputSection[];
320
+ let sections: readonly HashlineInputSection[];
442
321
  try {
443
- sections = splitHashlineInputs(input, { cwd: ctx.cwd, path: args.path });
322
+ sections = HashlinePatch.parse(input, { cwd: ctx.cwd }).sections;
444
323
  } catch {
445
- // Single-section fallback keeps the original error rendering for the
446
- // "haven't typed PATH` yet" case.
447
- const result = await computeHashlineDiff({ input, path: args.path }, ctx.cwd, {
324
+ // While streaming, the trailing op may still be mid-typed and fail
325
+ // to parse; suppress until the next chunk arrives. Once args are
326
+ // complete, surface the error so the model sees what went wrong.
327
+ if (ctx.isStreaming) return null;
328
+ const result = await computeHashlineDiff({ input }, ctx.cwd, {
448
329
  autoDropPureInsertDuplicates: ctx.hashlineAutoDropPureInsertDuplicates,
449
330
  });
450
331
  ctx.signal.throwIfAborted();
451
- if ("error" in result && !args.path) return [{ path: "", error: result.error }];
452
- return [toPerFilePreview(args.path ?? "", result)];
332
+ return [toPerFilePreview("", result)];
453
333
  }
454
334
  if (sections.length === 0) return null;
455
335
 
@@ -468,6 +348,7 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
468
348
  const section = sectionsToProcess[i];
469
349
  const result = await computeHashlineSectionDiff(section, ctx.cwd, {
470
350
  autoDropPureInsertDuplicates: ctx.hashlineAutoDropPureInsertDuplicates,
351
+ streaming: ctx.isStreaming,
471
352
  });
472
353
  ctx.signal.throwIfAborted();
473
354
  // In a multi-section preview, ignore parse/apply errors from the
@@ -480,8 +361,13 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
480
361
  }
481
362
  return previews.length > 0 ? previews : null;
482
363
  },
483
- renderStreamingFallback(args, uiTheme) {
484
- return typeof args.input === "string" ? renderHashlineInputFallback(args.input, uiTheme) : "";
364
+ renderStreamingFallback() {
365
+ // Never leak raw hashline syntax (`64:`, `|payload`, `¶path#hash`)
366
+ // to the user — the streaming preview already projects every
367
+ // parseable op onto the real file via applyPartialTo, and an
368
+ // unparseable trailing chunk renders as "no preview yet" rather
369
+ // than a sigil dump.
370
+ return "";
485
371
  },
486
372
  };
487
373
 
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Compatibility shim for legacy extensions importing the package root of
3
+ * `@oh-my-pi/pi-ai` (or one of its aliased scopes like `@earendil-works/pi-ai`
4
+ * or `@mariozechner/pi-ai`).
5
+ *
6
+ * pi-ai 15.1.0 removed the historical TypeBox root exports (`Type`, plus the
7
+ * runtime-relevant half of the `Static`/`TSchema` pair) from the package
8
+ * entrypoint. Legacy extensions still author parameter schemas as
9
+ * `Type.Object({ ... })`, so this file is served by `legacy-pi-compat.ts` in
10
+ * place of the real pi-ai entrypoint whenever a legacy extension imports the
11
+ * bare package root. Subpath imports (`@oh-my-pi/pi-ai/utils/oauth`, etc.)
12
+ * continue to resolve directly against the bundled pi-ai package.
13
+ *
14
+ * The `Type` runtime is borrowed from the Zod-backed TypeBox shim that
15
+ * already serves bare `@sinclair/typebox` imports for the same extension
16
+ * class, keeping the legacy-compat surface internally consistent.
17
+ *
18
+ * Type-level `Static` and `TSchema` continue to come from pi-ai's own
19
+ * `types.ts` via the `export *` below — pi-ai still exports both as types,
20
+ * only the runtime `Type` builder was removed.
21
+ */
22
+
23
+ export * from "@oh-my-pi/pi-ai";
24
+ export { Type } from "./typebox";
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import * as url from "node:url";
5
+ import { isCompiledBinary } from "@oh-my-pi/pi-utils";
5
6
 
6
7
  // Canonical scope for in-process pi packages. Plugins published against any of
7
8
  // the aliased scopes below (mariozechner's original publish, earendil-works'
@@ -56,7 +57,34 @@ const resolvedSpecifierFallbacks = new Map<string, string>();
56
57
  // relying on them must vendor `@sinclair/typebox` directly.
57
58
  const TYPEBOX_SPECIFIER = "@sinclair/typebox";
58
59
  const TYPEBOX_SPECIFIER_FILTER = /^@sinclair\/typebox$/;
59
- const TYPEBOX_SHIM_PATH = path.resolve(import.meta.dir, "../typebox.ts");
60
+
61
+ // In-process compat shim paths. In dev `import.meta.dir` is the source folder of
62
+ // this file, so the dev branches resolve to the real `.ts` source. In compiled
63
+ // binaries `import.meta.dir` collapses to `/$bunfs/root`, so the runtime cannot
64
+ // recover the source layout that way; instead, each shim file is registered as
65
+ // a `--compile` entrypoint in `scripts/build-binary.ts`, which Bun emits into
66
+ // bunfs at a deterministic `--root`-relative path with a `.js` extension. The
67
+ // literals below must stay in sync with that listing — if either path drifts,
68
+ // every legacy plugin loading the shim fails with a missing-module error in
69
+ // release builds (without affecting `bun test`/dev).
70
+ const TYPEBOX_SHIM_PATH = isCompiledBinary()
71
+ ? "/$bunfs/root/packages/coding-agent/src/extensibility/typebox.js"
72
+ : path.resolve(import.meta.dir, "../typebox.ts");
73
+
74
+ // Legacy extensions historically imported `Type` (and `Static`/`TSchema`) from
75
+ // the package root of `@(scope)/pi-ai`. pi-ai 15.1.0 removed the runtime `Type`
76
+ // export (see `packages/ai/CHANGELOG.md`), so the bare canonical specifier no
77
+ // longer satisfies those imports. The override below redirects only the bare
78
+ // pi-ai package root onto a sibling shim that re-exports the canonical surface
79
+ // plus the borrowed `Type` runtime from the Zod-backed TypeBox shim. Subpath
80
+ // imports such as `@oh-my-pi/pi-ai/utils/oauth` continue to resolve directly
81
+ // against the bundled pi-ai package.
82
+ const LEGACY_PI_AI_SHIM_PATH = isCompiledBinary()
83
+ ? "/$bunfs/root/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.js"
84
+ : path.resolve(import.meta.dir, "../legacy-pi-ai-shim.ts");
85
+ const LEGACY_PI_PACKAGE_ROOT_OVERRIDES: Record<string, string> = {
86
+ [`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH,
87
+ };
60
88
 
61
89
  let isLegacyPiSpecifierShimInstalled = false;
62
90
 
@@ -85,6 +113,22 @@ function getResolvedSpecifier(specifier: string): string {
85
113
  return resolved;
86
114
  }
87
115
 
116
+ /**
117
+ * Resolve a canonical `@oh-my-pi/*` specifier to a filesystem path, preferring
118
+ * a bundled compat shim when one is registered for the package root.
119
+ *
120
+ * Falls back to `getResolvedSpecifier` (which may throw under compiled binary
121
+ * mode); callers handle that the same way they would for non-overridden
122
+ * specifiers.
123
+ */
124
+ function resolveCanonicalPiSpecifier(remappedSpecifier: string): string {
125
+ const override = LEGACY_PI_PACKAGE_ROOT_OVERRIDES[remappedSpecifier];
126
+ if (override) {
127
+ return override;
128
+ }
129
+ return getResolvedSpecifier(remappedSpecifier);
130
+ }
131
+
88
132
  function toImportSpecifier(resolvedPath: string): string {
89
133
  return url.pathToFileURL(resolvedPath).href;
90
134
  }
@@ -99,7 +143,7 @@ function rewriteLegacyPiImports(source: string): string {
99
143
  }
100
144
 
101
145
  try {
102
- return `${prefix}${toImportSpecifier(getResolvedSpecifier(remappedSpecifier))}${suffix}`;
146
+ return `${prefix}${toImportSpecifier(resolveCanonicalPiSpecifier(remappedSpecifier))}${suffix}`;
103
147
  } catch {
104
148
  // Resolution failed — typically in compiled binary mode where
105
149
  // Bun.resolveSync cannot walk up from /$bunfs/root to find the
@@ -250,7 +294,7 @@ function resolveLegacyPiSpecifier(args: { path: string; importer: string }): { p
250
294
  // Primary: resolve the canonical @oh-my-pi/* specifier from the host binary
251
295
  // location. Works in dev mode and in source-link installs.
252
296
  try {
253
- return { path: getResolvedSpecifier(remappedSpecifier) };
297
+ return { path: resolveCanonicalPiSpecifier(remappedSpecifier) };
254
298
  } catch {
255
299
  // Fallback for compiled binary mode: the bundled packages live inside
256
300
  // /$bunfs/root and aren't reachable by filesystem resolution. Try the
package/src/index.ts CHANGED
@@ -26,7 +26,6 @@ export * from "./extensibility/extensions";
26
26
  export * from "./extensibility/skills";
27
27
  // Slash commands
28
28
  export { type FileSlashCommand, loadSlashCommands as discoverSlashCommands } from "./extensibility/slash-commands";
29
- export * from "./hashline";
30
29
  export type * from "./lsp";
31
30
  // Main entry point
32
31
  export * from "./main";