@oh-my-pi/pi-coding-agent 0.1.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 (337) hide show
  1. package/CHANGELOG.md +1629 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +541 -0
  6. package/docs/extension-loading.md +1004 -0
  7. package/docs/hooks.md +867 -0
  8. package/docs/rpc.md +1040 -0
  9. package/docs/sdk.md +994 -0
  10. package/docs/session-tree-plan.md +441 -0
  11. package/docs/session.md +240 -0
  12. package/docs/skills.md +290 -0
  13. package/docs/theme.md +670 -0
  14. package/docs/tree.md +197 -0
  15. package/docs/tui.md +341 -0
  16. package/examples/README.md +21 -0
  17. package/examples/custom-tools/README.md +124 -0
  18. package/examples/custom-tools/hello/index.ts +20 -0
  19. package/examples/custom-tools/question/index.ts +84 -0
  20. package/examples/custom-tools/subagent/README.md +172 -0
  21. package/examples/custom-tools/subagent/agents/planner.md +37 -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 +89 -0
  57. package/src/bun-imports.d.ts +16 -0
  58. package/src/capability/context-file.ts +40 -0
  59. package/src/capability/extension.ts +48 -0
  60. package/src/capability/hook.ts +40 -0
  61. package/src/capability/index.ts +616 -0
  62. package/src/capability/instruction.ts +37 -0
  63. package/src/capability/mcp.ts +52 -0
  64. package/src/capability/prompt.ts +35 -0
  65. package/src/capability/rule.ts +56 -0
  66. package/src/capability/settings.ts +35 -0
  67. package/src/capability/skill.ts +49 -0
  68. package/src/capability/slash-command.ts +40 -0
  69. package/src/capability/system-prompt.ts +35 -0
  70. package/src/capability/tool.ts +38 -0
  71. package/src/capability/types.ts +166 -0
  72. package/src/cli/args.ts +259 -0
  73. package/src/cli/file-processor.ts +121 -0
  74. package/src/cli/list-models.ts +104 -0
  75. package/src/cli/plugin-cli.ts +661 -0
  76. package/src/cli/session-picker.ts +41 -0
  77. package/src/cli/update-cli.ts +274 -0
  78. package/src/cli.ts +10 -0
  79. package/src/config.ts +391 -0
  80. package/src/core/agent-session.ts +2178 -0
  81. package/src/core/auth-storage.ts +258 -0
  82. package/src/core/bash-executor.ts +197 -0
  83. package/src/core/compaction/branch-summarization.ts +315 -0
  84. package/src/core/compaction/compaction.ts +664 -0
  85. package/src/core/compaction/index.ts +7 -0
  86. package/src/core/compaction/utils.ts +153 -0
  87. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  88. package/src/core/custom-commands/index.ts +15 -0
  89. package/src/core/custom-commands/loader.ts +226 -0
  90. package/src/core/custom-commands/types.ts +112 -0
  91. package/src/core/custom-tools/index.ts +22 -0
  92. package/src/core/custom-tools/loader.ts +248 -0
  93. package/src/core/custom-tools/types.ts +185 -0
  94. package/src/core/custom-tools/wrapper.ts +29 -0
  95. package/src/core/exec.ts +139 -0
  96. package/src/core/export-html/index.ts +159 -0
  97. package/src/core/export-html/template.css +774 -0
  98. package/src/core/export-html/template.generated.ts +2 -0
  99. package/src/core/export-html/template.html +45 -0
  100. package/src/core/export-html/template.js +1185 -0
  101. package/src/core/export-html/template.macro.ts +24 -0
  102. package/src/core/file-mentions.ts +54 -0
  103. package/src/core/hooks/index.ts +16 -0
  104. package/src/core/hooks/loader.ts +288 -0
  105. package/src/core/hooks/runner.ts +434 -0
  106. package/src/core/hooks/tool-wrapper.ts +98 -0
  107. package/src/core/hooks/types.ts +770 -0
  108. package/src/core/index.ts +53 -0
  109. package/src/core/logger.ts +112 -0
  110. package/src/core/mcp/client.ts +185 -0
  111. package/src/core/mcp/config.ts +248 -0
  112. package/src/core/mcp/index.ts +45 -0
  113. package/src/core/mcp/loader.ts +99 -0
  114. package/src/core/mcp/manager.ts +235 -0
  115. package/src/core/mcp/tool-bridge.ts +156 -0
  116. package/src/core/mcp/transports/http.ts +316 -0
  117. package/src/core/mcp/transports/index.ts +6 -0
  118. package/src/core/mcp/transports/stdio.ts +252 -0
  119. package/src/core/mcp/types.ts +228 -0
  120. package/src/core/messages.ts +211 -0
  121. package/src/core/model-registry.ts +334 -0
  122. package/src/core/model-resolver.ts +494 -0
  123. package/src/core/plugins/doctor.ts +67 -0
  124. package/src/core/plugins/index.ts +38 -0
  125. package/src/core/plugins/installer.ts +189 -0
  126. package/src/core/plugins/loader.ts +339 -0
  127. package/src/core/plugins/manager.ts +672 -0
  128. package/src/core/plugins/parser.ts +105 -0
  129. package/src/core/plugins/paths.ts +37 -0
  130. package/src/core/plugins/types.ts +190 -0
  131. package/src/core/sdk.ts +900 -0
  132. package/src/core/session-manager.ts +1837 -0
  133. package/src/core/settings-manager.ts +860 -0
  134. package/src/core/skills.ts +352 -0
  135. package/src/core/slash-commands.ts +132 -0
  136. package/src/core/system-prompt.ts +442 -0
  137. package/src/core/timings.ts +25 -0
  138. package/src/core/title-generator.ts +110 -0
  139. package/src/core/tools/ask.ts +193 -0
  140. package/src/core/tools/bash-interceptor.ts +120 -0
  141. package/src/core/tools/bash.ts +91 -0
  142. package/src/core/tools/context.ts +32 -0
  143. package/src/core/tools/edit-diff.ts +487 -0
  144. package/src/core/tools/edit.ts +140 -0
  145. package/src/core/tools/exa/company.ts +59 -0
  146. package/src/core/tools/exa/index.ts +63 -0
  147. package/src/core/tools/exa/linkedin.ts +59 -0
  148. package/src/core/tools/exa/mcp-client.ts +368 -0
  149. package/src/core/tools/exa/render.ts +200 -0
  150. package/src/core/tools/exa/researcher.ts +90 -0
  151. package/src/core/tools/exa/search.ts +338 -0
  152. package/src/core/tools/exa/types.ts +167 -0
  153. package/src/core/tools/exa/websets.ts +248 -0
  154. package/src/core/tools/find.ts +244 -0
  155. package/src/core/tools/grep.ts +584 -0
  156. package/src/core/tools/index.ts +283 -0
  157. package/src/core/tools/ls.ts +142 -0
  158. package/src/core/tools/lsp/client.ts +767 -0
  159. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  160. package/src/core/tools/lsp/clients/index.ts +49 -0
  161. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  162. package/src/core/tools/lsp/config.ts +845 -0
  163. package/src/core/tools/lsp/edits.ts +110 -0
  164. package/src/core/tools/lsp/index.ts +1364 -0
  165. package/src/core/tools/lsp/render.ts +560 -0
  166. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  167. package/src/core/tools/lsp/types.ts +495 -0
  168. package/src/core/tools/lsp/utils.ts +526 -0
  169. package/src/core/tools/notebook.ts +182 -0
  170. package/src/core/tools/output.ts +198 -0
  171. package/src/core/tools/path-utils.ts +61 -0
  172. package/src/core/tools/read.ts +507 -0
  173. package/src/core/tools/renderers.ts +820 -0
  174. package/src/core/tools/review.ts +275 -0
  175. package/src/core/tools/rulebook.ts +124 -0
  176. package/src/core/tools/task/agents.ts +158 -0
  177. package/src/core/tools/task/artifacts.ts +114 -0
  178. package/src/core/tools/task/commands.ts +157 -0
  179. package/src/core/tools/task/discovery.ts +217 -0
  180. package/src/core/tools/task/executor.ts +531 -0
  181. package/src/core/tools/task/index.ts +548 -0
  182. package/src/core/tools/task/model-resolver.ts +176 -0
  183. package/src/core/tools/task/parallel.ts +38 -0
  184. package/src/core/tools/task/render.ts +502 -0
  185. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  186. package/src/core/tools/task/types.ts +142 -0
  187. package/src/core/tools/truncate.ts +265 -0
  188. package/src/core/tools/web-fetch.ts +2511 -0
  189. package/src/core/tools/web-search/auth.ts +199 -0
  190. package/src/core/tools/web-search/index.ts +583 -0
  191. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  192. package/src/core/tools/web-search/providers/exa.ts +196 -0
  193. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  194. package/src/core/tools/web-search/render.ts +372 -0
  195. package/src/core/tools/web-search/types.ts +180 -0
  196. package/src/core/tools/write.ts +63 -0
  197. package/src/core/ttsr.ts +211 -0
  198. package/src/core/utils.ts +187 -0
  199. package/src/discovery/agents-md.ts +75 -0
  200. package/src/discovery/builtin.ts +647 -0
  201. package/src/discovery/claude.ts +623 -0
  202. package/src/discovery/cline.ts +104 -0
  203. package/src/discovery/codex.ts +571 -0
  204. package/src/discovery/cursor.ts +266 -0
  205. package/src/discovery/gemini.ts +368 -0
  206. package/src/discovery/github.ts +120 -0
  207. package/src/discovery/helpers.test.ts +127 -0
  208. package/src/discovery/helpers.ts +249 -0
  209. package/src/discovery/index.ts +84 -0
  210. package/src/discovery/mcp-json.ts +127 -0
  211. package/src/discovery/vscode.ts +99 -0
  212. package/src/discovery/windsurf.ts +219 -0
  213. package/src/index.ts +192 -0
  214. package/src/main.ts +507 -0
  215. package/src/migrations.ts +156 -0
  216. package/src/modes/cleanup.ts +23 -0
  217. package/src/modes/index.ts +48 -0
  218. package/src/modes/interactive/components/armin.ts +382 -0
  219. package/src/modes/interactive/components/assistant-message.ts +86 -0
  220. package/src/modes/interactive/components/bash-execution.ts +199 -0
  221. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  222. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  223. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  224. package/src/modes/interactive/components/custom-editor.ts +122 -0
  225. package/src/modes/interactive/components/diff.ts +147 -0
  226. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  227. package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
  228. package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
  229. package/src/modes/interactive/components/extensions/index.ts +9 -0
  230. package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
  231. package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
  232. package/src/modes/interactive/components/extensions/types.ts +191 -0
  233. package/src/modes/interactive/components/hook-editor.ts +117 -0
  234. package/src/modes/interactive/components/hook-input.ts +64 -0
  235. package/src/modes/interactive/components/hook-message.ts +96 -0
  236. package/src/modes/interactive/components/hook-selector.ts +91 -0
  237. package/src/modes/interactive/components/model-selector.ts +560 -0
  238. package/src/modes/interactive/components/oauth-selector.ts +136 -0
  239. package/src/modes/interactive/components/plugin-settings.ts +481 -0
  240. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  241. package/src/modes/interactive/components/session-selector.ts +220 -0
  242. package/src/modes/interactive/components/settings-defs.ts +597 -0
  243. package/src/modes/interactive/components/settings-selector.ts +545 -0
  244. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  245. package/src/modes/interactive/components/status-line/index.ts +4 -0
  246. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  247. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  248. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  249. package/src/modes/interactive/components/status-line/types.ts +81 -0
  250. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  251. package/src/modes/interactive/components/status-line.ts +384 -0
  252. package/src/modes/interactive/components/theme-selector.ts +62 -0
  253. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  254. package/src/modes/interactive/components/tool-execution.ts +946 -0
  255. package/src/modes/interactive/components/tree-selector.ts +877 -0
  256. package/src/modes/interactive/components/ttsr-notification.ts +82 -0
  257. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  258. package/src/modes/interactive/components/user-message.ts +18 -0
  259. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  260. package/src/modes/interactive/components/welcome.ts +228 -0
  261. package/src/modes/interactive/interactive-mode.ts +2669 -0
  262. package/src/modes/interactive/theme/dark.json +102 -0
  263. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  264. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  265. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  266. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  267. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  268. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  269. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  270. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  271. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  272. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  273. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  274. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  275. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  276. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  277. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  278. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  279. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  280. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  281. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  282. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  283. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  284. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  285. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  286. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  287. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  288. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  289. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  290. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  291. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  292. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  293. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  294. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  295. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  296. package/src/modes/interactive/theme/light.json +99 -0
  297. package/src/modes/interactive/theme/theme-schema.json +424 -0
  298. package/src/modes/interactive/theme/theme.ts +2211 -0
  299. package/src/modes/print-mode.ts +163 -0
  300. package/src/modes/rpc/rpc-client.ts +527 -0
  301. package/src/modes/rpc/rpc-mode.ts +494 -0
  302. package/src/modes/rpc/rpc-types.ts +203 -0
  303. package/src/prompts/architect-plan.md +10 -0
  304. package/src/prompts/branch-summary-preamble.md +3 -0
  305. package/src/prompts/branch-summary.md +28 -0
  306. package/src/prompts/browser.md +71 -0
  307. package/src/prompts/compaction-summary.md +34 -0
  308. package/src/prompts/compaction-turn-prefix.md +16 -0
  309. package/src/prompts/compaction-update-summary.md +41 -0
  310. package/src/prompts/explore.md +82 -0
  311. package/src/prompts/implement-with-critic.md +11 -0
  312. package/src/prompts/implement.md +11 -0
  313. package/src/prompts/init.md +30 -0
  314. package/src/prompts/plan.md +54 -0
  315. package/src/prompts/reviewer.md +81 -0
  316. package/src/prompts/summarization-system.md +3 -0
  317. package/src/prompts/system-prompt.md +27 -0
  318. package/src/prompts/task.md +56 -0
  319. package/src/prompts/title-system.md +8 -0
  320. package/src/prompts/tools/ask.md +24 -0
  321. package/src/prompts/tools/bash.md +23 -0
  322. package/src/prompts/tools/edit.md +9 -0
  323. package/src/prompts/tools/find.md +6 -0
  324. package/src/prompts/tools/grep.md +12 -0
  325. package/src/prompts/tools/lsp.md +14 -0
  326. package/src/prompts/tools/output.md +23 -0
  327. package/src/prompts/tools/read.md +25 -0
  328. package/src/prompts/tools/web-fetch.md +8 -0
  329. package/src/prompts/tools/web-search.md +10 -0
  330. package/src/prompts/tools/write.md +10 -0
  331. package/src/utils/changelog.ts +99 -0
  332. package/src/utils/clipboard.ts +265 -0
  333. package/src/utils/fuzzy.ts +108 -0
  334. package/src/utils/mime.ts +30 -0
  335. package/src/utils/shell-snapshot.ts +218 -0
  336. package/src/utils/shell.ts +364 -0
  337. package/src/utils/tools-manager.ts +265 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Shared utilities for compaction and branch summarization.
3
+ */
4
+
5
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
+ import type { Message } from "@oh-my-pi/pi-ai";
7
+ import summarizationSystemPrompt from "../../prompts/summarization-system.md" with { type: "text" };
8
+
9
+ // ============================================================================
10
+ // File Operation Tracking
11
+ // ============================================================================
12
+
13
+ export interface FileOperations {
14
+ read: Set<string>;
15
+ written: Set<string>;
16
+ edited: Set<string>;
17
+ }
18
+
19
+ export function createFileOps(): FileOperations {
20
+ return {
21
+ read: new Set(),
22
+ written: new Set(),
23
+ edited: new Set(),
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Extract file operations from tool calls in an assistant message.
29
+ */
30
+ export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void {
31
+ if (message.role !== "assistant") return;
32
+ if (!("content" in message) || !Array.isArray(message.content)) return;
33
+
34
+ for (const block of message.content) {
35
+ if (typeof block !== "object" || block === null) continue;
36
+ if (!("type" in block) || block.type !== "toolCall") continue;
37
+ if (!("arguments" in block) || !("name" in block)) continue;
38
+
39
+ const args = block.arguments as Record<string, unknown> | undefined;
40
+ if (!args) continue;
41
+
42
+ const path = typeof args.path === "string" ? args.path : undefined;
43
+ if (!path) continue;
44
+
45
+ switch (block.name) {
46
+ case "read":
47
+ fileOps.read.add(path);
48
+ break;
49
+ case "write":
50
+ fileOps.written.add(path);
51
+ break;
52
+ case "edit":
53
+ fileOps.edited.add(path);
54
+ break;
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Compute final file lists from file operations.
61
+ * Returns readFiles (files only read, not modified) and modifiedFiles.
62
+ */
63
+ export function computeFileLists(fileOps: FileOperations): { readFiles: string[]; modifiedFiles: string[] } {
64
+ const modified = new Set([...fileOps.edited, ...fileOps.written]);
65
+ const readOnly = [...fileOps.read].filter((f) => !modified.has(f)).sort();
66
+ const modifiedFiles = [...modified].sort();
67
+ return { readFiles: readOnly, modifiedFiles };
68
+ }
69
+
70
+ /**
71
+ * Format file operations as XML tags for summary.
72
+ */
73
+ export function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string {
74
+ const sections: string[] = [];
75
+ if (readFiles.length > 0) {
76
+ sections.push(`<read-files>\n${readFiles.join("\n")}\n</read-files>`);
77
+ }
78
+ if (modifiedFiles.length > 0) {
79
+ sections.push(`<modified-files>\n${modifiedFiles.join("\n")}\n</modified-files>`);
80
+ }
81
+ if (sections.length === 0) return "";
82
+ return `\n\n${sections.join("\n\n")}`;
83
+ }
84
+
85
+ // ============================================================================
86
+ // Message Serialization
87
+ // ============================================================================
88
+
89
+ /**
90
+ * Serialize LLM messages to text for summarization.
91
+ * This prevents the model from treating it as a conversation to continue.
92
+ * Call convertToLlm() first to handle custom message types.
93
+ */
94
+ export function serializeConversation(messages: Message[]): string {
95
+ const parts: string[] = [];
96
+
97
+ for (const msg of messages) {
98
+ if (msg.role === "user") {
99
+ const content =
100
+ typeof msg.content === "string"
101
+ ? msg.content
102
+ : msg.content
103
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
104
+ .map((c) => c.text)
105
+ .join("");
106
+ if (content) parts.push(`[User]: ${content}`);
107
+ } else if (msg.role === "assistant") {
108
+ const textParts: string[] = [];
109
+ const thinkingParts: string[] = [];
110
+ const toolCalls: string[] = [];
111
+
112
+ for (const block of msg.content) {
113
+ if (block.type === "text") {
114
+ textParts.push(block.text);
115
+ } else if (block.type === "thinking") {
116
+ thinkingParts.push(block.thinking);
117
+ } else if (block.type === "toolCall") {
118
+ const args = block.arguments as Record<string, unknown>;
119
+ const argsStr = Object.entries(args)
120
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
121
+ .join(", ");
122
+ toolCalls.push(`${block.name}(${argsStr})`);
123
+ }
124
+ }
125
+
126
+ if (thinkingParts.length > 0) {
127
+ parts.push(`[Assistant thinking]: ${thinkingParts.join("\n")}`);
128
+ }
129
+ if (textParts.length > 0) {
130
+ parts.push(`[Assistant]: ${textParts.join("\n")}`);
131
+ }
132
+ if (toolCalls.length > 0) {
133
+ parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
134
+ }
135
+ } else if (msg.role === "toolResult") {
136
+ const content = msg.content
137
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
138
+ .map((c) => c.text)
139
+ .join("");
140
+ if (content) {
141
+ parts.push(`[Tool result]: ${content}`);
142
+ }
143
+ }
144
+ }
145
+
146
+ return parts.join("\n\n");
147
+ }
148
+
149
+ // ============================================================================
150
+ // Summarization System Prompt
151
+ // ============================================================================
152
+
153
+ export const SUMMARIZATION_SYSTEM_PROMPT = summarizationSystemPrompt;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * /review command - Interactive code review launcher
3
+ *
4
+ * Provides a menu to select review mode:
5
+ * 1. Review against a base branch (PR style)
6
+ * 2. Review uncommitted changes
7
+ * 3. Review a specific commit
8
+ * 4. Custom review instructions
9
+ */
10
+
11
+ import type { HookCommandContext } from "../../../hooks/types";
12
+ import type { CustomCommand, CustomCommandAPI } from "../../types";
13
+
14
+ export function createReviewCommand(api: CustomCommandAPI): CustomCommand {
15
+ return {
16
+ name: "review",
17
+ description: "Launch interactive code review",
18
+
19
+ async execute(_args: string[], ctx: HookCommandContext): Promise<string | undefined> {
20
+ if (!ctx.hasUI) {
21
+ return "Use the Task tool to run the 'reviewer' agent to review recent code changes.";
22
+ }
23
+
24
+ // Main menu
25
+ const mode = await ctx.ui.select("Review Mode", [
26
+ "1. Review against a base branch (PR Style)",
27
+ "2. Review uncommitted changes",
28
+ "3. Review a specific commit",
29
+ "4. Custom review instructions",
30
+ ]);
31
+
32
+ if (!mode) return undefined;
33
+
34
+ const modeNum = parseInt(mode[0], 10);
35
+
36
+ switch (modeNum) {
37
+ case 1: {
38
+ // PR-style review against base branch
39
+ const branches = await getGitBranches(api);
40
+ if (branches.length === 0) {
41
+ ctx.ui.notify("No git branches found", "error");
42
+ return undefined;
43
+ }
44
+
45
+ const baseBranch = await ctx.ui.select("Select base branch to compare against", branches);
46
+ if (!baseBranch) return undefined;
47
+
48
+ const currentBranch = await getCurrentBranch(api);
49
+ return `Use the Task tool to run the "reviewer" agent with this task:
50
+
51
+ Review the changes between "${baseBranch}" and "${currentBranch}".
52
+
53
+ Run \`git diff ${baseBranch}...${currentBranch}\` to see the changes, then analyze the modified files.`;
54
+ }
55
+
56
+ case 2: {
57
+ // Uncommitted changes
58
+ const status = await getGitStatus(api);
59
+ if (!status.trim()) {
60
+ ctx.ui.notify("No uncommitted changes found", "warning");
61
+ return undefined;
62
+ }
63
+
64
+ return `Use the Task tool to run the "reviewer" agent with this task:
65
+
66
+ Review all uncommitted changes in the working directory.
67
+
68
+ Run \`git diff\` for unstaged changes and \`git diff --cached\` for staged changes.`;
69
+ }
70
+
71
+ case 3: {
72
+ // Specific commit
73
+ const commits = await getRecentCommits(api, 20);
74
+ if (commits.length === 0) {
75
+ ctx.ui.notify("No commits found", "error");
76
+ return undefined;
77
+ }
78
+
79
+ const selected = await ctx.ui.select("Select commit to review", commits);
80
+ if (!selected) return undefined;
81
+
82
+ // Extract commit hash from selection (format: "abc1234 message")
83
+ const hash = selected.split(" ")[0];
84
+
85
+ return `Use the Task tool to run the "reviewer" agent with this task:
86
+
87
+ Review commit ${hash}.
88
+
89
+ Run \`git show ${hash}\` to see the changes introduced by this commit.`;
90
+ }
91
+
92
+ case 4: {
93
+ // Custom instructions
94
+ const instructions = await ctx.ui.editor(
95
+ "Enter custom review instructions",
96
+ "Review the following:\n\n",
97
+ );
98
+ if (!instructions?.trim()) return undefined;
99
+
100
+ return `Use the Task tool to run the "reviewer" agent with this task:
101
+
102
+ ${instructions}`;
103
+ }
104
+
105
+ default:
106
+ return undefined;
107
+ }
108
+ },
109
+ };
110
+ }
111
+
112
+ async function getGitBranches(api: CustomCommandAPI): Promise<string[]> {
113
+ try {
114
+ const result = await api.exec("git", ["branch", "-a", "--format=%(refname:short)"]);
115
+ if (result.code !== 0) return [];
116
+ return result.stdout
117
+ .split("\n")
118
+ .map((b) => b.trim())
119
+ .filter(Boolean);
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
124
+
125
+ async function getCurrentBranch(api: CustomCommandAPI): Promise<string> {
126
+ try {
127
+ const result = await api.exec("git", ["branch", "--show-current"]);
128
+ return result.stdout.trim() || "HEAD";
129
+ } catch {
130
+ return "HEAD";
131
+ }
132
+ }
133
+
134
+ async function getGitStatus(api: CustomCommandAPI): Promise<string> {
135
+ try {
136
+ const result = await api.exec("git", ["status", "--porcelain"]);
137
+ return result.stdout;
138
+ } catch {
139
+ return "";
140
+ }
141
+ }
142
+
143
+ async function getRecentCommits(api: CustomCommandAPI, count: number): Promise<string[]> {
144
+ try {
145
+ const result = await api.exec("git", ["log", `-${count}`, "--oneline", "--no-decorate"]);
146
+ if (result.code !== 0) return [];
147
+ return result.stdout
148
+ .split("\n")
149
+ .map((c) => c.trim())
150
+ .filter(Boolean);
151
+ } catch {
152
+ return [];
153
+ }
154
+ }
155
+
156
+ export default createReviewCommand;
@@ -0,0 +1,15 @@
1
+ export {
2
+ type DiscoverCustomCommandsOptions,
3
+ type DiscoverCustomCommandsResult,
4
+ discoverCustomCommands,
5
+ type LoadCustomCommandsOptions,
6
+ loadCustomCommands,
7
+ } from "./loader";
8
+ export type {
9
+ CustomCommand,
10
+ CustomCommandAPI,
11
+ CustomCommandFactory,
12
+ CustomCommandSource,
13
+ CustomCommandsLoadResult,
14
+ LoadedCustomCommand,
15
+ } from "./types";
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Custom command loader - loads TypeScript command modules using native Bun import.
3
+ *
4
+ * Dependencies (@sinclair/typebox and pi-coding-agent) are injected via the CustomCommandAPI
5
+ * to avoid import resolution issues with custom commands loaded from user directories.
6
+ */
7
+
8
+ import { type Dirent, existsSync, readdirSync } from "node:fs";
9
+ import * as path from "node:path";
10
+ import * as typebox from "@sinclair/typebox";
11
+ import { getAgentDir, getConfigDirs } from "../../config";
12
+ import * as piCodingAgent from "../../index";
13
+ import { execCommand } from "../exec";
14
+ import { createReviewCommand } from "./bundled/review";
15
+ import type {
16
+ CustomCommand,
17
+ CustomCommandAPI,
18
+ CustomCommandFactory,
19
+ CustomCommandSource,
20
+ CustomCommandsLoadResult,
21
+ LoadedCustomCommand,
22
+ } from "./types";
23
+
24
+ /**
25
+ * Load a single command module using native Bun import.
26
+ */
27
+ async function loadCommandModule(
28
+ commandPath: string,
29
+ _cwd: string,
30
+ sharedApi: CustomCommandAPI,
31
+ ): Promise<{ commands: CustomCommand[] | null; error: string | null }> {
32
+ try {
33
+ const module = await import(commandPath);
34
+ const factory = (module.default ?? module) as CustomCommandFactory;
35
+
36
+ if (typeof factory !== "function") {
37
+ return { commands: null, error: "Command must export a default function" };
38
+ }
39
+
40
+ const result = await factory(sharedApi);
41
+ const commands = Array.isArray(result) ? result : [result];
42
+
43
+ // Validate commands
44
+ for (const cmd of commands) {
45
+ if (!cmd.name || typeof cmd.name !== "string") {
46
+ return { commands: null, error: "Command must have a name" };
47
+ }
48
+ if (!cmd.description || typeof cmd.description !== "string") {
49
+ return { commands: null, error: `Command "${cmd.name}" must have a description` };
50
+ }
51
+ if (typeof cmd.execute !== "function") {
52
+ return { commands: null, error: `Command "${cmd.name}" must have an execute function` };
53
+ }
54
+ }
55
+
56
+ return { commands, error: null };
57
+ } catch (err) {
58
+ const message = err instanceof Error ? err.message : String(err);
59
+ return { commands: null, error: `Failed to load command: ${message}` };
60
+ }
61
+ }
62
+
63
+ export interface DiscoverCustomCommandsOptions {
64
+ /** Current working directory. Default: process.cwd() */
65
+ cwd?: string;
66
+ /** Agent config directory. Default: from getAgentDir() */
67
+ agentDir?: string;
68
+ }
69
+
70
+ export interface DiscoverCustomCommandsResult {
71
+ /** Paths to command modules */
72
+ paths: Array<{ path: string; source: CustomCommandSource }>;
73
+ }
74
+
75
+ /**
76
+ * Discover custom command modules (TypeScript slash commands).
77
+ * Markdown slash commands are handled by core/slash-commands.ts.
78
+ */
79
+ export function discoverCustomCommands(options: DiscoverCustomCommandsOptions = {}): DiscoverCustomCommandsResult {
80
+ const cwd = options.cwd ?? process.cwd();
81
+ const agentDir = options.agentDir ?? getAgentDir();
82
+ const paths: Array<{ path: string; source: CustomCommandSource }> = [];
83
+ const seen = new Set<string>();
84
+
85
+ const addPath = (commandPath: string, source: CustomCommandSource): void => {
86
+ const resolved = path.resolve(commandPath);
87
+ if (seen.has(resolved)) return;
88
+ seen.add(resolved);
89
+ paths.push({ path: resolved, source });
90
+ };
91
+
92
+ const commandDirs: Array<{ path: string; source: CustomCommandSource }> = [];
93
+ if (agentDir) {
94
+ const userCommandsDir = path.join(agentDir, "commands");
95
+ if (existsSync(userCommandsDir)) {
96
+ commandDirs.push({ path: userCommandsDir, source: "user" });
97
+ }
98
+ }
99
+
100
+ for (const entry of getConfigDirs("commands", { cwd, existingOnly: true })) {
101
+ const source = entry.level === "user" ? "user" : "project";
102
+ if (!commandDirs.some((d) => d.path === entry.path)) {
103
+ commandDirs.push({ path: entry.path, source });
104
+ }
105
+ }
106
+
107
+ const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
108
+ for (const { path: commandsDir, source } of commandDirs) {
109
+ let entries: Dirent[];
110
+ try {
111
+ entries = readdirSync(commandsDir, { withFileTypes: true });
112
+ } catch {
113
+ continue;
114
+ }
115
+ for (const entry of entries) {
116
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
117
+ const commandDir = path.join(commandsDir, entry.name);
118
+
119
+ for (const filename of indexCandidates) {
120
+ const candidate = path.join(commandDir, filename);
121
+ if (existsSync(candidate)) {
122
+ addPath(candidate, source);
123
+ break;
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ return { paths };
130
+ }
131
+
132
+ export interface LoadCustomCommandsOptions {
133
+ /** Current working directory. Default: process.cwd() */
134
+ cwd?: string;
135
+ /** Agent config directory. Default: from getAgentDir() */
136
+ agentDir?: string;
137
+ }
138
+
139
+ /**
140
+ * Load bundled commands (shipped with pi-coding-agent).
141
+ */
142
+ function loadBundledCommands(sharedApi: CustomCommandAPI): LoadedCustomCommand[] {
143
+ const bundled: LoadedCustomCommand[] = [];
144
+
145
+ // Add bundled commands here
146
+ const reviewCommand = createReviewCommand(sharedApi);
147
+ bundled.push({
148
+ path: "bundled:review",
149
+ resolvedPath: "bundled:review",
150
+ command: reviewCommand,
151
+ source: "bundled",
152
+ });
153
+
154
+ return bundled;
155
+ }
156
+
157
+ /**
158
+ * Discover and load custom commands from standard locations.
159
+ */
160
+ export async function loadCustomCommands(options: LoadCustomCommandsOptions = {}): Promise<CustomCommandsLoadResult> {
161
+ const cwd = options.cwd ?? process.cwd();
162
+ const agentDir = options.agentDir ?? getAgentDir();
163
+
164
+ const { paths } = discoverCustomCommands({ cwd, agentDir });
165
+
166
+ const commands: LoadedCustomCommand[] = [];
167
+ const errors: Array<{ path: string; error: string }> = [];
168
+ const seenNames = new Set<string>();
169
+
170
+ // Shared API object - all commands get the same instance
171
+ const sharedApi: CustomCommandAPI = {
172
+ cwd,
173
+ exec: (command: string, args: string[], execOptions) =>
174
+ execCommand(command, args, execOptions?.cwd ?? cwd, execOptions),
175
+ typebox,
176
+ pi: piCodingAgent,
177
+ };
178
+
179
+ // 1. Load bundled commands first (lowest priority - can be overridden)
180
+ for (const loaded of loadBundledCommands(sharedApi)) {
181
+ seenNames.add(loaded.command.name);
182
+ commands.push(loaded);
183
+ }
184
+
185
+ // 2. Load user/project commands (can override bundled)
186
+ for (const { path: commandPath, source } of paths) {
187
+ const { commands: loadedCommands, error } = await loadCommandModule(commandPath, cwd, sharedApi);
188
+
189
+ if (error) {
190
+ errors.push({ path: commandPath, error });
191
+ continue;
192
+ }
193
+
194
+ if (loadedCommands) {
195
+ for (const command of loadedCommands) {
196
+ // Allow overriding bundled commands, but not user/project conflicts
197
+ const existingIdx = commands.findIndex((c) => c.command.name === command.name);
198
+ if (existingIdx !== -1) {
199
+ const existing = commands[existingIdx];
200
+ if (existing.source === "bundled") {
201
+ // Override bundled command
202
+ commands.splice(existingIdx, 1);
203
+ seenNames.delete(command.name);
204
+ } else {
205
+ // Conflict between user/project commands
206
+ errors.push({
207
+ path: commandPath,
208
+ error: `Command name "${command.name}" conflicts with existing command`,
209
+ });
210
+ continue;
211
+ }
212
+ }
213
+
214
+ seenNames.add(command.name);
215
+ commands.push({
216
+ path: commandPath,
217
+ resolvedPath: path.resolve(commandPath),
218
+ command,
219
+ source,
220
+ });
221
+ }
222
+ }
223
+ }
224
+
225
+ return { commands, errors };
226
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Custom command types.
3
+ *
4
+ * Custom commands are TypeScript modules that define executable slash commands.
5
+ * Unlike markdown commands which expand to prompts, custom commands can execute
6
+ * arbitrary logic with full access to the hook context.
7
+ */
8
+
9
+ import type { ExecOptions, ExecResult, HookCommandContext } from "../hooks/types";
10
+
11
+ // Re-export for custom commands to use
12
+ export type { ExecOptions, ExecResult, HookCommandContext };
13
+
14
+ /**
15
+ * API passed to custom command factory.
16
+ * Similar to HookAPI but focused on command needs.
17
+ */
18
+ export interface CustomCommandAPI {
19
+ /** Current working directory */
20
+ cwd: string;
21
+ /** Execute a shell command */
22
+ exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
23
+ /** Injected @sinclair/typebox module */
24
+ typebox: typeof import("@sinclair/typebox");
25
+ /** Injected pi-coding-agent exports */
26
+ pi: typeof import("../../index.js");
27
+ }
28
+
29
+ /**
30
+ * Custom command definition.
31
+ *
32
+ * Commands can either:
33
+ * - Return a string to be sent to the LLM as a prompt
34
+ * - Return void/undefined to do nothing (fire-and-forget)
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const factory: CustomCommandFactory = (pi) => ({
39
+ * name: "deploy",
40
+ * description: "Deploy current branch to staging",
41
+ * async execute(args, ctx) {
42
+ * const env = args[0] || "staging";
43
+ * const confirmed = await ctx.ui.confirm("Deploy", `Deploy to ${env}?`);
44
+ * if (!confirmed) return;
45
+ *
46
+ * const result = await pi.exec("./deploy.sh", [env]);
47
+ * if (result.exitCode !== 0) {
48
+ * ctx.ui.notify(`Deploy failed: ${result.stderr}`, "error");
49
+ * return;
50
+ * }
51
+ *
52
+ * ctx.ui.notify("Deploy successful!", "info");
53
+ * // No return = no prompt sent to LLM
54
+ * }
55
+ * });
56
+ * ```
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * // Return a prompt to send to the LLM
61
+ * const factory: CustomCommandFactory = (pi) => ({
62
+ * name: "git:status",
63
+ * description: "Show git status and suggest actions",
64
+ * async execute(args, ctx) {
65
+ * const result = await pi.exec("git", ["status", "--porcelain"]);
66
+ * return `Here's the git status:\n\`\`\`\n${result.stdout}\`\`\`\nSuggest what to do next.`;
67
+ * }
68
+ * });
69
+ * ```
70
+ */
71
+ export interface CustomCommand {
72
+ /** Command name (can include namespace like "git:commit") */
73
+ name: string;
74
+ /** Description shown in command autocomplete */
75
+ description: string;
76
+ /**
77
+ * Execute the command.
78
+ * @param args - Parsed command arguments
79
+ * @param ctx - Command context with UI and session control
80
+ * @returns String to send as prompt, or void for fire-and-forget
81
+ */
82
+ execute(args: string[], ctx: HookCommandContext): Promise<string | undefined> | string | undefined;
83
+ }
84
+
85
+ /**
86
+ * Factory function that creates custom command(s).
87
+ * Can return a single command or an array of commands.
88
+ */
89
+ export type CustomCommandFactory = (
90
+ api: CustomCommandAPI,
91
+ ) => CustomCommand | CustomCommand[] | Promise<CustomCommand | CustomCommand[]>;
92
+
93
+ /** Source of a loaded custom command */
94
+ export type CustomCommandSource = "bundled" | "user" | "project";
95
+
96
+ /** Loaded custom command with metadata */
97
+ export interface LoadedCustomCommand {
98
+ /** Original path to the command module */
99
+ path: string;
100
+ /** Resolved absolute path */
101
+ resolvedPath: string;
102
+ /** The command definition */
103
+ command: CustomCommand;
104
+ /** Where the command was loaded from */
105
+ source: CustomCommandSource;
106
+ }
107
+
108
+ /** Result from loading custom commands */
109
+ export interface CustomCommandsLoadResult {
110
+ commands: LoadedCustomCommand[];
111
+ errors: Array<{ path: string; error: string }>;
112
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Custom tools module.
3
+ */
4
+
5
+ export { discoverAndLoadCustomTools, loadCustomTools } from "./loader";
6
+ export type {
7
+ AgentToolResult,
8
+ AgentToolUpdateCallback,
9
+ CustomTool,
10
+ CustomToolAPI,
11
+ CustomToolContext,
12
+ CustomToolFactory,
13
+ CustomToolResult,
14
+ CustomToolSessionEvent,
15
+ CustomToolsLoadResult,
16
+ CustomToolUIContext,
17
+ ExecResult,
18
+ LoadedCustomTool,
19
+ RenderResultOptions,
20
+ ToolLoadError,
21
+ } from "./types";
22
+ export { wrapCustomTool, wrapCustomTools } from "./wrapper";