@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,232 @@
1
+ /**
2
+ * TUI rendering for task tool.
3
+ *
4
+ * Provides renderCall and renderResult functions for displaying
5
+ * task execution in the terminal UI.
6
+ */
7
+
8
+ import path from "node:path";
9
+ import type { Component } from "@oh-my-pi/pi-tui";
10
+ import { Text } from "@oh-my-pi/pi-tui";
11
+ import type { Theme } from "../../../modes/interactive/theme/theme.js";
12
+ import type { RenderResultOptions } from "../../custom-tools/types.js";
13
+ import type { AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types.js";
14
+
15
+ /**
16
+ * Format token count for display (e.g., 1.5k, 25k).
17
+ */
18
+ function formatTokens(tokens: number): string {
19
+ if (tokens >= 1000) {
20
+ return `${(tokens / 1000).toFixed(1)}k`;
21
+ }
22
+ return String(tokens);
23
+ }
24
+
25
+ /**
26
+ * Format duration for display.
27
+ */
28
+ export function formatDuration(ms: number): string {
29
+ if (ms < 1000) return `${ms}ms`;
30
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
31
+ return `${(ms / 60000).toFixed(1)}m`;
32
+ }
33
+
34
+ /**
35
+ * Truncate text to max length with ellipsis.
36
+ */
37
+ function truncate(text: string, maxLen: number): string {
38
+ if (text.length <= maxLen) return text;
39
+ return `${text.slice(0, maxLen - 3)}...`;
40
+ }
41
+
42
+ /**
43
+ * Get status icon for agent state.
44
+ */
45
+ function getStatusIcon(status: AgentProgress["status"]): string {
46
+ switch (status) {
47
+ case "pending":
48
+ return "○";
49
+ case "running":
50
+ return "◐";
51
+ case "completed":
52
+ return "✓";
53
+ case "failed":
54
+ return "✗";
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Render the tool call arguments.
60
+ */
61
+ export function renderCall(args: TaskParams, theme: Theme): Component {
62
+ const label = theme.fg("toolTitle", theme.bold("task"));
63
+
64
+ if (args.tasks.length === 1) {
65
+ // Single task - show agent and task preview
66
+ const task = args.tasks[0];
67
+ const taskPreview = truncate(task.task, 60);
68
+ return new Text(`${label} ${theme.fg("accent", task.agent)}: ${theme.fg("muted", taskPreview)}`, 0, 0);
69
+ }
70
+
71
+ // Multiple tasks - show count and agent names
72
+ const agents = args.tasks.map((t) => t.agent).join(", ");
73
+ return new Text(`${label} ${theme.fg("muted", `${args.tasks.length} agents: ${truncate(agents, 50)}`)}`, 0, 0);
74
+ }
75
+
76
+ /**
77
+ * Render streaming progress for a single agent.
78
+ */
79
+ function renderAgentProgress(progress: AgentProgress, isLast: boolean, expanded: boolean, theme: Theme): string[] {
80
+ const lines: string[] = [];
81
+ const prefix = isLast ? "└─" : "├─";
82
+ const continuePrefix = isLast ? " " : "│ ";
83
+
84
+ const icon = getStatusIcon(progress.status);
85
+ const iconColor = progress.status === "completed" ? "success" : progress.status === "failed" ? "error" : "accent";
86
+
87
+ // Main status line
88
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", progress.agent)}`;
89
+
90
+ if (progress.status === "running") {
91
+ const taskPreview = truncate(progress.task, 40);
92
+ statusLine += `: ${theme.fg("muted", taskPreview)}`;
93
+ statusLine += ` · ${theme.fg("dim", `${progress.toolCount} tools`)}`;
94
+ if (progress.tokens > 0) {
95
+ statusLine += ` · ${theme.fg("dim", `${formatTokens(progress.tokens)} tokens`)}`;
96
+ }
97
+ } else if (progress.status === "completed") {
98
+ statusLine += `: ${theme.fg("success", "done")}`;
99
+ statusLine += ` · ${theme.fg("dim", `${progress.toolCount} tools`)}`;
100
+ statusLine += ` · ${theme.fg("dim", `${formatTokens(progress.tokens)} tokens`)}`;
101
+ } else if (progress.status === "failed") {
102
+ statusLine += `: ${theme.fg("error", "failed")}`;
103
+ }
104
+
105
+ lines.push(statusLine);
106
+
107
+ // Current tool (if running)
108
+ if (progress.status === "running" && progress.currentTool) {
109
+ let toolLine = `${continuePrefix}⎿ ${theme.fg("muted", progress.currentTool)}`;
110
+ if (progress.currentToolArgs) {
111
+ toolLine += `: ${theme.fg("dim", truncate(progress.currentToolArgs, 40))}`;
112
+ }
113
+ if (progress.currentToolStartMs) {
114
+ const elapsed = Date.now() - progress.currentToolStartMs;
115
+ if (elapsed > 5000) {
116
+ toolLine += ` · ${theme.fg("warning", formatDuration(elapsed))}`;
117
+ }
118
+ }
119
+ lines.push(toolLine);
120
+ }
121
+
122
+ // Expanded view: recent output and tools
123
+ if (expanded && progress.status === "running") {
124
+ // Recent output
125
+ for (const line of progress.recentOutput.slice(0, 3)) {
126
+ lines.push(`${continuePrefix} ${theme.fg("dim", truncate(line, 60))}`);
127
+ }
128
+ }
129
+
130
+ return lines;
131
+ }
132
+
133
+ /**
134
+ * Render final result for a single agent.
135
+ */
136
+ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: boolean, theme: Theme): string[] {
137
+ const lines: string[] = [];
138
+ const prefix = isLast ? "└─" : "├─";
139
+ const continuePrefix = isLast ? " " : "│ ";
140
+
141
+ const success = result.exitCode === 0;
142
+ const icon = success ? "✓" : "✗";
143
+ const iconColor = success ? "success" : "error";
144
+
145
+ // Main status line
146
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", result.agent)}`;
147
+ statusLine += `: ${theme.fg(iconColor, success ? "done" : "failed")}`;
148
+ statusLine += ` · ${theme.fg("dim", `${formatTokens(result.tokens)} tokens`)}`;
149
+ statusLine += ` · ${theme.fg("dim", formatDuration(result.durationMs))}`;
150
+
151
+ if (result.truncated) {
152
+ statusLine += ` ${theme.fg("warning", "[truncated]")}`;
153
+ }
154
+
155
+ lines.push(statusLine);
156
+
157
+ // Output preview
158
+ const outputLines = result.output.split("\n").filter((l) => l.trim());
159
+ const previewCount = expanded ? 8 : 3;
160
+
161
+ for (const line of outputLines.slice(0, previewCount)) {
162
+ lines.push(`${continuePrefix}${theme.fg("dim", truncate(line, 70))}`);
163
+ }
164
+
165
+ if (outputLines.length > previewCount) {
166
+ lines.push(`${continuePrefix}${theme.fg("dim", `... ${outputLines.length - previewCount} more lines`)}`);
167
+ }
168
+
169
+ // Error message
170
+ if (result.error && !success) {
171
+ lines.push(`${continuePrefix}${theme.fg("error", truncate(result.error, 70))}`);
172
+ }
173
+
174
+ return lines;
175
+ }
176
+
177
+ /**
178
+ * Render the tool result.
179
+ */
180
+ export function renderResult(
181
+ result: { content: Array<{ type: string; text?: string }>; details?: TaskToolDetails },
182
+ options: RenderResultOptions,
183
+ theme: Theme,
184
+ ): Component {
185
+ const { expanded, isPartial } = options;
186
+ const details = result.details;
187
+
188
+ if (!details) {
189
+ // Fallback to simple text
190
+ const text = result.content.find((c) => c.type === "text")?.text || "";
191
+ return new Text(theme.fg("dim", truncate(text, 100)), 0, 0);
192
+ }
193
+
194
+ const lines: string[] = [];
195
+
196
+ if (isPartial && details.progress) {
197
+ // Streaming progress view
198
+ details.progress.forEach((progress, i) => {
199
+ const isLast = i === details.progress!.length - 1;
200
+ lines.push(...renderAgentProgress(progress, isLast, expanded, theme));
201
+ });
202
+ } else if (details.results.length > 0) {
203
+ // Final results view
204
+ details.results.forEach((res, i) => {
205
+ const isLast = i === details.results.length - 1;
206
+ lines.push(...renderAgentResult(res, isLast, expanded, theme));
207
+ });
208
+
209
+ // Summary line
210
+ const successCount = details.results.filter((r) => r.exitCode === 0).length;
211
+ const failCount = details.results.length - successCount;
212
+ let summary = `\n${theme.fg("dim", "Total:")} `;
213
+ summary += theme.fg("success", `${successCount} succeeded`);
214
+ if (failCount > 0) {
215
+ summary += `, ${theme.fg("error", `${failCount} failed`)}`;
216
+ }
217
+ summary += ` · ${theme.fg("dim", formatDuration(details.totalDurationMs))}`;
218
+ lines.push(summary);
219
+
220
+ // Artifacts location
221
+ if (details.outputPaths && details.outputPaths.length > 0) {
222
+ const artifactsDir = path.dirname(details.outputPaths[0]);
223
+ lines.push(`${theme.fg("dim", "Artifacts:")} ${theme.fg("muted", artifactsDir)}`);
224
+ }
225
+ }
226
+
227
+ if (lines.length === 0) {
228
+ return new Text(theme.fg("dim", "No results"), 0, 0);
229
+ }
230
+
231
+ return new Text(lines.join("\n"), 0, 0);
232
+ }
@@ -0,0 +1,99 @@
1
+ import { type Static, Type } from "@sinclair/typebox";
2
+
3
+ /** Source of an agent definition */
4
+ export type AgentSource = "bundled" | "user" | "project";
5
+
6
+ /** Single task item for parallel execution */
7
+ export const taskItemSchema = Type.Object({
8
+ agent: Type.String({ description: "Agent name" }),
9
+ task: Type.String({ description: "Task description for the agent" }),
10
+ model: Type.Optional(Type.String({ description: "Model override for this task" })),
11
+ });
12
+
13
+ export type TaskItem = Static<typeof taskItemSchema>;
14
+
15
+ /** Maximum tasks per call */
16
+ export const MAX_PARALLEL_TASKS = 32;
17
+
18
+ /** Maximum concurrent workers */
19
+ export const MAX_CONCURRENCY = 16;
20
+
21
+ /** Maximum output bytes per agent */
22
+ export const MAX_OUTPUT_BYTES = 500_000;
23
+
24
+ /** Maximum output lines per agent */
25
+ export const MAX_OUTPUT_LINES = 5000;
26
+
27
+ /** Maximum agents to show in description */
28
+ export const MAX_AGENTS_IN_DESCRIPTION = 10;
29
+
30
+ /** Environment variable to inhibit subagent spawning */
31
+ export const PI_NO_SUBAGENTS_ENV = "PI_NO_SUBAGENTS";
32
+
33
+ /** Task tool parameters */
34
+ export const taskSchema = Type.Object({
35
+ context: Type.Optional(Type.String({ description: "Shared context prepended to all task prompts" })),
36
+ tasks: Type.Array(taskItemSchema, {
37
+ description: "Tasks to run in parallel",
38
+ maxItems: MAX_PARALLEL_TASKS,
39
+ }),
40
+ });
41
+
42
+ export type TaskParams = Static<typeof taskSchema>;
43
+
44
+ /** Agent definition (bundled or discovered) */
45
+ export interface AgentDefinition {
46
+ name: string;
47
+ description: string;
48
+ systemPrompt: string;
49
+ tools?: string[];
50
+ model?: string;
51
+ recursive?: boolean;
52
+ source: AgentSource;
53
+ filePath?: string;
54
+ }
55
+
56
+ /** Progress tracking for a single agent */
57
+ export interface AgentProgress {
58
+ index: number;
59
+ agent: string;
60
+ agentSource: AgentSource;
61
+ status: "pending" | "running" | "completed" | "failed";
62
+ task: string;
63
+ currentTool?: string;
64
+ currentToolArgs?: string;
65
+ currentToolStartMs?: number;
66
+ recentTools: Array<{ tool: string; args: string; endMs: number }>;
67
+ recentOutput: string[];
68
+ toolCount: number;
69
+ tokens: number;
70
+ durationMs: number;
71
+ modelOverride?: string;
72
+ }
73
+
74
+ /** Result from a single agent execution */
75
+ export interface SingleResult {
76
+ index: number;
77
+ agent: string;
78
+ agentSource: AgentSource;
79
+ task: string;
80
+ exitCode: number;
81
+ output: string;
82
+ stderr: string;
83
+ truncated: boolean;
84
+ durationMs: number;
85
+ tokens: number;
86
+ modelOverride?: string;
87
+ error?: string;
88
+ jsonlEvents?: string[];
89
+ artifactPaths?: { inputPath: string; outputPath: string; jsonlPath?: string };
90
+ }
91
+
92
+ /** Tool details for TUI rendering */
93
+ export interface TaskToolDetails {
94
+ projectAgentsDir: string | null;
95
+ results: SingleResult[];
96
+ totalDurationMs: number;
97
+ outputPaths?: string[];
98
+ progress?: AgentProgress[];
99
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Shared truncation utilities for tool outputs.
3
+ *
4
+ * Truncation is based on two independent limits - whichever is hit first wins:
5
+ * - Line limit (default: 2000 lines)
6
+ * - Byte limit (default: 50KB)
7
+ *
8
+ * Never returns partial lines (except bash tail truncation edge case).
9
+ */
10
+
11
+ export const DEFAULT_MAX_LINES = 2000;
12
+ export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
13
+ export const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line
14
+
15
+ export interface TruncationResult {
16
+ /** The truncated content */
17
+ content: string;
18
+ /** Whether truncation occurred */
19
+ truncated: boolean;
20
+ /** Which limit was hit: "lines", "bytes", or null if not truncated */
21
+ truncatedBy: "lines" | "bytes" | null;
22
+ /** Total number of lines in the original content */
23
+ totalLines: number;
24
+ /** Total number of bytes in the original content */
25
+ totalBytes: number;
26
+ /** Number of complete lines in the truncated output */
27
+ outputLines: number;
28
+ /** Number of bytes in the truncated output */
29
+ outputBytes: number;
30
+ /** Whether the last line was partially truncated (only for tail truncation edge case) */
31
+ lastLinePartial: boolean;
32
+ /** Whether the first line exceeded the byte limit (for head truncation) */
33
+ firstLineExceedsLimit: boolean;
34
+ /** The max lines limit that was applied */
35
+ maxLines: number;
36
+ /** The max bytes limit that was applied */
37
+ maxBytes: number;
38
+ }
39
+
40
+ export interface TruncationOptions {
41
+ /** Maximum number of lines (default: 2000) */
42
+ maxLines?: number;
43
+ /** Maximum number of bytes (default: 50KB) */
44
+ maxBytes?: number;
45
+ }
46
+
47
+ /**
48
+ * Format bytes as human-readable size.
49
+ */
50
+ export function formatSize(bytes: number): string {
51
+ if (bytes < 1024) {
52
+ return `${bytes}B`;
53
+ } else if (bytes < 1024 * 1024) {
54
+ return `${(bytes / 1024).toFixed(1)}KB`;
55
+ } else {
56
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Truncate content from the head (keep first N lines/bytes).
62
+ * Suitable for file reads where you want to see the beginning.
63
+ *
64
+ * Never returns partial lines. If first line exceeds byte limit,
65
+ * returns empty content with firstLineExceedsLimit=true.
66
+ */
67
+ export function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {
68
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
69
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
70
+
71
+ const totalBytes = Buffer.byteLength(content, "utf-8");
72
+ const lines = content.split("\n");
73
+ const totalLines = lines.length;
74
+
75
+ // Check if no truncation needed
76
+ if (totalLines <= maxLines && totalBytes <= maxBytes) {
77
+ return {
78
+ content,
79
+ truncated: false,
80
+ truncatedBy: null,
81
+ totalLines,
82
+ totalBytes,
83
+ outputLines: totalLines,
84
+ outputBytes: totalBytes,
85
+ lastLinePartial: false,
86
+ firstLineExceedsLimit: false,
87
+ maxLines,
88
+ maxBytes,
89
+ };
90
+ }
91
+
92
+ // Check if first line alone exceeds byte limit
93
+ const firstLineBytes = Buffer.byteLength(lines[0], "utf-8");
94
+ if (firstLineBytes > maxBytes) {
95
+ return {
96
+ content: "",
97
+ truncated: true,
98
+ truncatedBy: "bytes",
99
+ totalLines,
100
+ totalBytes,
101
+ outputLines: 0,
102
+ outputBytes: 0,
103
+ lastLinePartial: false,
104
+ firstLineExceedsLimit: true,
105
+ maxLines,
106
+ maxBytes,
107
+ };
108
+ }
109
+
110
+ // Collect complete lines that fit
111
+ const outputLinesArr: string[] = [];
112
+ let outputBytesCount = 0;
113
+ let truncatedBy: "lines" | "bytes" = "lines";
114
+
115
+ for (let i = 0; i < lines.length && i < maxLines; i++) {
116
+ const line = lines[i];
117
+ const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0); // +1 for newline
118
+
119
+ if (outputBytesCount + lineBytes > maxBytes) {
120
+ truncatedBy = "bytes";
121
+ break;
122
+ }
123
+
124
+ outputLinesArr.push(line);
125
+ outputBytesCount += lineBytes;
126
+ }
127
+
128
+ // If we exited due to line limit
129
+ if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
130
+ truncatedBy = "lines";
131
+ }
132
+
133
+ const outputContent = outputLinesArr.join("\n");
134
+ const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
135
+
136
+ return {
137
+ content: outputContent,
138
+ truncated: true,
139
+ truncatedBy,
140
+ totalLines,
141
+ totalBytes,
142
+ outputLines: outputLinesArr.length,
143
+ outputBytes: finalOutputBytes,
144
+ lastLinePartial: false,
145
+ firstLineExceedsLimit: false,
146
+ maxLines,
147
+ maxBytes,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Truncate content from the tail (keep last N lines/bytes).
153
+ * Suitable for bash output where you want to see the end (errors, final results).
154
+ *
155
+ * May return partial first line if the last line of original content exceeds byte limit.
156
+ */
157
+ export function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {
158
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
159
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
160
+
161
+ const totalBytes = Buffer.byteLength(content, "utf-8");
162
+ const lines = content.split("\n");
163
+ const totalLines = lines.length;
164
+
165
+ // Check if no truncation needed
166
+ if (totalLines <= maxLines && totalBytes <= maxBytes) {
167
+ return {
168
+ content,
169
+ truncated: false,
170
+ truncatedBy: null,
171
+ totalLines,
172
+ totalBytes,
173
+ outputLines: totalLines,
174
+ outputBytes: totalBytes,
175
+ lastLinePartial: false,
176
+ firstLineExceedsLimit: false,
177
+ maxLines,
178
+ maxBytes,
179
+ };
180
+ }
181
+
182
+ // Work backwards from the end
183
+ const outputLinesArr: string[] = [];
184
+ let outputBytesCount = 0;
185
+ let truncatedBy: "lines" | "bytes" = "lines";
186
+ let lastLinePartial = false;
187
+
188
+ for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
189
+ const line = lines[i];
190
+ const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline
191
+
192
+ if (outputBytesCount + lineBytes > maxBytes) {
193
+ truncatedBy = "bytes";
194
+ // Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,
195
+ // take the end of the line (partial)
196
+ if (outputLinesArr.length === 0) {
197
+ const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
198
+ outputLinesArr.unshift(truncatedLine);
199
+ outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
200
+ lastLinePartial = true;
201
+ }
202
+ break;
203
+ }
204
+
205
+ outputLinesArr.unshift(line);
206
+ outputBytesCount += lineBytes;
207
+ }
208
+
209
+ // If we exited due to line limit
210
+ if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
211
+ truncatedBy = "lines";
212
+ }
213
+
214
+ const outputContent = outputLinesArr.join("\n");
215
+ const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
216
+
217
+ return {
218
+ content: outputContent,
219
+ truncated: true,
220
+ truncatedBy,
221
+ totalLines,
222
+ totalBytes,
223
+ outputLines: outputLinesArr.length,
224
+ outputBytes: finalOutputBytes,
225
+ lastLinePartial,
226
+ firstLineExceedsLimit: false,
227
+ maxLines,
228
+ maxBytes,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Truncate a string to fit within a byte limit (from the end).
234
+ * Handles multi-byte UTF-8 characters correctly.
235
+ */
236
+ function truncateStringToBytesFromEnd(str: string, maxBytes: number): string {
237
+ const buf = Buffer.from(str, "utf-8");
238
+ if (buf.length <= maxBytes) {
239
+ return str;
240
+ }
241
+
242
+ // Start from the end, skip maxBytes back
243
+ let start = buf.length - maxBytes;
244
+
245
+ // Find a valid UTF-8 boundary (start of a character)
246
+ while (start < buf.length && (buf[start] & 0xc0) === 0x80) {
247
+ start++;
248
+ }
249
+
250
+ return buf.slice(start).toString("utf-8");
251
+ }
252
+
253
+ /**
254
+ * Truncate a single line to max characters, adding [truncated] suffix.
255
+ * Used for grep match lines.
256
+ */
257
+ export function truncateLine(
258
+ line: string,
259
+ maxChars: number = GREP_MAX_LINE_LENGTH,
260
+ ): { text: string; wasTruncated: boolean } {
261
+ if (line.length <= maxChars) {
262
+ return { text: line, wasTruncated: false };
263
+ }
264
+ return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };
265
+ }