@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,494 @@
1
+ /**
2
+ * RPC mode: Headless operation with JSON stdin/stdout protocol.
3
+ *
4
+ * Used for embedding the agent in other applications.
5
+ * Receives commands as JSON on stdin, outputs events and responses as JSON on stdout.
6
+ *
7
+ * Protocol:
8
+ * - Commands: JSON objects with `type` field, optional `id` for correlation
9
+ * - Responses: JSON objects with `type: "response"`, `command`, `success`, and optional `data`/`error`
10
+ * - Events: AgentSessionEvent objects streamed as they occur
11
+ * - Hook UI: Hook UI requests are emitted, client responds with hook_ui_response
12
+ */
13
+
14
+ import { nanoid } from "nanoid";
15
+ import type { AgentSession } from "../../core/agent-session";
16
+ import type { HookUIContext } from "../../core/hooks/index";
17
+ import { logger } from "../../core/logger";
18
+ import { theme } from "../interactive/theme/theme";
19
+ import type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types";
20
+
21
+ // Re-export types for consumers
22
+ export type { RpcCommand, RpcHookUIRequest, RpcHookUIResponse, RpcResponse, RpcSessionState } from "./rpc-types";
23
+
24
+ /**
25
+ * Run in RPC mode.
26
+ * Listens for JSON commands on stdin, outputs events and responses on stdout.
27
+ */
28
+ export async function runRpcMode(session: AgentSession): Promise<never> {
29
+ const output = (obj: RpcResponse | RpcHookUIRequest | object) => {
30
+ console.log(JSON.stringify(obj));
31
+ };
32
+
33
+ const success = <T extends RpcCommand["type"]>(
34
+ id: string | undefined,
35
+ command: T,
36
+ data?: object | null,
37
+ ): RpcResponse => {
38
+ if (data === undefined) {
39
+ return { id, type: "response", command, success: true } as RpcResponse;
40
+ }
41
+ return { id, type: "response", command, success: true, data } as RpcResponse;
42
+ };
43
+
44
+ const error = (id: string | undefined, command: string, message: string): RpcResponse => {
45
+ return { id, type: "response", command, success: false, error: message };
46
+ };
47
+
48
+ // Pending hook UI requests waiting for response
49
+ const pendingHookRequests = new Map<string, { resolve: (value: any) => void; reject: (error: Error) => void }>();
50
+
51
+ /**
52
+ * Create a hook UI context that uses the RPC protocol.
53
+ */
54
+ const createHookUIContext = (): HookUIContext => ({
55
+ async select(title: string, options: string[]): Promise<string | undefined> {
56
+ const id = nanoid();
57
+ return new Promise((resolve, reject) => {
58
+ pendingHookRequests.set(id, {
59
+ resolve: (response: RpcHookUIResponse) => {
60
+ if ("cancelled" in response && response.cancelled) {
61
+ resolve(undefined);
62
+ } else if ("value" in response) {
63
+ resolve(response.value);
64
+ } else {
65
+ resolve(undefined);
66
+ }
67
+ },
68
+ reject,
69
+ });
70
+ output({ type: "hook_ui_request", id, method: "select", title, options } as RpcHookUIRequest);
71
+ });
72
+ },
73
+
74
+ async confirm(title: string, message: string): Promise<boolean> {
75
+ const id = nanoid();
76
+ return new Promise((resolve, reject) => {
77
+ pendingHookRequests.set(id, {
78
+ resolve: (response: RpcHookUIResponse) => {
79
+ if ("cancelled" in response && response.cancelled) {
80
+ resolve(false);
81
+ } else if ("confirmed" in response) {
82
+ resolve(response.confirmed);
83
+ } else {
84
+ resolve(false);
85
+ }
86
+ },
87
+ reject,
88
+ });
89
+ output({ type: "hook_ui_request", id, method: "confirm", title, message } as RpcHookUIRequest);
90
+ });
91
+ },
92
+
93
+ async input(title: string, placeholder?: string): Promise<string | undefined> {
94
+ const id = nanoid();
95
+ return new Promise((resolve, reject) => {
96
+ pendingHookRequests.set(id, {
97
+ resolve: (response: RpcHookUIResponse) => {
98
+ if ("cancelled" in response && response.cancelled) {
99
+ resolve(undefined);
100
+ } else if ("value" in response) {
101
+ resolve(response.value);
102
+ } else {
103
+ resolve(undefined);
104
+ }
105
+ },
106
+ reject,
107
+ });
108
+ output({ type: "hook_ui_request", id, method: "input", title, placeholder } as RpcHookUIRequest);
109
+ });
110
+ },
111
+
112
+ notify(message: string, type?: "info" | "warning" | "error"): void {
113
+ // Fire and forget - no response needed
114
+ output({
115
+ type: "hook_ui_request",
116
+ id: nanoid(),
117
+ method: "notify",
118
+ message,
119
+ notifyType: type,
120
+ } as RpcHookUIRequest);
121
+ },
122
+
123
+ setStatus(key: string, text: string | undefined): void {
124
+ // Fire and forget - no response needed
125
+ output({
126
+ type: "hook_ui_request",
127
+ id: nanoid(),
128
+ method: "setStatus",
129
+ statusKey: key,
130
+ statusText: text,
131
+ } as RpcHookUIRequest);
132
+ },
133
+
134
+ async custom() {
135
+ // Custom UI not supported in RPC mode
136
+ return undefined as never;
137
+ },
138
+
139
+ setEditorText(text: string): void {
140
+ // Fire and forget - host can implement editor control
141
+ output({
142
+ type: "hook_ui_request",
143
+ id: nanoid(),
144
+ method: "set_editor_text",
145
+ text,
146
+ } as RpcHookUIRequest);
147
+ },
148
+
149
+ getEditorText(): string {
150
+ // Synchronous method can't wait for RPC response
151
+ // Host should track editor state locally if needed
152
+ return "";
153
+ },
154
+
155
+ async editor(title: string, prefill?: string): Promise<string | undefined> {
156
+ const id = nanoid();
157
+ return new Promise((resolve, reject) => {
158
+ pendingHookRequests.set(id, {
159
+ resolve: (response: RpcHookUIResponse) => {
160
+ if ("cancelled" in response && response.cancelled) {
161
+ resolve(undefined);
162
+ } else if ("value" in response) {
163
+ resolve(response.value);
164
+ } else {
165
+ resolve(undefined);
166
+ }
167
+ },
168
+ reject,
169
+ });
170
+ output({ type: "hook_ui_request", id, method: "editor", title, prefill } as RpcHookUIRequest);
171
+ });
172
+ },
173
+
174
+ get theme() {
175
+ return theme;
176
+ },
177
+ });
178
+
179
+ // Set up hooks with RPC-based UI context
180
+ const hookRunner = session.hookRunner;
181
+ if (hookRunner) {
182
+ hookRunner.initialize({
183
+ getModel: () => session.agent.state.model,
184
+ sendMessageHandler: (message, triggerTurn) => {
185
+ session.sendHookMessage(message, triggerTurn).catch((e) => {
186
+ output(error(undefined, "hook_send", e.message));
187
+ });
188
+ },
189
+ appendEntryHandler: (customType, data) => {
190
+ session.sessionManager.appendCustomEntry(customType, data);
191
+ },
192
+ uiContext: createHookUIContext(),
193
+ hasUI: false,
194
+ });
195
+ hookRunner.onError((err) => {
196
+ output({ type: "hook_error", hookPath: err.hookPath, event: err.event, error: err.error });
197
+ });
198
+ // Emit session_start event
199
+ await hookRunner.emit({
200
+ type: "session_start",
201
+ });
202
+ }
203
+
204
+ // Emit session start event to custom tools
205
+ // Note: Tools get no-op UI context in RPC mode (host handles UI via protocol)
206
+ for (const { tool } of session.customTools) {
207
+ if (tool.onSession) {
208
+ try {
209
+ await tool.onSession(
210
+ {
211
+ previousSessionFile: undefined,
212
+ reason: "start",
213
+ },
214
+ {
215
+ sessionManager: session.sessionManager,
216
+ modelRegistry: session.modelRegistry,
217
+ model: session.model,
218
+ isIdle: () => !session.isStreaming,
219
+ hasQueuedMessages: () => session.queuedMessageCount > 0,
220
+ abort: () => {
221
+ session.abort();
222
+ },
223
+ },
224
+ );
225
+ } catch (err) {
226
+ logger.warn("Tool onSession error", { error: String(err) });
227
+ }
228
+ }
229
+ }
230
+
231
+ // Output all agent events as JSON
232
+ session.subscribe((event) => {
233
+ output(event);
234
+ });
235
+
236
+ // Serialize prompt commands to prevent concurrent execution
237
+ let activePrompt: Promise<void> | null = null;
238
+
239
+ // Handle a single command
240
+ const handleCommand = async (command: RpcCommand): Promise<RpcResponse> => {
241
+ const id = command.id;
242
+
243
+ switch (command.type) {
244
+ // =================================================================
245
+ // Prompting
246
+ // =================================================================
247
+
248
+ case "prompt": {
249
+ // Serialize prompts to prevent concurrent execution
250
+ if (activePrompt) {
251
+ await activePrompt;
252
+ }
253
+ activePrompt = session
254
+ .prompt(command.message, {
255
+ images: command.images,
256
+ })
257
+ .catch((e) => output(error(id, "prompt", e.message)))
258
+ .finally(() => {
259
+ activePrompt = null;
260
+ });
261
+ return success(id, "prompt");
262
+ }
263
+
264
+ case "queue_message": {
265
+ await session.queueMessage(command.message);
266
+ return success(id, "queue_message");
267
+ }
268
+
269
+ case "abort": {
270
+ await session.abort();
271
+ return success(id, "abort");
272
+ }
273
+
274
+ case "new_session": {
275
+ const options = command.parentSession ? { parentSession: command.parentSession } : undefined;
276
+ const cancelled = !(await session.newSession(options));
277
+ return success(id, "new_session", { cancelled });
278
+ }
279
+
280
+ // =================================================================
281
+ // State
282
+ // =================================================================
283
+
284
+ case "get_state": {
285
+ const state: RpcSessionState = {
286
+ model: session.model,
287
+ thinkingLevel: session.thinkingLevel,
288
+ isStreaming: session.isStreaming,
289
+ isCompacting: session.isCompacting,
290
+ queueMode: session.queueMode,
291
+ sessionFile: session.sessionFile,
292
+ sessionId: session.sessionId,
293
+ autoCompactionEnabled: session.autoCompactionEnabled,
294
+ messageCount: session.messages.length,
295
+ queuedMessageCount: session.queuedMessageCount,
296
+ };
297
+ return success(id, "get_state", state);
298
+ }
299
+
300
+ // =================================================================
301
+ // Model
302
+ // =================================================================
303
+
304
+ case "set_model": {
305
+ const models = await session.getAvailableModels();
306
+ const model = models.find((m) => m.provider === command.provider && m.id === command.modelId);
307
+ if (!model) {
308
+ return error(id, "set_model", `Model not found: ${command.provider}/${command.modelId}`);
309
+ }
310
+ await session.setModel(model);
311
+ return success(id, "set_model", model);
312
+ }
313
+
314
+ case "cycle_model": {
315
+ const result = await session.cycleModel();
316
+ if (!result) {
317
+ return success(id, "cycle_model", null);
318
+ }
319
+ return success(id, "cycle_model", result);
320
+ }
321
+
322
+ case "get_available_models": {
323
+ const models = await session.getAvailableModels();
324
+ return success(id, "get_available_models", { models });
325
+ }
326
+
327
+ // =================================================================
328
+ // Thinking
329
+ // =================================================================
330
+
331
+ case "set_thinking_level": {
332
+ session.setThinkingLevel(command.level);
333
+ return success(id, "set_thinking_level");
334
+ }
335
+
336
+ case "cycle_thinking_level": {
337
+ const level = session.cycleThinkingLevel();
338
+ if (!level) {
339
+ return success(id, "cycle_thinking_level", null);
340
+ }
341
+ return success(id, "cycle_thinking_level", { level });
342
+ }
343
+
344
+ // =================================================================
345
+ // Queue Mode
346
+ // =================================================================
347
+
348
+ case "set_queue_mode": {
349
+ session.setQueueMode(command.mode);
350
+ return success(id, "set_queue_mode");
351
+ }
352
+
353
+ // =================================================================
354
+ // Compaction
355
+ // =================================================================
356
+
357
+ case "compact": {
358
+ const result = await session.compact(command.customInstructions);
359
+ return success(id, "compact", result);
360
+ }
361
+
362
+ case "set_auto_compaction": {
363
+ session.setAutoCompactionEnabled(command.enabled);
364
+ return success(id, "set_auto_compaction");
365
+ }
366
+
367
+ // =================================================================
368
+ // Retry
369
+ // =================================================================
370
+
371
+ case "set_auto_retry": {
372
+ session.setAutoRetryEnabled(command.enabled);
373
+ return success(id, "set_auto_retry");
374
+ }
375
+
376
+ case "abort_retry": {
377
+ session.abortRetry();
378
+ return success(id, "abort_retry");
379
+ }
380
+
381
+ // =================================================================
382
+ // Bash
383
+ // =================================================================
384
+
385
+ case "bash": {
386
+ const result = await session.executeBash(command.command);
387
+ return success(id, "bash", result);
388
+ }
389
+
390
+ case "abort_bash": {
391
+ session.abortBash();
392
+ return success(id, "abort_bash");
393
+ }
394
+
395
+ // =================================================================
396
+ // Session
397
+ // =================================================================
398
+
399
+ case "get_session_stats": {
400
+ const stats = session.getSessionStats();
401
+ return success(id, "get_session_stats", stats);
402
+ }
403
+
404
+ case "export_html": {
405
+ const path = await session.exportToHtml(command.outputPath);
406
+ return success(id, "export_html", { path });
407
+ }
408
+
409
+ case "switch_session": {
410
+ const cancelled = !(await session.switchSession(command.sessionPath));
411
+ return success(id, "switch_session", { cancelled });
412
+ }
413
+
414
+ case "branch": {
415
+ const result = await session.branch(command.entryId);
416
+ return success(id, "branch", { text: result.selectedText, cancelled: result.cancelled });
417
+ }
418
+
419
+ case "get_branch_messages": {
420
+ const messages = session.getUserMessagesForBranching();
421
+ return success(id, "get_branch_messages", { messages });
422
+ }
423
+
424
+ case "get_last_assistant_text": {
425
+ const text = session.getLastAssistantText();
426
+ return success(id, "get_last_assistant_text", { text });
427
+ }
428
+
429
+ // =================================================================
430
+ // Messages
431
+ // =================================================================
432
+
433
+ case "get_messages": {
434
+ return success(id, "get_messages", { messages: session.messages });
435
+ }
436
+
437
+ default: {
438
+ const unknownCommand = command as { type: string };
439
+ return error(undefined, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);
440
+ }
441
+ }
442
+ };
443
+
444
+ // Listen for JSON input - use Bun's ReadableStream
445
+ const stdinReader = (Bun.stdin.stream() as ReadableStream<Uint8Array>)
446
+ .pipeThrough(new TextDecoderStream())
447
+ .pipeThrough(
448
+ new TransformStream({
449
+ transform(chunk, controller) {
450
+ const lines = chunk.split("\n");
451
+ for (const line of lines) {
452
+ if (line.trim()) {
453
+ controller.enqueue(line);
454
+ }
455
+ }
456
+ },
457
+ }),
458
+ )
459
+ .getReader();
460
+
461
+ // Process lines in background
462
+ (async () => {
463
+ while (true) {
464
+ const { done, value: line } = await stdinReader.read();
465
+ if (done) break;
466
+
467
+ try {
468
+ const parsed = JSON.parse(line);
469
+
470
+ // Handle hook UI responses
471
+ if (parsed.type === "hook_ui_response") {
472
+ const response = parsed as RpcHookUIResponse;
473
+ const pending = pendingHookRequests.get(response.id);
474
+ if (pending) {
475
+ // Atomic delete: remove before resolve to prevent double-resolution
476
+ pendingHookRequests.delete(response.id);
477
+ pending.resolve(response);
478
+ }
479
+ return;
480
+ }
481
+
482
+ // Handle regular commands
483
+ const command = parsed as RpcCommand;
484
+ const response = await handleCommand(command);
485
+ output(response);
486
+ } catch (e: any) {
487
+ output(error(undefined, "parse", `Failed to parse command: ${e.message}`));
488
+ }
489
+ }
490
+ })();
491
+
492
+ // Keep process alive forever
493
+ return new Promise(() => {});
494
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * RPC protocol types for headless operation.
3
+ *
4
+ * Commands are sent as JSON lines on stdin.
5
+ * Responses and events are emitted as JSON lines on stdout.
6
+ */
7
+
8
+ import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
9
+ import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
10
+ import type { SessionStats } from "../../core/agent-session";
11
+ import type { BashResult } from "../../core/bash-executor";
12
+ import type { CompactionResult } from "../../core/compaction/index";
13
+
14
+ // ============================================================================
15
+ // RPC Commands (stdin)
16
+ // ============================================================================
17
+
18
+ export type RpcCommand =
19
+ // Prompting
20
+ | { id?: string; type: "prompt"; message: string; images?: ImageContent[] }
21
+ | { id?: string; type: "queue_message"; message: string }
22
+ | { id?: string; type: "abort" }
23
+ | { id?: string; type: "new_session"; parentSession?: string }
24
+
25
+ // State
26
+ | { id?: string; type: "get_state" }
27
+
28
+ // Model
29
+ | { id?: string; type: "set_model"; provider: string; modelId: string }
30
+ | { id?: string; type: "cycle_model" }
31
+ | { id?: string; type: "get_available_models" }
32
+
33
+ // Thinking
34
+ | { id?: string; type: "set_thinking_level"; level: ThinkingLevel }
35
+ | { id?: string; type: "cycle_thinking_level" }
36
+
37
+ // Queue mode
38
+ | { id?: string; type: "set_queue_mode"; mode: "all" | "one-at-a-time" }
39
+
40
+ // Compaction
41
+ | { id?: string; type: "compact"; customInstructions?: string }
42
+ | { id?: string; type: "set_auto_compaction"; enabled: boolean }
43
+
44
+ // Retry
45
+ | { id?: string; type: "set_auto_retry"; enabled: boolean }
46
+ | { id?: string; type: "abort_retry" }
47
+
48
+ // Bash
49
+ | { id?: string; type: "bash"; command: string }
50
+ | { id?: string; type: "abort_bash" }
51
+
52
+ // Session
53
+ | { id?: string; type: "get_session_stats" }
54
+ | { id?: string; type: "export_html"; outputPath?: string }
55
+ | { id?: string; type: "switch_session"; sessionPath: string }
56
+ | { id?: string; type: "branch"; entryId: string }
57
+ | { id?: string; type: "get_branch_messages" }
58
+ | { id?: string; type: "get_last_assistant_text" }
59
+
60
+ // Messages
61
+ | { id?: string; type: "get_messages" };
62
+
63
+ // ============================================================================
64
+ // RPC State
65
+ // ============================================================================
66
+
67
+ export interface RpcSessionState {
68
+ model?: Model<any>;
69
+ thinkingLevel: ThinkingLevel;
70
+ isStreaming: boolean;
71
+ isCompacting: boolean;
72
+ queueMode: "all" | "one-at-a-time";
73
+ sessionFile?: string;
74
+ sessionId: string;
75
+ autoCompactionEnabled: boolean;
76
+ messageCount: number;
77
+ queuedMessageCount: number;
78
+ }
79
+
80
+ // ============================================================================
81
+ // RPC Responses (stdout)
82
+ // ============================================================================
83
+
84
+ // Success responses with data
85
+ export type RpcResponse =
86
+ // Prompting (async - events follow)
87
+ | { id?: string; type: "response"; command: "prompt"; success: true }
88
+ | { id?: string; type: "response"; command: "queue_message"; success: true }
89
+ | { id?: string; type: "response"; command: "abort"; success: true }
90
+ | { id?: string; type: "response"; command: "new_session"; success: true; data: { cancelled: boolean } }
91
+
92
+ // State
93
+ | { id?: string; type: "response"; command: "get_state"; success: true; data: RpcSessionState }
94
+
95
+ // Model
96
+ | {
97
+ id?: string;
98
+ type: "response";
99
+ command: "set_model";
100
+ success: true;
101
+ data: Model<any>;
102
+ }
103
+ | {
104
+ id?: string;
105
+ type: "response";
106
+ command: "cycle_model";
107
+ success: true;
108
+ data: { model: Model<any>; thinkingLevel: ThinkingLevel; isScoped: boolean } | null;
109
+ }
110
+ | {
111
+ id?: string;
112
+ type: "response";
113
+ command: "get_available_models";
114
+ success: true;
115
+ data: { models: Model<any>[] };
116
+ }
117
+
118
+ // Thinking
119
+ | { id?: string; type: "response"; command: "set_thinking_level"; success: true }
120
+ | {
121
+ id?: string;
122
+ type: "response";
123
+ command: "cycle_thinking_level";
124
+ success: true;
125
+ data: { level: ThinkingLevel } | null;
126
+ }
127
+
128
+ // Queue mode
129
+ | { id?: string; type: "response"; command: "set_queue_mode"; success: true }
130
+
131
+ // Compaction
132
+ | { id?: string; type: "response"; command: "compact"; success: true; data: CompactionResult }
133
+ | { id?: string; type: "response"; command: "set_auto_compaction"; success: true }
134
+
135
+ // Retry
136
+ | { id?: string; type: "response"; command: "set_auto_retry"; success: true }
137
+ | { id?: string; type: "response"; command: "abort_retry"; success: true }
138
+
139
+ // Bash
140
+ | { id?: string; type: "response"; command: "bash"; success: true; data: BashResult }
141
+ | { id?: string; type: "response"; command: "abort_bash"; success: true }
142
+
143
+ // Session
144
+ | { id?: string; type: "response"; command: "get_session_stats"; success: true; data: SessionStats }
145
+ | { id?: string; type: "response"; command: "export_html"; success: true; data: { path: string } }
146
+ | { id?: string; type: "response"; command: "switch_session"; success: true; data: { cancelled: boolean } }
147
+ | { id?: string; type: "response"; command: "branch"; success: true; data: { text: string; cancelled: boolean } }
148
+ | {
149
+ id?: string;
150
+ type: "response";
151
+ command: "get_branch_messages";
152
+ success: true;
153
+ data: { messages: Array<{ entryId: string; text: string }> };
154
+ }
155
+ | {
156
+ id?: string;
157
+ type: "response";
158
+ command: "get_last_assistant_text";
159
+ success: true;
160
+ data: { text: string | null };
161
+ }
162
+
163
+ // Messages
164
+ | { id?: string; type: "response"; command: "get_messages"; success: true; data: { messages: AgentMessage[] } }
165
+
166
+ // Error response (any command can fail)
167
+ | { id?: string; type: "response"; command: string; success: false; error: string };
168
+
169
+ // ============================================================================
170
+ // Hook UI Events (stdout)
171
+ // ============================================================================
172
+
173
+ /** Emitted when a hook needs user input */
174
+ export type RpcHookUIRequest =
175
+ | { type: "hook_ui_request"; id: string; method: "select"; title: string; options: string[] }
176
+ | { type: "hook_ui_request"; id: string; method: "confirm"; title: string; message: string }
177
+ | { type: "hook_ui_request"; id: string; method: "input"; title: string; placeholder?: string }
178
+ | { type: "hook_ui_request"; id: string; method: "editor"; title: string; prefill?: string }
179
+ | {
180
+ type: "hook_ui_request";
181
+ id: string;
182
+ method: "notify";
183
+ message: string;
184
+ notifyType?: "info" | "warning" | "error";
185
+ }
186
+ | { type: "hook_ui_request"; id: string; method: "setStatus"; statusKey: string; statusText: string | undefined }
187
+ | { type: "hook_ui_request"; id: string; method: "set_editor_text"; text: string };
188
+
189
+ // ============================================================================
190
+ // Hook UI Commands (stdin)
191
+ // ============================================================================
192
+
193
+ /** Response to a hook UI request */
194
+ export type RpcHookUIResponse =
195
+ | { type: "hook_ui_response"; id: string; value: string }
196
+ | { type: "hook_ui_response"; id: string; confirmed: boolean }
197
+ | { type: "hook_ui_response"; id: string; cancelled: true };
198
+
199
+ // ============================================================================
200
+ // Helper type for extracting command types
201
+ // ============================================================================
202
+
203
+ export type RpcCommandType = RpcCommand["type"];
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Explore gathers context, planner creates implementation plan (no implementation)
3
+ ---
4
+
5
+ Use the subagent tool with the chain parameter to execute this workflow:
6
+
7
+ 1. First, use the "explore" agent to find all code relevant to: $@
8
+ 2. Then, use the "planner" agent to create an implementation plan for "$@" using the context from the previous step (use {previous} placeholder)
9
+
10
+ Execute this as a chain, passing output between steps via {previous}. Do NOT implement - just return the plan.