@oh-my-pi/pi-coding-agent 16.1.18 → 16.1.20

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 (57) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/cli.js +15452 -15142
  3. package/dist/types/cli/gallery-cli.d.ts +6 -0
  4. package/dist/types/commands/gallery.d.ts +1 -1
  5. package/dist/types/config/service-tier.d.ts +34 -0
  6. package/dist/types/config/settings-schema.d.ts +36 -33
  7. package/dist/types/edit/hashline/filesystem.d.ts +1 -18
  8. package/dist/types/extensibility/plugins/legacy-pi-bundled-keys.d.ts +10 -0
  9. package/dist/types/extensibility/plugins/legacy-pi-bundled-registry.d.ts +4 -1
  10. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +12 -0
  11. package/dist/types/internal-urls/skill-protocol.d.ts +2 -2
  12. package/dist/types/internal-urls/types.d.ts +3 -0
  13. package/dist/types/modes/components/custom-editor.d.ts +0 -2
  14. package/dist/types/modes/controllers/input-controller.d.ts +0 -1
  15. package/dist/types/session/agent-session.d.ts +2 -0
  16. package/dist/types/task/executor.d.ts +9 -1
  17. package/dist/types/task/parallel.d.ts +17 -1
  18. package/dist/types/tools/index.d.ts +3 -1
  19. package/dist/types/tools/path-utils.d.ts +3 -0
  20. package/package.json +13 -13
  21. package/scripts/build-binary.ts +20 -0
  22. package/scripts/generate-legacy-pi-bundled-registry.ts +404 -0
  23. package/src/cli/gallery-cli.ts +31 -2
  24. package/src/cli/gallery-fixtures/agentic.ts +13 -4
  25. package/src/commands/gallery.ts +11 -3
  26. package/src/config/service-tier.ts +87 -0
  27. package/src/config/settings-schema.ts +48 -23
  28. package/src/edit/hashline/filesystem.ts +14 -0
  29. package/src/eval/agent-bridge.ts +2 -0
  30. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +987 -0
  31. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +3330 -18
  32. package/src/extensibility/plugins/legacy-pi-compat.ts +29 -14
  33. package/src/internal-urls/local-protocol.ts +116 -9
  34. package/src/internal-urls/skill-protocol.ts +3 -3
  35. package/src/internal-urls/types.ts +3 -0
  36. package/src/main.ts +1 -0
  37. package/src/mcp/transports/stdio.ts +5 -0
  38. package/src/modes/components/custom-editor.test.ts +7 -5
  39. package/src/modes/components/custom-editor.ts +6 -16
  40. package/src/modes/controllers/input-controller.ts +143 -137
  41. package/src/sdk.ts +1 -0
  42. package/src/session/agent-session.ts +72 -15
  43. package/src/session/messages.ts +70 -47
  44. package/src/session/session-history-format.ts +21 -4
  45. package/src/task/executor.ts +79 -2
  46. package/src/task/index.ts +11 -6
  47. package/src/task/parallel.ts +59 -7
  48. package/src/tools/ast-edit.ts +1 -0
  49. package/src/tools/ast-grep.ts +1 -0
  50. package/src/tools/find.ts +1 -0
  51. package/src/tools/index.ts +3 -1
  52. package/src/tools/irc.ts +16 -2
  53. package/src/tools/path-utils.ts +4 -0
  54. package/src/tools/read.ts +43 -17
  55. package/src/tools/search.ts +4 -0
  56. package/src/utils/shell-snapshot-fn-env.sh +60 -0
  57. package/src/utils/shell-snapshot.ts +77 -20
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import * as url from "node:url";
4
4
  import { isCompiledBinary } from "@oh-my-pi/pi-utils";
5
+ import { BUNDLED_PI_REGISTRY_KEYS } from "./legacy-pi-bundled-keys";
5
6
 
6
7
  const IS_COMPILED_BINARY = isCompiledBinary();
7
8
 
@@ -344,20 +345,34 @@ export function __validateLegacyPiPackageRootOverrides(
344
345
  );
345
346
  }
346
347
 
347
- const LEGACY_PI_PACKAGE_ROOT_OVERRIDES = __validateLegacyPiPackageRootOverrides({
348
- [`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH,
349
- [`${CANONICAL_PI_SCOPE}/pi-coding-agent`]: LEGACY_PI_CODING_AGENT_SHIM_PATH,
350
- ...(IS_COMPILED_BINARY
351
- ? {
352
- [`${CANONICAL_PI_SCOPE}/pi-agent-core`]: bundledRegistryVirtualSpecifier(
353
- `${CANONICAL_PI_SCOPE}/pi-agent-core`,
354
- ),
355
- [`${CANONICAL_PI_SCOPE}/pi-natives`]: bundledRegistryVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-natives`),
356
- [`${CANONICAL_PI_SCOPE}/pi-tui`]: bundledRegistryVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-tui`),
357
- [`${CANONICAL_PI_SCOPE}/pi-utils`]: bundledRegistryVirtualSpecifier(`${CANONICAL_PI_SCOPE}/pi-utils`),
358
- }
359
- : {}),
360
- });
348
+ /**
349
+ * Compute the override map keyed by every canonical specifier the host serves
350
+ * directly: the pi-ai / pi-coding-agent roots (compat shims that re-attach
351
+ * legacy helpers) plus, in compiled-binary mode, every other canonical pi-*
352
+ * package root AND every non-wildcard subpath registered in the bundled
353
+ * registry (see `legacy-pi-bundled-keys.ts`). Subpath coverage is what stops
354
+ * `@(scope)/pi-ai/oauth` and friends from falling through to the extension's
355
+ * own — possibly absent — peer install when bunfs filesystem walks fail
356
+ * (issue #3442 follow-up to #3423). Exported as a test seam so the
357
+ * compiled-binary branch is verifiable from dev tests.
358
+ */
359
+ export function __buildLegacyPiPackageRootOverrides(isCompiled: boolean): Record<string, string> {
360
+ const candidates: Record<string, string> = {
361
+ [`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH,
362
+ [`${CANONICAL_PI_SCOPE}/pi-coding-agent`]: LEGACY_PI_CODING_AGENT_SHIM_PATH,
363
+ };
364
+ if (isCompiled) {
365
+ for (const key of BUNDLED_PI_REGISTRY_KEYS) {
366
+ // Shim-bearing roots above already mapped to their compat surface;
367
+ // the bundled typebox shim has a dedicated TYPEBOX_SHIM_PATH route.
368
+ if (key in candidates || key === TYPEBOX_BUNDLED_REGISTRY_KEY) continue;
369
+ candidates[key] = bundledRegistryVirtualSpecifier(key);
370
+ }
371
+ }
372
+ return __validateLegacyPiPackageRootOverrides(candidates);
373
+ }
374
+
375
+ const LEGACY_PI_PACKAGE_ROOT_OVERRIDES = __buildLegacyPiPackageRootOverrides(IS_COMPILED_BINARY);
361
376
 
362
377
  let isLegacyPiSpecifierShimInstalled = false;
363
378
 
@@ -49,6 +49,121 @@ function getContentType(filePath: string): InternalResource["contentType"] {
49
49
  return "text/plain";
50
50
  }
51
51
 
52
+ const LOCAL_TEXT_SNIFF_BYTES = 8 * 1024;
53
+ const LOCAL_TEXT_RESOURCE_MAX_BYTES = 1024 * 1024;
54
+ const BINARY_FILE_EXTENSIONS = new Set([
55
+ ".7z",
56
+ ".avi",
57
+ ".bmp",
58
+ ".bz2",
59
+ ".db",
60
+ ".doc",
61
+ ".docx",
62
+ ".gif",
63
+ ".gz",
64
+ ".ico",
65
+ ".jpeg",
66
+ ".jpg",
67
+ ".m4v",
68
+ ".mkv",
69
+ ".mov",
70
+ ".mp4",
71
+ ".pdf",
72
+ ".png",
73
+ ".ppt",
74
+ ".pptx",
75
+ ".rar",
76
+ ".sqlite",
77
+ ".tgz",
78
+ ".webm",
79
+ ".webp",
80
+ ".wmv",
81
+ ".xls",
82
+ ".xlsx",
83
+ ".xz",
84
+ ".zip",
85
+ ]);
86
+
87
+ function formatLocalByteSize(bytes: number): string {
88
+ if (bytes < 1024) return `${bytes} B`;
89
+ const kib = bytes / 1024;
90
+ if (kib < 1024) return `${kib.toFixed(1)} KiB`;
91
+ const mib = kib / 1024;
92
+ if (mib < 1024) return `${mib.toFixed(1)} MiB`;
93
+ return `${(mib / 1024).toFixed(1)} GiB`;
94
+ }
95
+
96
+ function buildNonTextLocalResource(url: InternalUrl, filePath: string, size: number, reason: string): InternalResource {
97
+ const content = `[Cannot read binary local:// file '${url.href}' (${formatLocalByteSize(size)}): ${reason}. This resource is not text. Use a metadata/key-frame/video-specific workflow instead.]`;
98
+ return {
99
+ url: url.href,
100
+ content,
101
+ contentType: "text/plain",
102
+ size: Buffer.byteLength(content, "utf-8"),
103
+ sourcePath: filePath,
104
+ notes: [LOCAL_WRITE_NOTE],
105
+ };
106
+ }
107
+
108
+ function buildLargeLocalTextResource(url: InternalUrl, filePath: string, size: number): InternalResource {
109
+ const content = `[Cannot materialize local:// file '${url.href}' as an internal text resource (${formatLocalByteSize(size)} exceeds ${formatLocalByteSize(LOCAL_TEXT_RESOURCE_MAX_BYTES)}). Use the read tool's filesystem path handling or a line selector so content is streamed with file-size safeguards.]`;
110
+ return {
111
+ url: url.href,
112
+ content,
113
+ contentType: "text/plain",
114
+ size: Buffer.byteLength(content, "utf-8"),
115
+ sourcePath: filePath,
116
+ notes: [LOCAL_WRITE_NOTE],
117
+ };
118
+ }
119
+
120
+ async function readFilePrefix(filePath: string, maxBytes: number): Promise<Uint8Array> {
121
+ if (maxBytes <= 0) return new Uint8Array();
122
+ const handle = await fs.open(filePath, "r");
123
+ try {
124
+ const buffer = Buffer.allocUnsafe(maxBytes);
125
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
126
+ return buffer.subarray(0, bytesRead);
127
+ } finally {
128
+ await handle.close();
129
+ }
130
+ }
131
+
132
+ function isUtf8Text(bytes: Uint8Array): boolean {
133
+ if (bytes.indexOf(0) !== -1) return false;
134
+ try {
135
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
136
+ return true;
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ async function buildFileResource(
143
+ url: InternalUrl,
144
+ resolved: Extract<ResolvedLocalTarget, { kind: "file" }>,
145
+ ): Promise<InternalResource> {
146
+ if (BINARY_FILE_EXTENSIONS.has(path.extname(resolved.path).toLowerCase())) {
147
+ return buildNonTextLocalResource(url, resolved.path, resolved.size, "extension is a known binary/container type");
148
+ }
149
+ const sniffBytes = await readFilePrefix(resolved.path, Math.min(resolved.size, LOCAL_TEXT_SNIFF_BYTES));
150
+ if (!isUtf8Text(sniffBytes)) {
151
+ return buildNonTextLocalResource(url, resolved.path, resolved.size, "content is not valid UTF-8 text");
152
+ }
153
+ if (resolved.size > LOCAL_TEXT_RESOURCE_MAX_BYTES) {
154
+ return buildLargeLocalTextResource(url, resolved.path, resolved.size);
155
+ }
156
+ const content = await Bun.file(resolved.path).text();
157
+ return {
158
+ url: url.href,
159
+ content,
160
+ contentType: getContentType(resolved.path),
161
+ size: Buffer.byteLength(content, "utf-8"),
162
+ sourcePath: resolved.path,
163
+ notes: [LOCAL_WRITE_NOTE],
164
+ };
165
+ }
166
+
52
167
  async function listFilesRecursively(rootPath: string): Promise<string[]> {
53
168
  const pending = [""];
54
169
  const files: string[] = [];
@@ -336,15 +451,7 @@ export class LocalProtocolHandler implements ProtocolHandler {
336
451
  return buildDirectoryResource(url.href, resolved.path, [LOCAL_WRITE_NOTE]);
337
452
  }
338
453
 
339
- const content = await Bun.file(resolved.path).text();
340
- return {
341
- url: url.href,
342
- content,
343
- contentType: getContentType(resolved.path),
344
- size: Buffer.byteLength(content, "utf-8"),
345
- sourcePath: resolved.path,
346
- notes: [LOCAL_WRITE_NOTE],
347
- };
454
+ return buildFileResource(url, resolved);
348
455
  }
349
456
 
350
457
  async complete(_query?: string, context?: ResolveContext): Promise<UrlCompletion[]> {
@@ -13,7 +13,7 @@ import * as path from "node:path";
13
13
  import { isEnoent } from "@oh-my-pi/pi-utils";
14
14
  import { getActiveSkills } from "../extensibility/skills";
15
15
  import { buildDirectoryResource } from "./filesystem-resource";
16
- import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
16
+ import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
17
17
 
18
18
  function getContentType(filePath: string): InternalResource["contentType"] {
19
19
  const ext = path.extname(filePath).toLowerCase();
@@ -42,8 +42,8 @@ export class SkillProtocolHandler implements ProtocolHandler {
42
42
  readonly scheme = "skill";
43
43
  readonly immutable = true;
44
44
 
45
- async resolve(url: InternalUrl): Promise<InternalResource> {
46
- const skills = getActiveSkills();
45
+ async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
46
+ const skills = context?.skills ?? getActiveSkills();
47
47
 
48
48
  const skillName = url.rawHost || url.hostname;
49
49
  if (!skillName) {
@@ -5,6 +5,7 @@
5
5
  * providing access to agent outputs and server resources without exposing filesystem paths.
6
6
  */
7
7
 
8
+ import type { Skill } from "../extensibility/skills";
8
9
  import type { LocalProtocolOptions } from "./local-protocol";
9
10
 
10
11
  /**
@@ -90,6 +91,8 @@ export interface ResolveContext {
90
91
  * [#1608](https://github.com/can1357/oh-my-pi/issues/1608).
91
92
  */
92
93
  localProtocolOptions?: LocalProtocolOptions;
94
+ /** Calling session's loaded skills. Prefer this over process-global skill state. */
95
+ skills?: readonly Skill[];
93
96
  }
94
97
 
95
98
  /**
package/src/main.ts CHANGED
@@ -140,6 +140,7 @@ const HOST_DEFAULTED_SETTING_PATHS: SettingPath[] = [
140
140
  "advisor.subagents",
141
141
  "advisor.syncBacklog",
142
142
  "advisor.immuneTurns",
143
+ "serviceTierAdvisor",
143
144
  ];
144
145
 
145
146
  const RPC_BACKGROUND_DEFAULTED_SETTING_PATHS: SettingPath[] = [
@@ -340,6 +340,10 @@ export class StdioTransport implements MCPTransport {
340
340
  platform: process.platform,
341
341
  });
342
342
 
343
+ // Spawn in a new session (detached → setsid) so the MCP process tree has
344
+ // no controlling terminal. Otherwise terminal job-control signals (Ctrl+Z
345
+ // SIGTSTP, background-read SIGTTIN) can stop stdio servers such as
346
+ // chrome-devtools-mcp and leave our read loop blocked on silent pipes.
343
347
  this.#process = spawn({
344
348
  cmd: spawnCommand.cmd,
345
349
  cwd,
@@ -348,6 +352,7 @@ export class StdioTransport implements MCPTransport {
348
352
  stdout: "pipe",
349
353
  stderr: "pipe",
350
354
  windowsHide: spawnCommand.windowsHide,
355
+ detached: true,
351
356
  });
352
357
 
353
358
  this.#connected = true;
@@ -93,15 +93,17 @@ describe("CustomEditor bracketed path paste", () => {
93
93
  expect(extractBracketedImagePastePaths(bracketedPaste("/tmp/report.csv"))).toBeUndefined();
94
94
  });
95
95
 
96
- it("routes non-image path pastes through the file-path hook", async () => {
96
+ it("inserts non-image path pastes as literal text instead of attaching them", () => {
97
97
  const { editor } = makeEditor();
98
- const pasted = Promise.withResolvers<string>();
99
- editor.onPasteFilePath = path => pasted.resolve(path);
98
+ let imagePathCalls = 0;
99
+ editor.onPasteImagePath = () => {
100
+ imagePathCalls++;
101
+ };
100
102
 
101
103
  editor.handleInput(bracketedPaste("/tmp/report.csv"));
102
104
 
103
- expect(await pasted.promise).toBe("/tmp/report.csv");
104
- expect(editor.getText()).toBe("");
105
+ expect(editor.getText()).toBe("/tmp/report.csv");
106
+ expect(imagePathCalls).toBe(0);
105
107
  });
106
108
  });
107
109
 
@@ -318,8 +318,6 @@ export class CustomEditor extends Editor {
318
318
  onPasteImage?: () => Promise<boolean>;
319
319
  /** Called when a bracketed paste contains one or more image-file paths. */
320
320
  onPasteImagePath?: (path: string) => void | Promise<void>;
321
- /** Called when a bracketed paste contains one or more non-image file paths. */
322
- onPasteFilePath?: (path: string) => void | Promise<void>;
323
321
  /** Called when the configured raw text-paste shortcut is pressed. */
324
322
  onPasteTextRaw?: () => void;
325
323
  /** Called when the configured dequeue shortcut is pressed. */
@@ -505,20 +503,12 @@ export class CustomEditor extends Editor {
505
503
  return;
506
504
  }
507
505
 
508
- const pastedPaths = extractBracketedPastePaths(data);
509
- if (pastedPaths) {
510
- const canHandlePaths = pastedPaths.every(path =>
511
- isImagePath(path) ? this.onPasteImagePath !== undefined : this.onPasteFilePath !== undefined,
512
- );
513
- if (canHandlePaths) {
514
- void (async () => {
515
- for (const path of pastedPaths) {
516
- if (isImagePath(path)) await this.onPasteImagePath?.(path);
517
- else await this.onPasteFilePath?.(path);
518
- }
519
- })();
520
- return;
521
- }
506
+ const pastedImagePaths = extractBracketedImagePastePaths(data);
507
+ if (pastedImagePaths && this.onPasteImagePath) {
508
+ void (async () => {
509
+ for (const path of pastedImagePaths) await this.onPasteImagePath?.(path);
510
+ })();
511
+ return;
522
512
  }
523
513
 
524
514
  const parsedKey = parseKey(data);