@oh-my-pi/pi-coding-agent 16.3.11 → 16.3.12

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 (80) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/cli.js +3119 -3028
  3. package/dist/types/advisor/runtime.d.ts +11 -0
  4. package/dist/types/config/model-registry.d.ts +4 -0
  5. package/dist/types/config/settings-schema.d.ts +6 -0
  6. package/dist/types/config/settings.d.ts +2 -0
  7. package/dist/types/discovery/helpers.d.ts +9 -0
  8. package/dist/types/exec/bash-executor.d.ts +1 -0
  9. package/dist/types/extensibility/shared-events.d.ts +2 -2
  10. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +1 -0
  11. package/dist/types/internal-urls/registry-helpers.d.ts +7 -5
  12. package/dist/types/modes/components/model-selector.d.ts +2 -1
  13. package/dist/types/modes/github-ref-autocomplete.d.ts +35 -0
  14. package/dist/types/modes/utils/context-usage.d.ts +0 -12
  15. package/dist/types/modes/workflow.d.ts +5 -1
  16. package/dist/types/session/agent-session.d.ts +5 -0
  17. package/dist/types/system-prompt.d.ts +1 -1
  18. package/dist/types/tools/bash-interactive.d.ts +1 -1
  19. package/dist/types/tools/bash-skill-urls.d.ts +1 -0
  20. package/dist/types/tools/bash.d.ts +2 -1
  21. package/dist/types/tools/browser/launch.d.ts +1 -0
  22. package/dist/types/tools/grep.d.ts +2 -0
  23. package/dist/types/tools/index.d.ts +4 -0
  24. package/dist/types/tools/path-utils.d.ts +24 -0
  25. package/dist/types/tools/read.d.ts +2 -0
  26. package/dist/types/utils/local-date.d.ts +2 -0
  27. package/package.json +12 -12
  28. package/src/advisor/__tests__/advisor.test.ts +145 -0
  29. package/src/advisor/runtime.ts +19 -0
  30. package/src/config/api-key-resolver.ts +7 -2
  31. package/src/config/model-registry.ts +9 -0
  32. package/src/config/settings-schema.ts +11 -1
  33. package/src/config/settings.ts +11 -0
  34. package/src/discovery/builtin.ts +2 -1
  35. package/src/discovery/claude-plugins.ts +167 -46
  36. package/src/discovery/helpers.ts +16 -1
  37. package/src/edit/renderer.ts +20 -6
  38. package/src/eval/js/worker-core.ts +163 -6
  39. package/src/exec/bash-executor.ts +14 -9
  40. package/src/extensibility/plugins/legacy-pi-compat.ts +6 -2
  41. package/src/extensibility/plugins/marketplace/fetcher.ts +15 -14
  42. package/src/extensibility/shared-events.ts +2 -2
  43. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +68 -0
  44. package/src/internal-urls/docs-index.generated.txt +1 -1
  45. package/src/internal-urls/registry-helpers.ts +9 -6
  46. package/src/modes/components/model-selector.ts +30 -6
  47. package/src/modes/components/settings-defs.ts +1 -1
  48. package/src/modes/components/status-line/component.ts +14 -2
  49. package/src/modes/controllers/command-controller.ts +13 -23
  50. package/src/modes/controllers/event-controller.ts +12 -12
  51. package/src/modes/controllers/extension-ui-controller.ts +6 -35
  52. package/src/modes/controllers/input-controller.ts +18 -2
  53. package/src/modes/controllers/mcp-command-controller.ts +10 -9
  54. package/src/modes/controllers/selector-controller.ts +16 -5
  55. package/src/modes/github-ref-autocomplete.ts +75 -0
  56. package/src/modes/interactive-mode.ts +54 -10
  57. package/src/modes/prompt-action-autocomplete.ts +35 -0
  58. package/src/modes/utils/context-usage.ts +58 -5
  59. package/src/modes/utils/hotkeys-markdown.ts +2 -1
  60. package/src/modes/utils/ui-helpers.ts +2 -2
  61. package/src/modes/workflow.ts +14 -8
  62. package/src/prompts/system/plan-mode-active.md +5 -2
  63. package/src/prompts/system/system-prompt.md +1 -2
  64. package/src/prompts/system/workflow-notice.md +69 -50
  65. package/src/prompts/tools/bash.md +18 -7
  66. package/src/prompts/tools/grep.md +1 -1
  67. package/src/prompts/tools/read.md +4 -3
  68. package/src/sdk.ts +11 -0
  69. package/src/session/agent-session.ts +128 -14
  70. package/src/system-prompt.ts +3 -2
  71. package/src/tools/bash-interactive.ts +1 -1
  72. package/src/tools/bash-skill-urls.ts +39 -7
  73. package/src/tools/bash.ts +69 -39
  74. package/src/tools/browser/launch.ts +31 -4
  75. package/src/tools/grep.ts +53 -14
  76. package/src/tools/index.ts +11 -0
  77. package/src/tools/path-utils.ts +46 -1
  78. package/src/tools/read.ts +108 -50
  79. package/src/utils/local-date.ts +7 -0
  80. package/src/utils/open.ts +36 -10
@@ -14,6 +14,7 @@ import { buildNonInteractiveEnv } from "./non-interactive-env";
14
14
 
15
15
  export interface BashExecutorOptions {
16
16
  cwd?: string;
17
+ /** Milliseconds before aborting the command; 0 disables the executor deadline. */
17
18
  timeout?: number;
18
19
  onChunk?: (chunk: string) => void;
19
20
  chunkThrottleMs?: number;
@@ -296,11 +297,15 @@ export async function executeBash(command: string, options?: BashExecutorOptions
296
297
 
297
298
  let timeoutTimer: NodeJS.Timeout | undefined;
298
299
  const timeoutDeferred = Promise.withResolvers<"timeout">();
299
- const baseTimeoutMs = Math.max(1_000, options?.timeout ?? 300_000);
300
- timeoutTimer = setTimeout(() => {
301
- abortCurrentExecution();
302
- timeoutDeferred.resolve("timeout");
303
- }, baseTimeoutMs);
300
+ const requestedTimeoutMs = options?.timeout;
301
+ const deadlineTimeoutMs = requestedTimeoutMs === 0 ? undefined : Math.max(1_000, requestedTimeoutMs ?? 300_000);
302
+ const nativeTimeoutMs = requestedTimeoutMs !== undefined && requestedTimeoutMs > 0 ? requestedTimeoutMs : undefined;
303
+ if (deadlineTimeoutMs !== undefined) {
304
+ timeoutTimer = setTimeout(() => {
305
+ abortCurrentExecution();
306
+ timeoutDeferred.resolve("timeout");
307
+ }, deadlineTimeoutMs);
308
+ }
304
309
 
305
310
  let resetSession = false;
306
311
 
@@ -311,7 +316,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
311
316
  command: finalCommand,
312
317
  cwd: commandCwd,
313
318
  env: commandEnv,
314
- timeoutMs: options?.timeout,
319
+ timeoutMs: nativeTimeoutMs,
315
320
  signal: runAbortController.signal,
316
321
  },
317
322
  (err, chunk) => {
@@ -328,7 +333,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
328
333
  sessionEnv: shellEnv,
329
334
  snapshotPath: snapshotPath ?? undefined,
330
335
  minimizer,
331
- timeoutMs: options?.timeout,
336
+ timeoutMs: nativeTimeoutMs,
332
337
  signal: runAbortController.signal,
333
338
  },
334
339
  (err, chunk) => {
@@ -359,8 +364,8 @@ export async function executeBash(command: string, options?: BashExecutorOptions
359
364
  exitCode: undefined,
360
365
  cancelled: true,
361
366
  ...(await sink.dump(
362
- winner.kind === "timeout"
363
- ? `Command timed out after ${Math.round(baseTimeoutMs / 1000)} seconds`
367
+ winner.kind === "timeout" && deadlineTimeoutMs !== undefined
368
+ ? `Command timed out after ${Math.round(deadlineTimeoutMs / 1000)} seconds`
364
369
  : "Command cancelled",
365
370
  )),
366
371
  };
@@ -1059,7 +1059,8 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
1059
1059
  let resolved: string | null = null;
1060
1060
  let nextFollowsBareDependencies = followBareDependencies;
1061
1061
  if (specifier.startsWith(".")) {
1062
- resolved = await realpathOrSelf(Bun.resolveSync(specifier, dir));
1062
+ const candidate = Bun.resolveSync(specifier, dir);
1063
+ resolved = hasSourceModuleExtension(candidate) ? await realpathOrSelf(candidate) : null;
1063
1064
  } else if (specifier.startsWith("#")) {
1064
1065
  resolved = await resolvePackageImportSpecifier(specifier, file);
1065
1066
  } else if (
@@ -1078,7 +1079,10 @@ async function collectExtensionModules(entryRealPath: string): Promise<Map<strin
1078
1079
  dependencyExtension === ".cjs" ||
1079
1080
  dependencyExtension === ".cts" ||
1080
1081
  ((dependencyExtension === ".js" || dependencyExtension === ".jsx") && manifest?.type !== "module");
1081
- resolved = dependencyEntry && !isCommonJsEntry ? await realpathOrSelf(dependencyEntry) : null;
1082
+ resolved =
1083
+ dependencyEntry && hasSourceModuleExtension(dependencyEntry) && !isCommonJsEntry
1084
+ ? await realpathOrSelf(dependencyEntry)
1085
+ : null;
1082
1086
  nextFollowsBareDependencies = false;
1083
1087
  }
1084
1088
  if (resolved && !modules.has(resolved)) {
@@ -196,19 +196,20 @@ export function parseMarketplaceCatalog(content: string, filePath: string): Mark
196
196
  * Catalog paths tried in priority order: omp-namespaced override first, then
197
197
  * the Claude Code-compatible fallback so existing marketplaces keep loading.
198
198
  */
199
- const CATALOG_RELATIVE_PATHS: readonly string[] = [
200
- path.join(".omp-plugin", "marketplace.json"),
201
- path.join(".claude-plugin", "marketplace.json"),
202
- ];
199
+ const CATALOG_RELATIVE_PATHS: readonly string[] = [".omp-plugin/marketplace.json", ".claude-plugin/marketplace.json"];
203
200
 
204
- async function readMarketplaceCatalog(root: string): Promise<{ catalogPath: string; content: string }> {
201
+ async function readMarketplaceCatalog(
202
+ root: string,
203
+ options: { relativeDisplayPaths?: boolean } = {},
204
+ ): Promise<{ catalogPath: string; displayPath: string; content: string }> {
205
205
  const tried: string[] = [];
206
206
  for (const rel of CATALOG_RELATIVE_PATHS) {
207
- const catalogPath = path.join(root, rel);
208
- tried.push(catalogPath);
207
+ const catalogPath = path.join(root, ...rel.split("/"));
208
+ const displayPath = options.relativeDisplayPaths ? rel : catalogPath;
209
+ tried.push(displayPath);
209
210
  try {
210
211
  const content = await Bun.file(catalogPath).text();
211
- return { catalogPath, content };
212
+ return { catalogPath, displayPath, content };
212
213
  } catch (err) {
213
214
  if (isEnoent(err)) continue;
214
215
  throw err;
@@ -252,11 +253,11 @@ export async function fetchMarketplace(source: string, cacheDir: string): Promis
252
253
 
253
254
  if (type === "github") {
254
255
  const url = `https://github.com/${source}.git`;
255
- return cloneAndReadCatalog(url, cacheDir);
256
+ return cloneAndReadCatalog(url, source, cacheDir);
256
257
  }
257
258
 
258
259
  if (type === "git") {
259
- return cloneAndReadCatalog(source, cacheDir);
260
+ return cloneAndReadCatalog(source, source, cacheDir);
260
261
  }
261
262
 
262
263
  // type === "url"
@@ -284,7 +285,7 @@ export async function fetchMarketplace(source: string, cacheDir: string): Promis
284
285
  * responsible for promoting the clone to its final cache location via
285
286
  * `promoteCloneToCache` after any duplicate/drift checks pass.
286
287
  */
287
- async function cloneAndReadCatalog(url: string, cacheDir: string): Promise<FetchResult> {
288
+ async function cloneAndReadCatalog(url: string, source: string, cacheDir: string): Promise<FetchResult> {
288
289
  const tmpDir = path.join(cacheDir, `.tmp-clone-${Date.now()}`);
289
290
  await fs.mkdir(cacheDir, { recursive: true });
290
291
 
@@ -292,12 +293,12 @@ async function cloneAndReadCatalog(url: string, cacheDir: string): Promise<Fetch
292
293
  await git.clone(url, tmpDir);
293
294
 
294
295
  try {
295
- const { catalogPath, content } = await readMarketplaceCatalog(tmpDir);
296
- const catalog = parseMarketplaceCatalog(content, catalogPath);
296
+ const { displayPath, content } = await readMarketplaceCatalog(tmpDir, { relativeDisplayPaths: true });
297
+ const catalog = parseMarketplaceCatalog(content, displayPath);
297
298
  return { catalog, clonePath: tmpDir };
298
299
  } catch (err) {
299
300
  await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
300
- throw new Error(`Cloned repository ${url}: ${(err as Error).message}`, { cause: err });
301
+ throw new Error(`Cloned repository ${url}: ${(err as Error).message} (source: ${source})`, { cause: err });
301
302
  }
302
303
  }
303
304
 
@@ -33,7 +33,7 @@ export interface SessionStartEvent {
33
33
  export interface SessionBeforeSwitchEvent {
34
34
  type: "session_before_switch";
35
35
  /** Reason for the switch */
36
- reason: "new" | "resume" | "fork";
36
+ reason: "new" | "resume" | "fork" | "handoff";
37
37
  /** Session file we're switching to (only for "resume") */
38
38
  targetSessionFile?: string;
39
39
  }
@@ -42,7 +42,7 @@ export interface SessionBeforeSwitchEvent {
42
42
  export interface SessionSwitchEvent {
43
43
  type: "session_switch";
44
44
  /** Reason for the switch */
45
- reason: "new" | "resume" | "fork";
45
+ reason: "new" | "resume" | "fork" | "handoff";
46
46
  /** Session file we came from */
47
47
  previousSessionFile: string | undefined;
48
48
  }
@@ -0,0 +1,68 @@
1
+ import { afterAll, afterEach, expect, it } from "bun:test";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { TempDir } from "@oh-my-pi/pi-utils";
5
+ import { AgentRegistry } from "../../registry/agent-registry";
6
+ import type { AgentSession } from "../../session/agent-session";
7
+ import { ArtifactManager } from "../../session/artifacts";
8
+ import { AgentProtocolHandler } from "../agent-protocol";
9
+ import { resetRegisteredArtifactDirsForTests } from "../registry-helpers";
10
+
11
+ const tempDir = TempDir.createSync("omp-nested-agent-repro-");
12
+ afterEach(() => {
13
+ AgentRegistry.resetGlobalForTests();
14
+ resetRegisteredArtifactDirsForTests();
15
+ });
16
+ afterAll(() => {
17
+ tempDir.removeSync();
18
+ });
19
+
20
+ it("agent:// resolves a depth-2 subagent's .md output while its session is live and artifact-manager-adopted", async () => {
21
+ const root = tempDir.path();
22
+ const rootSessionFile = path.join(root, "session.jsonl");
23
+ const rootArtifactsDir = rootSessionFile.slice(0, -6);
24
+ await fs.mkdir(rootArtifactsDir, { recursive: true });
25
+ // Every subagent adopts the root ArtifactManager and reports its dir.
26
+ const sharedArtifactManager = new ArtifactManager(rootArtifactsDir);
27
+
28
+ // A depth-1 subagent's OWN children are written under its own
29
+ // sessionFile.slice(0, -6) (task/index.ts), i.e. one level deeper.
30
+ const midSessionFile = path.join(rootArtifactsDir, "CodexDeepDive.jsonl");
31
+ const midOwnArtifactsDir = midSessionFile.slice(0, -6);
32
+ await fs.mkdir(midOwnArtifactsDir, { recursive: true });
33
+
34
+ const grandchildId = "CodexDeepDive.GraphStore";
35
+ const grandchildSessionFile = path.join(midOwnArtifactsDir, `${grandchildId}.jsonl`);
36
+ await fs.writeFile(path.join(midOwnArtifactsDir, `${grandchildId}.md`), "full report content");
37
+
38
+ const fakeSession = {
39
+ sessionManager: { getArtifactsDir: () => sharedArtifactManager.dir },
40
+ } as unknown as AgentSession;
41
+ const registry = AgentRegistry.global();
42
+ registry.register({
43
+ id: "Main",
44
+ displayName: "main",
45
+ kind: "main",
46
+ session: fakeSession,
47
+ sessionFile: rootSessionFile,
48
+ });
49
+ registry.register({
50
+ id: "CodexDeepDive",
51
+ displayName: "sub",
52
+ kind: "sub",
53
+ parentId: "Main",
54
+ session: fakeSession,
55
+ sessionFile: midSessionFile,
56
+ });
57
+ registry.register({
58
+ id: grandchildId,
59
+ displayName: "sub",
60
+ kind: "sub",
61
+ parentId: "CodexDeepDive",
62
+ session: fakeSession,
63
+ sessionFile: grandchildSessionFile,
64
+ });
65
+
66
+ const resource = await new AgentProtocolHandler().resolve(new URL(`agent://${grandchildId}`) as never);
67
+ expect(resource.content).toBe("full report content");
68
+ });