@oh-my-pi/pi-coding-agent 14.9.2 → 14.9.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +3 -3
  4. package/src/async/job-manager.ts +66 -9
  5. package/src/capability/rule.ts +20 -0
  6. package/src/config/model-registry.ts +13 -0
  7. package/src/config/model-resolver.ts +8 -2
  8. package/src/config/prompt-templates.ts +0 -5
  9. package/src/config/settings-schema.ts +39 -1
  10. package/src/edit/index.ts +8 -0
  11. package/src/edit/renderer.ts +6 -1
  12. package/src/edit/streaming.ts +53 -2
  13. package/src/eval/eval.lark +10 -31
  14. package/src/eval/index.ts +1 -0
  15. package/src/eval/js/context-manager.ts +1 -38
  16. package/src/eval/js/prelude.txt +0 -2
  17. package/src/eval/parse.ts +156 -255
  18. package/src/eval/py/executor.ts +24 -8
  19. package/src/eval/py/index.ts +1 -0
  20. package/src/eval/py/prelude.py +11 -80
  21. package/src/eval/sniff.ts +28 -0
  22. package/src/export/html/template.css +50 -0
  23. package/src/export/html/template.generated.ts +1 -1
  24. package/src/export/html/template.js +229 -17
  25. package/src/extensibility/plugins/loader.ts +31 -6
  26. package/src/extensibility/skills.ts +20 -0
  27. package/src/hashline/constants.ts +20 -0
  28. package/src/hashline/grammar.lark +16 -23
  29. package/src/hashline/hash.ts +4 -34
  30. package/src/hashline/input.ts +16 -2
  31. package/src/hashline/parser.ts +12 -1
  32. package/src/internal-urls/agent-protocol.ts +64 -52
  33. package/src/internal-urls/artifact-protocol.ts +52 -51
  34. package/src/internal-urls/docs-index.generated.ts +34 -1
  35. package/src/internal-urls/index.ts +6 -19
  36. package/src/internal-urls/local-protocol.ts +50 -7
  37. package/src/internal-urls/mcp-protocol.ts +3 -8
  38. package/src/internal-urls/memory-protocol.ts +90 -59
  39. package/src/internal-urls/pi-protocol.ts +1 -0
  40. package/src/internal-urls/router.ts +40 -23
  41. package/src/internal-urls/rule-protocol.ts +3 -20
  42. package/src/internal-urls/skill-protocol.ts +5 -27
  43. package/src/internal-urls/types.ts +18 -2
  44. package/src/main.ts +1 -1
  45. package/src/mcp/manager.ts +17 -0
  46. package/src/modes/components/session-observer-overlay.ts +2 -2
  47. package/src/modes/components/tool-execution.ts +6 -0
  48. package/src/modes/components/tree-selector.ts +4 -0
  49. package/src/modes/controllers/event-controller.ts +23 -2
  50. package/src/modes/controllers/mcp-command-controller.ts +7 -10
  51. package/src/modes/interactive-mode.ts +2 -2
  52. package/src/modes/theme/theme.ts +27 -27
  53. package/src/modes/types.ts +1 -1
  54. package/src/modes/utils/ui-helpers.ts +14 -9
  55. package/src/prompts/commands/orchestrate.md +1 -0
  56. package/src/prompts/system/custom-system-prompt.md +0 -2
  57. package/src/prompts/system/project-prompt.md +10 -0
  58. package/src/prompts/system/subagent-system-prompt.md +18 -9
  59. package/src/prompts/system/subagent-user-prompt.md +1 -10
  60. package/src/prompts/system/system-prompt.md +159 -232
  61. package/src/prompts/tools/ask.md +0 -1
  62. package/src/prompts/tools/bash.md +0 -34
  63. package/src/prompts/tools/eval.md +27 -16
  64. package/src/prompts/tools/github.md +6 -5
  65. package/src/prompts/tools/hashline.md +1 -0
  66. package/src/prompts/tools/job.md +14 -6
  67. package/src/prompts/tools/task.md +20 -3
  68. package/src/registry/agent-registry.ts +2 -1
  69. package/src/sdk.ts +87 -89
  70. package/src/session/agent-session.ts +107 -37
  71. package/src/session/artifacts.ts +7 -4
  72. package/src/session/session-manager.ts +30 -1
  73. package/src/ssh/connection-manager.ts +32 -16
  74. package/src/ssh/sshfs-mount.ts +10 -7
  75. package/src/system-prompt.ts +3 -9
  76. package/src/task/executor.ts +23 -7
  77. package/src/task/index.ts +57 -36
  78. package/src/tool-discovery/tool-index.ts +21 -8
  79. package/src/tools/ast-edit.ts +3 -2
  80. package/src/tools/ast-grep.ts +3 -2
  81. package/src/tools/bash.ts +30 -50
  82. package/src/tools/browser/tab-supervisor.ts +12 -2
  83. package/src/tools/eval.ts +59 -44
  84. package/src/tools/fetch.ts +1 -1
  85. package/src/tools/gh.ts +140 -4
  86. package/src/tools/index.ts +12 -11
  87. package/src/tools/job.ts +48 -12
  88. package/src/tools/path-utils.ts +21 -1
  89. package/src/tools/read.ts +74 -31
  90. package/src/tools/search.ts +16 -3
  91. package/src/tools/todo-write.ts +1 -1
  92. package/src/utils/file-display-mode.ts +11 -5
  93. package/src/web/scrapers/mastodon.ts +1 -1
  94. package/src/web/scrapers/repology.ts +7 -7
  95. package/src/internal-urls/jobs-protocol.ts +0 -119
  96. package/src/task/template.ts +0 -47
  97. package/src/tools/bash-normalize.ts +0 -107
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Protocol handler for agent:// URLs.
3
3
  *
4
- * Resolves agent output IDs to artifact files in the session directory.
4
+ * Resolves agent output IDs against the artifacts directories of every active
5
+ * session. Parents and subagents share outputs via this registry: a subagent
6
+ * can read its parent's output IDs because both sessions are registered in
7
+ * the shared context.
5
8
  *
6
9
  * URL forms:
7
10
  * - agent://<id> - Full output content
@@ -11,27 +14,27 @@
11
14
  import * as fs from "node:fs/promises";
12
15
  import * as path from "node:path";
13
16
  import { isEnoent } from "@oh-my-pi/pi-utils";
17
+ import { AgentRegistry } from "../registry/agent-registry";
14
18
  import { applyQuery, pathToQuery } from "./json-query";
15
19
  import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
16
20
 
17
- export interface AgentProtocolOptions {
18
- /**
19
- * Returns the artifacts directory path, or null if no session.
20
- * Artifacts directory is the session file path without .jsonl extension.
21
- */
22
- getArtifactsDir: () => string | null;
23
- }
24
-
25
21
  /**
26
- * List available output IDs in artifacts directory.
22
+ * Snapshot of artifacts dirs for every registered session, deduped.
23
+ *
24
+ * Prefers `sessionManager.getArtifactsDir()` because subagents adopt the
25
+ * parent's manager and report the parent's dir there; dedup then collapses
26
+ * the whole agent tree to one entry. Falls back to the raw session file
27
+ * when no live session reference is attached.
27
28
  */
28
- async function listAvailableOutputs(artifactsDir: string): Promise<string[]> {
29
- try {
30
- const files = await fs.readdir(artifactsDir);
31
- return files.filter(f => f.endsWith(".md")).map(f => f.replace(".md", ""));
32
- } catch {
33
- return [];
29
+ function artifactsDirsFromRegistry(): string[] {
30
+ const dirs: string[] = [];
31
+ for (const ref of AgentRegistry.global().list()) {
32
+ const dir =
33
+ ref.session?.sessionManager.getArtifactsDir() ?? (ref.sessionFile ? ref.sessionFile.slice(0, -6) : null);
34
+ if (!dir) continue;
35
+ if (!dirs.includes(dir)) dirs.push(dir);
34
36
  }
37
+ return dirs;
35
38
  }
36
39
 
37
40
  /**
@@ -42,31 +45,14 @@ async function listAvailableOutputs(artifactsDir: string): Promise<string[]> {
42
45
  */
43
46
  export class AgentProtocolHandler implements ProtocolHandler {
44
47
  readonly scheme = "agent";
45
-
46
- constructor(private readonly options: AgentProtocolOptions) {}
48
+ readonly immutable = true;
47
49
 
48
50
  async resolve(url: InternalUrl): Promise<InternalResource> {
49
- const artifactsDir = this.options.getArtifactsDir();
50
- if (!artifactsDir) {
51
- throw new Error("No session - agent outputs unavailable");
52
- }
53
-
54
- try {
55
- await fs.stat(artifactsDir);
56
- } catch (err) {
57
- if (isEnoent(err)) {
58
- throw new Error("No artifacts directory found");
59
- }
60
- throw err;
61
- }
62
-
63
- // Extract output ID from host
64
51
  const outputId = url.rawHost || url.hostname;
65
52
  if (!outputId) {
66
53
  throw new Error("agent:// URL requires an output ID: agent://<id>");
67
54
  }
68
55
 
69
- // Check for conflicting extraction methods
70
56
  const urlPath = url.pathname;
71
57
  const queryParam = url.searchParams.get("q");
72
58
  const hasPathExtraction = urlPath && urlPath !== "/" && urlPath !== "";
@@ -76,28 +62,57 @@ export class AgentProtocolHandler implements ProtocolHandler {
76
62
  throw new Error("agent:// URL cannot combine path extraction with ?q=");
77
63
  }
78
64
 
79
- // Load the output file
80
- const outputPath = path.join(artifactsDir, `${outputId}.md`);
81
- try {
82
- await fs.stat(outputPath);
83
- } catch (err) {
84
- if (isEnoent(err)) {
85
- const available = await listAvailableOutputs(artifactsDir);
86
- const availableStr = available.length > 0 ? available.join(", ") : "none";
87
- throw new Error(`Not found: ${outputId}\nAvailable: ${availableStr}`);
65
+ const dirs = artifactsDirsFromRegistry();
66
+
67
+ if (dirs.length === 0) {
68
+ throw new Error("No session - agent outputs unavailable");
69
+ }
70
+
71
+ let foundPath: string | undefined;
72
+ let anyDirExists = false;
73
+ const availableIds = new Set<string>();
74
+
75
+ for (const dir of dirs) {
76
+ try {
77
+ await fs.stat(dir);
78
+ anyDirExists = true;
79
+ } catch (err) {
80
+ if (isEnoent(err)) continue;
81
+ throw err;
82
+ }
83
+ const candidate = path.join(dir, `${outputId}.md`);
84
+ try {
85
+ await fs.stat(candidate);
86
+ foundPath = candidate;
87
+ break;
88
+ } catch (err) {
89
+ if (!isEnoent(err)) throw err;
90
+ try {
91
+ const files = await fs.readdir(dir);
92
+ for (const f of files) {
93
+ if (f.endsWith(".md")) availableIds.add(f.replace(/\.md$/, ""));
94
+ }
95
+ } catch {
96
+ // Listing failures are non-fatal; continue searching.
97
+ }
88
98
  }
89
- throw err;
90
99
  }
91
100
 
92
- const rawContent = await Bun.file(outputPath).text();
93
- const notes: string[] = [];
101
+ if (!anyDirExists) {
102
+ throw new Error("No artifacts directory found");
103
+ }
94
104
 
95
- // Handle extraction
105
+ if (!foundPath) {
106
+ const availableStr = availableIds.size > 0 ? [...availableIds].join(", ") : "none";
107
+ throw new Error(`Not found: ${outputId}\nAvailable: ${availableStr}`);
108
+ }
109
+
110
+ const rawContent = await Bun.file(foundPath).text();
111
+ const notes: string[] = [];
96
112
  let content = rawContent;
97
113
  let contentType: InternalResource["contentType"] = "text/markdown";
98
114
 
99
115
  if (hasPathExtraction || hasQueryExtraction) {
100
- // Parse JSON
101
116
  let jsonValue: unknown;
102
117
  try {
103
118
  jsonValue = JSON.parse(rawContent);
@@ -106,9 +121,7 @@ export class AgentProtocolHandler implements ProtocolHandler {
106
121
  throw new Error(`Output ${outputId} is not valid JSON: ${message}`);
107
122
  }
108
123
 
109
- // Convert path to query if needed
110
124
  const query = hasPathExtraction ? pathToQuery(urlPath) : queryParam!;
111
-
112
125
  if (query) {
113
126
  const extracted = applyQuery(jsonValue, query);
114
127
  try {
@@ -118,7 +131,6 @@ export class AgentProtocolHandler implements ProtocolHandler {
118
131
  }
119
132
  notes.push(`Extracted: ${query}`);
120
133
  } else {
121
- // Empty path/query means return full JSON
122
134
  content = JSON.stringify(jsonValue, null, 2);
123
135
  }
124
136
  contentType = "application/json";
@@ -129,7 +141,7 @@ export class AgentProtocolHandler implements ProtocolHandler {
129
141
  content,
130
142
  contentType,
131
143
  size: Buffer.byteLength(content, "utf-8"),
132
- sourcePath: outputPath,
144
+ sourcePath: foundPath,
133
145
  notes,
134
146
  };
135
147
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Protocol handler for artifact:// URLs.
3
3
  *
4
- * Resolves artifact IDs to files in the session artifacts directory.
5
- * Unlike agent://, artifacts are raw text with no JSON extraction.
4
+ * Resolves artifact IDs against the artifacts directories of every active
5
+ * session. Unlike agent://, artifacts are raw text with no JSON extraction.
6
6
  *
7
7
  * URL form:
8
8
  * - artifact://<id> - Full artifact content
@@ -12,86 +12,87 @@
12
12
  import * as fs from "node:fs/promises";
13
13
  import * as path from "node:path";
14
14
  import { isEnoent } from "@oh-my-pi/pi-utils";
15
+ import { AgentRegistry } from "../registry/agent-registry";
15
16
  import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
16
17
 
17
- export interface ArtifactProtocolOptions {
18
- /**
19
- * Returns the artifacts directory path, or null if no session.
20
- */
21
- getArtifactsDir: () => string | null;
22
- }
23
-
24
18
  /**
25
- * List available artifact IDs in the directory.
19
+ * Snapshot of artifacts dirs across all registered sessions, deduped.
20
+ *
21
+ * Subagents adopt their parent's `ArtifactManager`, so their
22
+ * `sessionManager.getArtifactsDir()` returns the parent's dir; dedup
23
+ * collapses parent + N subagents to a single entry.
26
24
  */
27
- async function listAvailableArtifacts(artifactsDir: string): Promise<string[]> {
28
- try {
29
- const files = await fs.readdir(artifactsDir);
30
- return files
31
- .filter(f => /^\d+\./.test(f))
32
- .map(f => f.split(".")[0])
33
- .sort((a, b) => Number(a) - Number(b));
34
- } catch {
35
- return [];
25
+ function artifactsDirsFromRegistry(): string[] {
26
+ const dirs: string[] = [];
27
+ for (const ref of AgentRegistry.global().list()) {
28
+ const dir =
29
+ ref.session?.sessionManager.getArtifactsDir() ?? (ref.sessionFile ? ref.sessionFile.slice(0, -6) : null);
30
+ if (!dir) continue;
31
+ if (!dirs.includes(dir)) dirs.push(dir);
36
32
  }
33
+ return dirs;
37
34
  }
38
35
 
39
- /**
40
- * Handler for artifact:// URLs.
41
- *
42
- * Resolves numeric artifact IDs to their text content.
43
- * Artifacts are created by tools when output is truncated.
44
- */
45
36
  export class ArtifactProtocolHandler implements ProtocolHandler {
46
37
  readonly scheme = "artifact";
47
-
48
- constructor(private readonly options: ArtifactProtocolOptions) {}
38
+ readonly immutable = true;
49
39
 
50
40
  async resolve(url: InternalUrl): Promise<InternalResource> {
51
- const artifactsDir = this.options.getArtifactsDir();
52
- if (!artifactsDir) {
53
- throw new Error("No session - artifacts unavailable");
54
- }
55
-
56
- // Extract artifact ID from host
57
41
  const id = url.rawHost || url.hostname;
58
42
  if (!id) {
59
43
  throw new Error("artifact:// URL requires a numeric ID: artifact://0");
60
44
  }
61
-
62
- // Validate ID is numeric
63
45
  if (!/^\d+$/.test(id)) {
64
46
  throw new Error(`artifact:// ID must be numeric, got: ${id}`);
65
47
  }
66
48
 
67
- // Check directory exists and find file matching ID prefix
68
- let files: string[];
69
- try {
70
- files = await fs.readdir(artifactsDir);
71
- } catch (err) {
72
- if (isEnoent(err)) {
73
- throw new Error("No artifacts directory found");
49
+ const dirs = artifactsDirsFromRegistry();
50
+
51
+ if (dirs.length === 0) {
52
+ throw new Error("No session - artifacts unavailable");
53
+ }
54
+
55
+ let foundPath: string | undefined;
56
+ let anyDirExists = false;
57
+ const availableIds = new Set<string>();
58
+
59
+ for (const dir of dirs) {
60
+ let files: string[];
61
+ try {
62
+ files = await fs.readdir(dir);
63
+ anyDirExists = true;
64
+ } catch (err) {
65
+ if (isEnoent(err)) continue;
66
+ throw err;
67
+ }
68
+ const match = files.find(f => f.startsWith(`${id}.`));
69
+ if (match) {
70
+ foundPath = path.join(dir, match);
71
+ break;
72
+ }
73
+ for (const f of files) {
74
+ const m = f.match(/^(\d+)\./);
75
+ if (m) availableIds.add(m[1]);
74
76
  }
75
- throw err;
76
77
  }
77
78
 
78
- const match = files.find(f => f.startsWith(`${id}.`));
79
+ if (!anyDirExists) {
80
+ throw new Error("No artifacts directory found");
81
+ }
79
82
 
80
- if (!match) {
81
- const available = await listAvailableArtifacts(artifactsDir);
82
- const availableStr = available.length > 0 ? available.join(", ") : "none";
83
+ if (!foundPath) {
84
+ const sorted = [...availableIds].sort((a, b) => Number(a) - Number(b));
85
+ const availableStr = sorted.length > 0 ? sorted.join(", ") : "none";
83
86
  throw new Error(`Artifact ${id} not found. Available: ${availableStr}`);
84
87
  }
85
88
 
86
- const filePath = path.join(artifactsDir, match);
87
- const content = await Bun.file(filePath).text();
88
-
89
+ const content = await Bun.file(foundPath).text();
89
90
  return {
90
91
  url: url.href,
91
92
  content,
92
93
  contentType: "text/plain",
93
94
  size: Buffer.byteLength(content, "utf-8"),
94
- sourcePath: filePath,
95
+ sourcePath: foundPath,
95
96
  };
96
97
  }
97
98
  }