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