@oh-my-pi/pi-coding-agent 1.337.0

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 (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Agent discovery from filesystem.
3
+ *
4
+ * Discovers agent definitions from:
5
+ * - ~/.pi/agent/agents/*.md (user-level, primary)
6
+ * - ~/.claude/agents/*.md (user-level, fallback)
7
+ * - .pi/agents/*.md (project-level, primary)
8
+ * - .claude/agents/*.md (project-level, fallback)
9
+ *
10
+ * Agent files use markdown with YAML frontmatter.
11
+ */
12
+
13
+ import * as fs from "node:fs";
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+ import { loadBundledAgents } from "./agents.js";
17
+ import type { AgentDefinition, AgentSource } from "./types.js";
18
+
19
+ /** Result of agent discovery */
20
+ export interface DiscoveryResult {
21
+ agents: AgentDefinition[];
22
+ projectAgentsDir: string | null;
23
+ }
24
+
25
+ /**
26
+ * Parse YAML frontmatter from markdown content.
27
+ */
28
+ function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
29
+ const frontmatter: Record<string, string> = {};
30
+ const normalized = content.replace(/\r\n/g, "\n");
31
+
32
+ if (!normalized.startsWith("---")) {
33
+ return { frontmatter, body: normalized };
34
+ }
35
+
36
+ const endIndex = normalized.indexOf("\n---", 3);
37
+ if (endIndex === -1) {
38
+ return { frontmatter, body: normalized };
39
+ }
40
+
41
+ const frontmatterBlock = normalized.slice(4, endIndex);
42
+ const body = normalized.slice(endIndex + 4).trim();
43
+
44
+ for (const line of frontmatterBlock.split("\n")) {
45
+ const match = line.match(/^([\w-]+):\s*(.*)$/);
46
+ if (match) {
47
+ let value = match[2].trim();
48
+ // Strip quotes
49
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
50
+ value = value.slice(1, -1);
51
+ }
52
+ frontmatter[match[1]] = value;
53
+ }
54
+ }
55
+
56
+ return { frontmatter, body };
57
+ }
58
+
59
+ /**
60
+ * Load agents from a directory.
61
+ */
62
+ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[] {
63
+ const agents: AgentDefinition[] = [];
64
+
65
+ if (!fs.existsSync(dir)) {
66
+ return agents;
67
+ }
68
+
69
+ let entries: fs.Dirent[];
70
+ try {
71
+ entries = fs.readdirSync(dir, { withFileTypes: true });
72
+ } catch {
73
+ return agents;
74
+ }
75
+
76
+ for (const entry of entries) {
77
+ if (!entry.name.endsWith(".md")) continue;
78
+
79
+ const filePath = path.join(dir, entry.name);
80
+
81
+ // Handle both regular files and symlinks
82
+ try {
83
+ if (!fs.statSync(filePath).isFile()) continue;
84
+ } catch {
85
+ continue;
86
+ }
87
+
88
+ let content: string;
89
+ try {
90
+ content = fs.readFileSync(filePath, "utf-8");
91
+ } catch {
92
+ continue;
93
+ }
94
+
95
+ const { frontmatter, body } = parseFrontmatter(content);
96
+
97
+ // Require name and description
98
+ if (!frontmatter.name || !frontmatter.description) {
99
+ continue;
100
+ }
101
+
102
+ const tools = frontmatter.tools
103
+ ?.split(",")
104
+ .map((t) => t.trim())
105
+ .filter(Boolean);
106
+
107
+ const recursive =
108
+ frontmatter.recursive === undefined
109
+ ? undefined
110
+ : frontmatter.recursive === "true" || frontmatter.recursive === "1";
111
+
112
+ agents.push({
113
+ name: frontmatter.name,
114
+ description: frontmatter.description,
115
+ tools: tools && tools.length > 0 ? tools : undefined,
116
+ model: frontmatter.model,
117
+ recursive,
118
+ systemPrompt: body,
119
+ source,
120
+ filePath,
121
+ });
122
+ }
123
+
124
+ return agents;
125
+ }
126
+
127
+ /**
128
+ * Check if path is a directory.
129
+ */
130
+ function isDirectory(p: string): boolean {
131
+ try {
132
+ return fs.statSync(p).isDirectory();
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Find nearest directory by walking up from cwd.
140
+ */
141
+ function findNearestDir(cwd: string, relPath: string): string | null {
142
+ let currentDir = cwd;
143
+ while (true) {
144
+ const candidate = path.join(currentDir, relPath);
145
+ if (isDirectory(candidate)) return candidate;
146
+
147
+ const parentDir = path.dirname(currentDir);
148
+ if (parentDir === currentDir) return null;
149
+ currentDir = parentDir;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Discover agents from filesystem and merge with bundled agents.
155
+ *
156
+ * Precedence (highest wins): project > user > bundled
157
+ * Within each level: .pi > .claude
158
+ *
159
+ * @param cwd - Current working directory for project agent discovery
160
+ */
161
+ export function discoverAgents(cwd: string): DiscoveryResult {
162
+ // Primary directories (.pi)
163
+ const userPiDir = path.join(os.homedir(), ".pi", "agent", "agents");
164
+ const projectPiDir = findNearestDir(cwd, ".pi/agents");
165
+
166
+ // Fallback directories (.claude)
167
+ const userClaudeDir = path.join(os.homedir(), ".claude", "agents");
168
+ const projectClaudeDir = findNearestDir(cwd, ".claude/agents");
169
+
170
+ const agentMap = new Map<string, AgentDefinition>();
171
+
172
+ // 1. Bundled agents (lowest priority)
173
+ for (const agent of loadBundledAgents()) {
174
+ agentMap.set(agent.name, agent);
175
+ }
176
+
177
+ // 2. User agents (.claude then .pi - .pi overrides .claude)
178
+ for (const agent of loadAgentsFromDir(userClaudeDir, "user")) {
179
+ agentMap.set(agent.name, agent);
180
+ }
181
+ for (const agent of loadAgentsFromDir(userPiDir, "user")) {
182
+ agentMap.set(agent.name, agent);
183
+ }
184
+
185
+ // 3. Project agents (highest priority - .claude then .pi)
186
+ if (projectClaudeDir) {
187
+ for (const agent of loadAgentsFromDir(projectClaudeDir, "project")) {
188
+ agentMap.set(agent.name, agent);
189
+ }
190
+ }
191
+ if (projectPiDir) {
192
+ for (const agent of loadAgentsFromDir(projectPiDir, "project")) {
193
+ agentMap.set(agent.name, agent);
194
+ }
195
+ }
196
+
197
+ return {
198
+ agents: Array.from(agentMap.values()),
199
+ projectAgentsDir: projectPiDir,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Get an agent by name from discovered agents.
205
+ */
206
+ export function getAgent(agents: AgentDefinition[], name: string): AgentDefinition | undefined {
207
+ return agents.find((a) => a.name === name);
208
+ }
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Subprocess execution for subagents.
3
+ *
4
+ * Spawns `pi` in JSON mode to execute tasks with isolated context.
5
+ * Parses JSON events for progress tracking.
6
+ */
7
+
8
+ import { spawn } from "node:child_process";
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+ import * as readline from "node:readline";
13
+ import { resolveModelPattern } from "./model-resolver.js";
14
+ import {
15
+ type AgentDefinition,
16
+ type AgentProgress,
17
+ MAX_OUTPUT_BYTES,
18
+ MAX_OUTPUT_LINES,
19
+ PI_NO_SUBAGENTS_ENV,
20
+ type SingleResult,
21
+ } from "./types.js";
22
+
23
+ /** pi command: 'pi.cmd' on Windows, 'pi' elsewhere */
24
+ const PI_CMD = process.platform === "win32" ? "pi.cmd" : "pi";
25
+
26
+ /** Windows shell option for spawn */
27
+ const PI_SHELL_OPT = process.platform === "win32";
28
+
29
+ /** Options for subprocess execution */
30
+ export interface ExecutorOptions {
31
+ cwd: string;
32
+ agent: AgentDefinition;
33
+ task: string;
34
+ index: number;
35
+ context?: string;
36
+ modelOverride?: string;
37
+ signal?: AbortSignal;
38
+ onProgress?: (progress: AgentProgress) => void;
39
+ sessionFile?: string | null;
40
+ persistArtifacts?: boolean;
41
+ artifactsDir?: string;
42
+ }
43
+
44
+ /**
45
+ * Truncate output to byte and line limits.
46
+ */
47
+ function truncateOutput(output: string): { text: string; truncated: boolean } {
48
+ let truncated = false;
49
+ let byteBudget = MAX_OUTPUT_BYTES;
50
+ let lineBudget = MAX_OUTPUT_LINES;
51
+
52
+ let i = 0;
53
+ let lastNewlineIndex = -1;
54
+ while (i < output.length && byteBudget > 0) {
55
+ const ch = output.charCodeAt(i);
56
+ byteBudget--;
57
+
58
+ if (ch === 10 /* \n */) {
59
+ lineBudget--;
60
+ lastNewlineIndex = i;
61
+ if (lineBudget <= 0) {
62
+ truncated = true;
63
+ break;
64
+ }
65
+ }
66
+
67
+ i++;
68
+ }
69
+
70
+ if (i < output.length) {
71
+ truncated = true;
72
+ }
73
+
74
+ if (truncated && lineBudget <= 0 && lastNewlineIndex >= 0) {
75
+ output = output.slice(0, lastNewlineIndex);
76
+ } else {
77
+ output = output.slice(0, i);
78
+ }
79
+
80
+ return { text: output, truncated };
81
+ }
82
+
83
+ /**
84
+ * Extract a short preview from tool args for display.
85
+ */
86
+ function extractToolArgsPreview(args: Record<string, unknown>): string {
87
+ // Priority order for preview
88
+ const previewKeys = ["command", "file_path", "path", "pattern", "query", "url", "task", "prompt"];
89
+
90
+ for (const key of previewKeys) {
91
+ if (args[key] && typeof args[key] === "string") {
92
+ const value = args[key] as string;
93
+ return value.length > 60 ? `${value.slice(0, 57)}...` : value;
94
+ }
95
+ }
96
+
97
+ return "";
98
+ }
99
+
100
+ /**
101
+ * Run a single agent as a subprocess.
102
+ */
103
+ export async function runSubprocess(options: ExecutorOptions): Promise<SingleResult> {
104
+ const { cwd, agent, task, index, context, modelOverride, signal, onProgress } = options;
105
+ const startTime = Date.now();
106
+
107
+ // Initialize progress
108
+ const progress: AgentProgress = {
109
+ index,
110
+ agent: agent.name,
111
+ agentSource: agent.source,
112
+ status: "running",
113
+ task,
114
+ recentTools: [],
115
+ recentOutput: [],
116
+ toolCount: 0,
117
+ tokens: 0,
118
+ durationMs: 0,
119
+ modelOverride,
120
+ };
121
+
122
+ // Check if already aborted
123
+ if (signal?.aborted) {
124
+ return {
125
+ index,
126
+ agent: agent.name,
127
+ agentSource: agent.source,
128
+ task,
129
+ exitCode: 1,
130
+ output: "",
131
+ stderr: "Aborted before start",
132
+ truncated: false,
133
+ durationMs: 0,
134
+ tokens: 0,
135
+ modelOverride,
136
+ error: "Aborted",
137
+ };
138
+ }
139
+
140
+ // Write system prompt to temp file
141
+ const tempDir = os.tmpdir();
142
+ const promptFile = path.join(
143
+ tempDir,
144
+ `pi-agent-${agent.name}-${Date.now()}-${Math.random().toString(36).slice(2)}.md`,
145
+ );
146
+
147
+ try {
148
+ fs.writeFileSync(promptFile, agent.systemPrompt, "utf-8");
149
+ } catch (err) {
150
+ return {
151
+ index,
152
+ agent: agent.name,
153
+ agentSource: agent.source,
154
+ task,
155
+ exitCode: 1,
156
+ output: "",
157
+ stderr: `Failed to write prompt file: ${err}`,
158
+ truncated: false,
159
+ durationMs: Date.now() - startTime,
160
+ tokens: 0,
161
+ modelOverride,
162
+ error: `Failed to write prompt file: ${err}`,
163
+ };
164
+ }
165
+
166
+ // Build full task with context
167
+ const fullTask = context ? `${context}\n\n${task}` : task;
168
+
169
+ // Build args
170
+ const args: string[] = ["--mode", "json", "--non-interactive"];
171
+
172
+ // Add system prompt
173
+ args.push("--append-system-prompt", promptFile);
174
+
175
+ // Add tools if specified
176
+ if (agent.tools && agent.tools.length > 0) {
177
+ args.push("--tools", agent.tools.join(","));
178
+ }
179
+
180
+ // Resolve and add model
181
+ const resolvedModel = resolveModelPattern(modelOverride || agent.model);
182
+ if (resolvedModel) {
183
+ args.push("--model", resolvedModel);
184
+ }
185
+
186
+ // Add session options
187
+ if (options.sessionFile) {
188
+ args.push("--session", options.sessionFile);
189
+ } else {
190
+ args.push("--no-session");
191
+ }
192
+
193
+ // Add task as prompt
194
+ args.push("--prompt", fullTask);
195
+
196
+ // Set up environment
197
+ const env = { ...process.env };
198
+ if (!agent.recursive) {
199
+ env[PI_NO_SUBAGENTS_ENV] = "1";
200
+ }
201
+
202
+ // Spawn subprocess
203
+ const proc = spawn(PI_CMD, args, {
204
+ cwd,
205
+ stdio: ["ignore", "pipe", "pipe"],
206
+ shell: PI_SHELL_OPT,
207
+ env,
208
+ });
209
+
210
+ let output = "";
211
+ let stderr = "";
212
+ let finalOutput = "";
213
+ let resolved = false;
214
+ const jsonlEvents: string[] = [];
215
+
216
+ // Handle abort signal
217
+ const onAbort = () => {
218
+ if (!resolved) {
219
+ proc.kill("SIGTERM");
220
+ }
221
+ };
222
+ if (signal) {
223
+ signal.addEventListener("abort", onAbort, { once: true });
224
+ }
225
+
226
+ // Parse JSON events from stdout
227
+ const rl = readline.createInterface({ input: proc.stdout! });
228
+
229
+ rl.on("line", (line) => {
230
+ if (resolved) return;
231
+
232
+ try {
233
+ const event = JSON.parse(line);
234
+ jsonlEvents.push(line);
235
+ const now = Date.now();
236
+
237
+ switch (event.type) {
238
+ case "tool_execution_start":
239
+ progress.toolCount++;
240
+ progress.currentTool = event.toolName;
241
+ progress.currentToolArgs = extractToolArgsPreview(event.toolArgs || event.args || {});
242
+ progress.currentToolStartMs = now;
243
+ break;
244
+
245
+ case "tool_execution_end":
246
+ if (progress.currentTool) {
247
+ progress.recentTools.unshift({
248
+ tool: progress.currentTool,
249
+ args: progress.currentToolArgs || "",
250
+ endMs: now,
251
+ });
252
+ // Keep only last 5
253
+ if (progress.recentTools.length > 5) {
254
+ progress.recentTools.pop();
255
+ }
256
+ }
257
+ progress.currentTool = undefined;
258
+ progress.currentToolArgs = undefined;
259
+ progress.currentToolStartMs = undefined;
260
+ break;
261
+
262
+ case "message_update":
263
+ case "message_end": {
264
+ // Extract text content for recent output (prefer message.content, fallback to event.content)
265
+ const messageContent = event.message?.content || event.content;
266
+ if (messageContent && Array.isArray(messageContent)) {
267
+ for (const block of messageContent) {
268
+ if (block.type === "text" && block.text) {
269
+ const lines = block.text.split("\n").filter((l: string) => l.trim());
270
+ for (const l of lines) {
271
+ if (!progress.recentOutput.includes(l)) {
272
+ progress.recentOutput.unshift(l);
273
+ if (progress.recentOutput.length > 8) {
274
+ progress.recentOutput.pop();
275
+ }
276
+ }
277
+ }
278
+ output += block.text;
279
+ }
280
+ }
281
+ }
282
+ // Extract usage (prefer message.usage, fallback to event.usage)
283
+ const messageUsage = event.message?.usage || event.usage;
284
+ if (messageUsage) {
285
+ progress.tokens = (messageUsage.input_tokens || 0) + (messageUsage.output_tokens || 0);
286
+ }
287
+ break;
288
+ }
289
+
290
+ case "agent_end":
291
+ // Extract final content from messages array
292
+ if (event.messages && Array.isArray(event.messages)) {
293
+ for (const msg of event.messages) {
294
+ if (msg.content && Array.isArray(msg.content)) {
295
+ for (const block of msg.content) {
296
+ if (block.type === "text" && block.text) {
297
+ finalOutput += block.text;
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+ break;
304
+ }
305
+
306
+ progress.durationMs = now - startTime;
307
+ onProgress?.(progress);
308
+ } catch {
309
+ // Ignore non-JSON lines
310
+ }
311
+ });
312
+
313
+ // Capture stderr
314
+ const stderrDecoder = new TextDecoder();
315
+ proc.stderr?.on("data", (chunk: Buffer) => {
316
+ stderr += stderrDecoder.decode(chunk, { stream: true });
317
+ });
318
+
319
+ // Wait for process to exit
320
+ const exitCode = await new Promise<number>((resolve) => {
321
+ proc.on("close", (code) => {
322
+ resolved = true;
323
+ resolve(code ?? 1);
324
+ });
325
+ proc.on("error", (err) => {
326
+ resolved = true;
327
+ stderr += `\nProcess error: ${err.message}`;
328
+ resolve(1);
329
+ });
330
+ });
331
+
332
+ // Cleanup
333
+ if (signal) {
334
+ signal.removeEventListener("abort", onAbort);
335
+ }
336
+
337
+ try {
338
+ fs.unlinkSync(promptFile);
339
+ } catch {
340
+ // Ignore cleanup errors
341
+ }
342
+
343
+ // Use final output if available, otherwise accumulated output
344
+ const rawOutput = finalOutput || output;
345
+ const { text: truncatedOutput, truncated } = truncateOutput(rawOutput);
346
+
347
+ // Update final progress
348
+ progress.status = exitCode === 0 ? "completed" : "failed";
349
+ progress.durationMs = Date.now() - startTime;
350
+ onProgress?.(progress);
351
+
352
+ return {
353
+ index,
354
+ agent: agent.name,
355
+ agentSource: agent.source,
356
+ task,
357
+ exitCode,
358
+ output: truncatedOutput,
359
+ stderr,
360
+ truncated,
361
+ durationMs: Date.now() - startTime,
362
+ tokens: progress.tokens,
363
+ modelOverride,
364
+ error: exitCode !== 0 && stderr ? stderr : undefined,
365
+ jsonlEvents,
366
+ };
367
+ }