aibroker 0.2.6 → 0.6.1

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 (170) hide show
  1. package/README.md +164 -4
  2. package/dist/adapters/iterm/core.d.ts +2 -0
  3. package/dist/adapters/iterm/core.d.ts.map +1 -1
  4. package/dist/adapters/iterm/core.js +13 -5
  5. package/dist/adapters/iterm/core.js.map +1 -1
  6. package/dist/adapters/iterm/iterm2-api.d.ts +20 -0
  7. package/dist/adapters/iterm/iterm2-api.d.ts.map +1 -0
  8. package/dist/adapters/iterm/iterm2-api.js +244 -0
  9. package/dist/adapters/iterm/iterm2-api.js.map +1 -0
  10. package/dist/adapters/iterm/sessions.d.ts.map +1 -1
  11. package/dist/adapters/iterm/sessions.js +3 -2
  12. package/dist/adapters/iterm/sessions.js.map +1 -1
  13. package/dist/adapters/kokoro/media.d.ts +2 -1
  14. package/dist/adapters/kokoro/media.d.ts.map +1 -1
  15. package/dist/adapters/kokoro/media.js +53 -5
  16. package/dist/adapters/kokoro/media.js.map +1 -1
  17. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-56).d.ts +49 -0
  18. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-56).d.ts.map +1 -0
  19. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-56).js +632 -0
  20. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-56).js.map +1 -0
  21. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-13-59).js +632 -0
  22. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-36).d.ts +49 -0
  23. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-36).d.ts.map +1 -0
  24. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-36).js +614 -0
  25. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-36).js.map +1 -0
  26. package/dist/adapters/pailot/gateway (SFConflict mnott 2026-03-06-21-15-46).js +614 -0
  27. package/dist/adapters/pailot/gateway.d.ts +48 -0
  28. package/dist/adapters/pailot/gateway.d.ts (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
  29. package/dist/adapters/pailot/gateway.d.ts (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
  30. package/dist/adapters/pailot/gateway.d.ts.map +1 -0
  31. package/dist/adapters/pailot/gateway.js +828 -0
  32. package/dist/adapters/pailot/gateway.js (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
  33. package/dist/adapters/pailot/gateway.js (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
  34. package/dist/adapters/pailot/gateway.js.map +1 -0
  35. package/dist/backend/api.d.ts +5 -1
  36. package/dist/backend/api.d.ts.map +1 -1
  37. package/dist/backend/api.js +74 -3
  38. package/dist/backend/api.js.map +1 -1
  39. package/dist/core/hybrid.d.ts +7 -0
  40. package/dist/core/hybrid.d.ts.map +1 -1
  41. package/dist/core/hybrid.js +33 -0
  42. package/dist/core/hybrid.js.map +1 -1
  43. package/dist/core/state.d.ts +3 -0
  44. package/dist/core/state.d.ts.map +1 -1
  45. package/dist/core/state.js +4 -0
  46. package/dist/core/state.js.map +1 -1
  47. package/dist/core/status-cache.d.ts +51 -0
  48. package/dist/core/status-cache.d.ts.map +1 -0
  49. package/dist/core/status-cache.js +62 -0
  50. package/dist/core/status-cache.js.map +1 -0
  51. package/dist/daemon/adapter-registry (SFConflict mnott 2026-03-06-21-15-36).d.ts +63 -0
  52. package/dist/daemon/adapter-registry (SFConflict mnott 2026-03-06-21-15-36).d.ts.map +1 -0
  53. package/dist/daemon/adapter-registry (SFConflict mnott 2026-03-06-21-15-36).js +229 -0
  54. package/dist/daemon/adapter-registry (SFConflict mnott 2026-03-06-21-15-36).js.map +1 -0
  55. package/dist/daemon/adapter-registry.d.ts +63 -0
  56. package/dist/daemon/adapter-registry.d.ts.map +1 -0
  57. package/dist/daemon/adapter-registry.js +240 -0
  58. package/dist/daemon/adapter-registry.js.map +1 -0
  59. package/dist/daemon/cli.d.ts +14 -0
  60. package/dist/daemon/cli.d.ts.map +1 -0
  61. package/dist/daemon/cli.js +150 -0
  62. package/dist/daemon/cli.js.map +1 -0
  63. package/dist/daemon/command-context.d.ts +24 -0
  64. package/dist/daemon/command-context.d.ts.map +1 -0
  65. package/dist/daemon/command-context.js +13 -0
  66. package/dist/daemon/command-context.js.map +1 -0
  67. package/dist/daemon/commands.d.ts +22 -0
  68. package/dist/daemon/commands.d.ts.map +1 -0
  69. package/dist/daemon/commands.js +632 -0
  70. package/dist/daemon/commands.js.map +1 -0
  71. package/dist/daemon/core-handlers.d.ts +24 -0
  72. package/dist/daemon/core-handlers.d.ts.map +1 -0
  73. package/dist/daemon/core-handlers.js +640 -0
  74. package/dist/daemon/core-handlers.js.map +1 -0
  75. package/dist/daemon/create-adapter.d.ts +22 -0
  76. package/dist/daemon/create-adapter.d.ts.map +1 -0
  77. package/dist/daemon/create-adapter.js +153 -0
  78. package/dist/daemon/create-adapter.js.map +1 -0
  79. package/dist/daemon/image-gen.d.ts +28 -0
  80. package/dist/daemon/image-gen.d.ts.map +1 -0
  81. package/dist/daemon/image-gen.js +97 -0
  82. package/dist/daemon/image-gen.js.map +1 -0
  83. package/dist/daemon/index.d.ts +12 -0
  84. package/dist/daemon/index.d.ts.map +1 -0
  85. package/dist/daemon/index.js +184 -0
  86. package/dist/daemon/index.js.map +1 -0
  87. package/dist/daemon/pai-projects.d.ts +68 -0
  88. package/dist/daemon/pai-projects.d.ts.map +1 -0
  89. package/dist/daemon/pai-projects.js +174 -0
  90. package/dist/daemon/pai-projects.js.map +1 -0
  91. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-56).d.ts +12 -0
  92. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-56).d.ts.map +1 -0
  93. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-56).js +252 -0
  94. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-56).js.map +1 -0
  95. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-13-59).js +252 -0
  96. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-36).d.ts +12 -0
  97. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-36).d.ts.map +1 -0
  98. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-36).js +240 -0
  99. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-36).js.map +1 -0
  100. package/dist/daemon/screenshot (SFConflict mnott 2026-03-06-21-15-46).js +240 -0
  101. package/dist/daemon/screenshot.d.ts +12 -0
  102. package/dist/daemon/screenshot.d.ts (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
  103. package/dist/daemon/screenshot.d.ts (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
  104. package/dist/daemon/screenshot.d.ts.map +1 -0
  105. package/dist/daemon/screenshot.js +252 -0
  106. package/dist/daemon/screenshot.js (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
  107. package/dist/daemon/screenshot.js (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
  108. package/dist/daemon/screenshot.js.map +1 -0
  109. package/dist/daemon/session-content.d.ts +27 -0
  110. package/dist/daemon/session-content.d.ts.map +1 -0
  111. package/dist/daemon/session-content.js +76 -0
  112. package/dist/daemon/session-content.js.map +1 -0
  113. package/dist/daemon/vision.d.ts +46 -0
  114. package/dist/daemon/vision.d.ts.map +1 -0
  115. package/dist/daemon/vision.js +176 -0
  116. package/dist/daemon/vision.js.map +1 -0
  117. package/dist/index.d.ts +16 -2
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +12 -1
  120. package/dist/index.js.map +1 -1
  121. package/dist/ipc/client.d.ts +4 -1
  122. package/dist/ipc/client.d.ts.map +1 -1
  123. package/dist/ipc/client.js +10 -1
  124. package/dist/ipc/client.js.map +1 -1
  125. package/dist/ipc/validate.d.ts +52 -0
  126. package/dist/ipc/validate.d.ts.map +1 -0
  127. package/dist/ipc/validate.js +129 -0
  128. package/dist/ipc/validate.js.map +1 -0
  129. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-56).d.ts +23 -0
  130. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-56).d.ts.map +1 -0
  131. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-56).js +595 -0
  132. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-56).js.map +1 -0
  133. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-13-59).js +595 -0
  134. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-36).d.ts +23 -0
  135. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-36).d.ts.map +1 -0
  136. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-36).js +592 -0
  137. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-36).js.map +1 -0
  138. package/dist/mcp/index (SFConflict mnott 2026-03-06-21-15-46).js +592 -0
  139. package/dist/mcp/index.d.ts +23 -0
  140. package/dist/mcp/index.d.ts.map +1 -0
  141. package/dist/mcp/index.js +660 -0
  142. package/dist/mcp/index.js (SFConflict mnott 2026-03-06-21-13-59).map +1 -0
  143. package/dist/mcp/index.js (SFConflict mnott 2026-03-06-21-15-46).map +1 -0
  144. package/dist/mcp/index.js.map +1 -0
  145. package/dist/types/adapter.d.ts +41 -0
  146. package/dist/types/adapter.d.ts.map +1 -0
  147. package/dist/types/adapter.js +2 -0
  148. package/dist/types/adapter.js.map +1 -0
  149. package/dist/types/backend.d.ts +29 -1
  150. package/dist/types/backend.d.ts.map +1 -1
  151. package/dist/types/broker.d.ts +47 -0
  152. package/dist/types/broker.d.ts.map +1 -0
  153. package/dist/types/broker.js +21 -0
  154. package/dist/types/broker.js.map +1 -0
  155. package/dist/types/index.d.ts +2 -0
  156. package/dist/types/index.d.ts.map +1 -1
  157. package/dist/types/index.js +2 -0
  158. package/dist/types/index.js.map +1 -1
  159. package/package.json +12 -2
  160. package/templates/adapter/ONBOARDING_PROMPT.md +309 -0
  161. package/templates/adapter/README.md.tmpl +81 -0
  162. package/templates/adapter/package.json.tmpl +23 -0
  163. package/templates/adapter/src/watcher/cli.ts.tmpl +12 -0
  164. package/templates/adapter/src/watcher/commands.ts.tmpl +44 -0
  165. package/templates/adapter/src/watcher/connection.ts.tmpl +59 -0
  166. package/templates/adapter/src/watcher/index.ts.tmpl +201 -0
  167. package/templates/adapter/src/watcher/ipc-server.ts.tmpl +250 -0
  168. package/templates/adapter/src/watcher/send.ts.tmpl +62 -0
  169. package/templates/adapter/src/watcher/state.ts.tmpl +39 -0
  170. package/templates/adapter/tsconfig.json.tmpl +14 -0
@@ -0,0 +1,640 @@
1
+ /**
2
+ * daemon/core-handlers.ts — Hub-level IPC handlers.
3
+ *
4
+ * Registers the core methods that any adapter or MCP client can call
5
+ * on the hub socket. Transport-specific methods (send, contacts, history)
6
+ * are NOT registered here — adapters handle those on their own sockets.
7
+ *
8
+ * Phase 1 methods:
9
+ * register_adapter — adapter announces itself to the hub
10
+ * unregister_adapter
11
+ * adapter_list — list connected adapters
12
+ * sessions — list hybrid sessions
13
+ * switch — switch active session
14
+ * end_session — end a hybrid session
15
+ * broadcast_status — push status to all PAILot clients
16
+ * voice_config — get/set TTS config
17
+ * status — hub health/connection summary
18
+ */
19
+ import { createBrokerMessage } from "../types/broker.js";
20
+ import { broadcastStatus, broadcastVoice, broadcastImage, broadcastText } from "../adapters/pailot/gateway.js";
21
+ import { WatcherClient } from "../ipc/client.js";
22
+ import { saveVoiceConfig } from "../core/persistence.js";
23
+ import { voiceConfig, setVoiceConfig, activeItermSessionId } from "../core/state.js";
24
+ import { splitIntoChunks } from "../adapters/kokoro/media.js";
25
+ import { stripMarkdown } from "../core/markdown.js";
26
+ import { listPaiProjects, findPaiProject, launchPaiProject } from "./pai-projects.js";
27
+ import { readSessionContent, readAllSessionContent } from "./session-content.js";
28
+ import { statusCache, hashContent } from "../core/status-cache.js";
29
+ import { readFileSync } from "node:fs";
30
+ import { join, dirname } from "node:path";
31
+ import { fileURLToPath } from "node:url";
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = dirname(__filename);
34
+ function getPackageVersion() {
35
+ try {
36
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
37
+ return pkg.version ?? "unknown";
38
+ }
39
+ catch {
40
+ return "unknown";
41
+ }
42
+ }
43
+ const HUB_VERSION = getPackageVersion();
44
+ export function registerCoreHandlers(server, registry, _apiBackend, manager) {
45
+ server.on("register_adapter", async (req) => {
46
+ const { name, socketPath } = req.params;
47
+ if (!name || !socketPath)
48
+ return { ok: false, error: "name and socketPath required" };
49
+ registry.register({ name, socketPath, registeredAt: Date.now() });
50
+ return { ok: true, result: { registered: true } };
51
+ });
52
+ server.on("unregister_adapter", async (req) => {
53
+ const { name } = req.params;
54
+ registry.unregister(name);
55
+ return { ok: true, result: { unregistered: true } };
56
+ });
57
+ server.on("adapter_list", async (_req) => {
58
+ return { ok: true, result: { adapters: registry.list() } };
59
+ });
60
+ server.on("sessions", async (_req) => {
61
+ const sessions = manager.listSessions().map((s, i) => ({
62
+ index: i + 1,
63
+ name: s.name,
64
+ kind: s.kind,
65
+ active: manager.activeSession?.id === s.id,
66
+ }));
67
+ return { ok: true, result: { sessions } };
68
+ });
69
+ server.on("switch", async (req) => {
70
+ const { target } = req.params;
71
+ const index = typeof target === "number" ? target : parseInt(String(target), 10);
72
+ const session = manager.switchToIndex(index);
73
+ if (!session)
74
+ return { ok: false, error: `Session ${target} not found` };
75
+ return { ok: true, result: { switched: true, name: session.name } };
76
+ });
77
+ server.on("end_session", async (req) => {
78
+ const { target } = req.params;
79
+ const index = typeof target === "number" ? target : parseInt(String(target), 10);
80
+ const session = manager.removeByIndex(index);
81
+ if (!session)
82
+ return { ok: false, error: `Session ${target} not found` };
83
+ return { ok: true, result: { ended: true, name: session.name } };
84
+ });
85
+ server.on("broadcast_status", async (req) => {
86
+ const { status } = req.params;
87
+ broadcastStatus(status);
88
+ return { ok: true, result: { status } };
89
+ });
90
+ server.on("voice_config", async (req) => {
91
+ const { action, ...updates } = req.params;
92
+ if (action === "get") {
93
+ return { ok: true, result: { config: voiceConfig } };
94
+ }
95
+ const merged = { ...voiceConfig, ...updates };
96
+ setVoiceConfig(merged);
97
+ saveVoiceConfig(merged);
98
+ return { ok: true, result: { success: true, config: merged } };
99
+ });
100
+ server.on("status", async (_req) => {
101
+ const adapterHealth = {};
102
+ for (const [name, health] of registry.getAllHealth()) {
103
+ adapterHealth[name] = health;
104
+ }
105
+ return {
106
+ ok: true,
107
+ result: {
108
+ version: HUB_VERSION,
109
+ adapters: registry.list().map(a => a.name),
110
+ activeSessions: manager.listSessions().length,
111
+ activeSession: manager.activeSession?.name ?? null,
112
+ adapterHealth,
113
+ },
114
+ };
115
+ });
116
+ /**
117
+ * ping — Lightweight heartbeat for adapter health checks.
118
+ * Returns immediately with the hub uptime. No side effects.
119
+ */
120
+ server.on("ping", async (_req) => {
121
+ return { ok: true, result: { pong: true, uptime: process.uptime() } };
122
+ });
123
+ // ── TTS / Voice Pipeline ──
124
+ /**
125
+ * tts — Convert text to voice note and deliver to requesting adapter.
126
+ *
127
+ * The hub generates the audio (Kokoro TTS) and sends the OGG buffer
128
+ * back to the adapter that requested it (via the "source" field).
129
+ */
130
+ server.on("tts", async (req) => {
131
+ const { text, voice, source, recipient } = req.params;
132
+ if (!text)
133
+ return { ok: false, error: "text is required" };
134
+ const resolvedVoice = voice ?? voiceConfig.defaultVoice;
135
+ try {
136
+ const { textToVoiceNote } = await import("../adapters/kokoro/tts.js");
137
+ const audioBuffer = await textToVoiceNote(text, resolvedVoice);
138
+ // If a source adapter is specified, deliver the voice note through it
139
+ if (source) {
140
+ const adapter = registry.get(source);
141
+ if (adapter) {
142
+ const msg = createBrokerMessage("hub", "voice", {
143
+ buffer: audioBuffer.toString("base64"),
144
+ text: text.slice(0, 100),
145
+ recipient,
146
+ metadata: { voice: resolvedVoice },
147
+ });
148
+ await registry.deliverToAdapter(adapter, msg);
149
+ }
150
+ }
151
+ // Also broadcast to PAILot clients
152
+ broadcastVoice(audioBuffer, text.slice(0, 200));
153
+ return { ok: true, result: { generated: true, voice: resolvedVoice, bytes: audioBuffer.length } };
154
+ }
155
+ catch (err) {
156
+ return { ok: false, error: `TTS failed: ${err instanceof Error ? err.message : String(err)}` };
157
+ }
158
+ });
159
+ /**
160
+ * speak — Play text locally via afplay (no network delivery).
161
+ */
162
+ server.on("speak", async (req) => {
163
+ const { text, voice } = req.params;
164
+ if (!text)
165
+ return { ok: false, error: "text is required" };
166
+ try {
167
+ const { speakLocally } = await import("../adapters/kokoro/tts.js");
168
+ await speakLocally(text, voice ?? voiceConfig.defaultVoice);
169
+ return { ok: true, result: { speaking: true } };
170
+ }
171
+ catch (err) {
172
+ return { ok: false, error: `Speak failed: ${err instanceof Error ? err.message : String(err)}` };
173
+ }
174
+ });
175
+ /**
176
+ * dictate — Record from mic and transcribe via Whisper.
177
+ */
178
+ server.on("dictate", async (req) => {
179
+ const { maxDuration } = req.params;
180
+ try {
181
+ const { recordFromMic, transcribeLocalAudio } = await import("../adapters/iterm/dictation.js");
182
+ const audioPath = await recordFromMic(maxDuration ?? 30);
183
+ const text = await transcribeLocalAudio(audioPath);
184
+ return { ok: true, result: { text, audioPath } };
185
+ }
186
+ catch (err) {
187
+ return { ok: false, error: `Dictation failed: ${err instanceof Error ? err.message : String(err)}` };
188
+ }
189
+ });
190
+ /**
191
+ * transcribe — Transcribe an audio buffer via Whisper.
192
+ */
193
+ server.on("transcribe", async (req) => {
194
+ const { audioBase64, mimetype } = req.params;
195
+ if (!audioBase64)
196
+ return { ok: false, error: "audioBase64 is required" };
197
+ try {
198
+ const { transcribeAudio, mimetypeToExt } = await import("../adapters/kokoro/media.js");
199
+ const { writeFileSync, unlinkSync } = await import("node:fs");
200
+ const { tmpdir } = await import("node:os");
201
+ const { join } = await import("node:path");
202
+ const ext = mimetypeToExt(mimetype ?? "audio/ogg");
203
+ const tmpPath = join(tmpdir(), `aibroker-transcribe-${Date.now()}.${ext}`);
204
+ writeFileSync(tmpPath, Buffer.from(audioBase64, "base64"));
205
+ try {
206
+ const text = await transcribeAudio(tmpPath);
207
+ return { ok: true, result: { text } };
208
+ }
209
+ finally {
210
+ try {
211
+ unlinkSync(tmpPath);
212
+ }
213
+ catch { /* ignore */ }
214
+ }
215
+ }
216
+ catch (err) {
217
+ return { ok: false, error: `Transcription failed: ${err instanceof Error ? err.message : String(err)}` };
218
+ }
219
+ });
220
+ /**
221
+ * list_voices — List available TTS voices.
222
+ */
223
+ server.on("list_voices", async (_req) => {
224
+ const { listVoices } = await import("../adapters/kokoro/tts.js");
225
+ return { ok: true, result: { voices: listVoices() } };
226
+ });
227
+ // ── PAI Named Sessions ──
228
+ server.on("pai_projects", async (_req) => {
229
+ const projects = await listPaiProjects();
230
+ return { ok: true, result: { projects } };
231
+ });
232
+ server.on("pai_find", async (req) => {
233
+ const { name } = req.params;
234
+ if (!name)
235
+ return { ok: false, error: "name is required" };
236
+ const project = await findPaiProject(name);
237
+ if (!project)
238
+ return { ok: false, error: `Project "${name}" not found` };
239
+ return { ok: true, result: { project } };
240
+ });
241
+ server.on("pai_launch", async (req) => {
242
+ const { name } = req.params;
243
+ if (!name)
244
+ return { ok: false, error: "name is required" };
245
+ let itermSessionId;
246
+ let sessionId;
247
+ try {
248
+ ({ itermSessionId, sessionId } = await launchPaiProject(name));
249
+ }
250
+ catch (err) {
251
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
252
+ }
253
+ // Register the visual session with HybridSessionManager
254
+ const project = await findPaiProject(name);
255
+ const displayName = project?.displayName || project?.name || name;
256
+ manager.registerVisualSession(displayName, project?.rootPath ?? "", itermSessionId);
257
+ return { ok: true, result: { itermSessionId, sessionId, name } };
258
+ });
259
+ // ── Phase 6: Image Generation ──
260
+ /**
261
+ * generate_image — Generate an image from a text prompt.
262
+ *
263
+ * Optionally sends an "on it..." ack and delivers the generated image
264
+ * back to the requesting adapter.
265
+ */
266
+ server.on("generate_image", async (req) => {
267
+ const { prompt, source, recipient, ack, width, height } = req.params;
268
+ if (!prompt)
269
+ return { ok: false, error: "prompt is required" };
270
+ // Send "on it..." ack to the requesting adapter
271
+ if (ack !== false && source) {
272
+ const adapter = registry.get(source);
273
+ if (adapter) {
274
+ const ackMsg = createBrokerMessage("hub", "text", {
275
+ text: "On it... generating your image.",
276
+ recipient,
277
+ });
278
+ registry.deliverToAdapter(adapter, ackMsg).catch(() => { });
279
+ }
280
+ }
281
+ try {
282
+ const { generateImage } = await import("./image-gen.js");
283
+ const result = await generateImage({ prompt, width, height });
284
+ // Deliver image to requesting adapter
285
+ if (source && result.images.length > 0) {
286
+ const adapter = registry.get(source);
287
+ if (adapter) {
288
+ const imgMsg = createBrokerMessage("hub", "image", {
289
+ buffer: result.images[0].toString("base64"),
290
+ caption: prompt.slice(0, 200),
291
+ recipient,
292
+ metadata: { model: result.model, durationMs: result.durationMs },
293
+ });
294
+ await registry.deliverToAdapter(adapter, imgMsg);
295
+ }
296
+ }
297
+ // Also broadcast to PAILot clients
298
+ if (result.images.length > 0) {
299
+ broadcastImage(result.images[0], prompt.slice(0, 200));
300
+ }
301
+ return {
302
+ ok: true,
303
+ result: {
304
+ generated: true,
305
+ model: result.model,
306
+ durationMs: result.durationMs,
307
+ imageCount: result.images.length,
308
+ bytes: result.images.reduce((s, b) => s + b.length, 0),
309
+ },
310
+ };
311
+ }
312
+ catch (err) {
313
+ return { ok: false, error: `Image generation failed: ${err instanceof Error ? err.message : String(err)}` };
314
+ }
315
+ });
316
+ // ── Phase 7: Vision & Understanding ──
317
+ /**
318
+ * analyze_image — Save image and deliver to active Claude Code session.
319
+ *
320
+ * The image is saved to ~/.aibroker/media/ and the path is routed through
321
+ * the command handler to the active iTerm2 session. Claude Code in that
322
+ * session reads the image with its Read tool (covered by Max plan).
323
+ */
324
+ server.on("analyze_image", async (req) => {
325
+ const { imageBase64, mimetype, prompt, source, recipient } = req.params;
326
+ if (!imageBase64)
327
+ return { ok: false, error: "imageBase64 is required" };
328
+ try {
329
+ const { saveReceivedImage } = await import("./vision.js");
330
+ const imageBuffer = Buffer.from(imageBase64, "base64");
331
+ const { path, sizeBytes } = saveReceivedImage(imageBuffer, mimetype);
332
+ // Route through the command handler → active iTerm2 session
333
+ const userPrompt = prompt ?? "Analyze this image.";
334
+ const messageText = `[Image: ${path}] ${userPrompt}`;
335
+ const sourceAdapter = source ? registry.get(source) : undefined;
336
+ const msg = createBrokerMessage(source ?? "hub", "command", {
337
+ text: messageText,
338
+ recipient,
339
+ });
340
+ await registry.route(msg);
341
+ return { ok: true, result: { saved: true, path, sizeBytes } };
342
+ }
343
+ catch (err) {
344
+ return { ok: false, error: `Image analysis failed: ${err instanceof Error ? err.message : String(err)}` };
345
+ }
346
+ });
347
+ /**
348
+ * analyze_video — Analyze a video using Gemini 2.0 Flash (free tier).
349
+ *
350
+ * Video can't be read by Claude Code's Read tool, so we use Gemini's
351
+ * native video understanding and deliver the text result back.
352
+ */
353
+ server.on("analyze_video", async (req) => {
354
+ const { videoBase64, mimetype, prompt, source, recipient } = req.params;
355
+ if (!videoBase64)
356
+ return { ok: false, error: "videoBase64 is required" };
357
+ // Ack — video analysis takes longer
358
+ if (source) {
359
+ const adapter = registry.get(source);
360
+ if (adapter) {
361
+ const ackMsg = createBrokerMessage("hub", "text", {
362
+ text: "Analyzing your video...",
363
+ recipient,
364
+ });
365
+ registry.deliverToAdapter(adapter, ackMsg).catch(() => { });
366
+ }
367
+ }
368
+ try {
369
+ const { analyzeVideo, saveReceivedVideo } = await import("./vision.js");
370
+ const videoBuffer = Buffer.from(videoBase64, "base64");
371
+ const { path } = saveReceivedVideo(videoBuffer, mimetype);
372
+ const result = await analyzeVideo({ videoBuffer, mimetype, prompt });
373
+ // Deliver the analysis text to the active session
374
+ if (result.text) {
375
+ const analysisText = `[Video analysis of ${path}]\n\n${result.text}`;
376
+ const msg = createBrokerMessage(source ?? "hub", "command", {
377
+ text: analysisText,
378
+ recipient,
379
+ });
380
+ await registry.route(msg);
381
+ }
382
+ return { ok: true, result: { text: result.text, model: result.model, durationMs: result.durationMs, path } };
383
+ }
384
+ catch (err) {
385
+ return { ok: false, error: `Video analysis failed: ${err instanceof Error ? err.message : String(err)}` };
386
+ }
387
+ });
388
+ // ── Session Orchestration (Phase 1) ──
389
+ /**
390
+ * session_content — Read raw terminal content from iTerm2 sessions.
391
+ *
392
+ * If sessionId is provided, reads that specific session.
393
+ * If omitted, reads all sessions. Returns raw content + busy/idle flag
394
+ * + whether content has changed since last probe (via content hash).
395
+ */
396
+ server.on("session_content", async (req) => {
397
+ const { sessionId, lines } = req.params;
398
+ const lineCount = lines ?? 100;
399
+ if (sessionId) {
400
+ const content = readSessionContent(sessionId, lineCount);
401
+ if (!content)
402
+ return { ok: false, error: `Session ${sessionId} not found in iTerm2` };
403
+ const contentHash = hashContent(content.content);
404
+ const changed = statusCache.hasChanged(sessionId, contentHash);
405
+ const cached = statusCache.get(sessionId);
406
+ if (!changed) {
407
+ statusCache.touch(sessionId);
408
+ }
409
+ return {
410
+ ok: true,
411
+ result: {
412
+ session: {
413
+ ...content,
414
+ contentHash,
415
+ changed,
416
+ cachedSummary: cached?.summary ?? null,
417
+ cachedAt: cached?.timestamp ?? null,
418
+ },
419
+ },
420
+ };
421
+ }
422
+ // All sessions
423
+ const contents = readAllSessionContent(lineCount);
424
+ const sessions = contents.map((c) => {
425
+ const contentHash = hashContent(c.content);
426
+ const changed = statusCache.hasChanged(c.sessionId, contentHash);
427
+ const cached = statusCache.get(c.sessionId);
428
+ if (!changed)
429
+ statusCache.touch(c.sessionId);
430
+ return {
431
+ ...c,
432
+ contentHash,
433
+ changed,
434
+ cachedSummary: cached?.summary ?? null,
435
+ cachedAt: cached?.timestamp ?? null,
436
+ };
437
+ });
438
+ return { ok: true, result: { sessions } };
439
+ });
440
+ /**
441
+ * cache_status — Store a parsed summary for a session.
442
+ *
443
+ * Called by the requesting session's AI after parsing raw terminal content.
444
+ * The summary is cached with the content hash so future probes can skip parsing
445
+ * if content hasn't changed.
446
+ */
447
+ server.on("cache_status", async (req) => {
448
+ const { sessionId, sessionName, summary, contentHash, state } = req.params;
449
+ if (!sessionId)
450
+ return { ok: false, error: "sessionId is required" };
451
+ if (!summary)
452
+ return { ok: false, error: "summary is required" };
453
+ statusCache.set(sessionId, {
454
+ sessionId,
455
+ sessionName: sessionName ?? sessionId,
456
+ timestamp: Date.now(),
457
+ state: state ?? "idle",
458
+ summary,
459
+ contentHash: contentHash ?? "",
460
+ lastProbeAt: Date.now(),
461
+ });
462
+ return { ok: true, result: { cached: true, sessionId } };
463
+ });
464
+ /**
465
+ * get_cached_status — Retrieve cached session summaries without re-probing.
466
+ *
467
+ * If sessionId is provided, returns that session's cached snapshot.
468
+ * If omitted, returns all cached snapshots.
469
+ */
470
+ server.on("get_cached_status", async (req) => {
471
+ const { sessionId } = req.params;
472
+ if (sessionId) {
473
+ const cached = statusCache.get(sessionId);
474
+ if (!cached)
475
+ return { ok: true, result: { snapshot: null } };
476
+ return { ok: true, result: { snapshot: cached } };
477
+ }
478
+ return { ok: true, result: { snapshots: statusCache.getAll() } };
479
+ });
480
+ // ── Unified MCP Support ──
481
+ /**
482
+ * adapter_call — Proxy an IPC call to a named adapter through the hub.
483
+ * The unified MCP server uses this to reach adapter-specific methods
484
+ * (send, receive, contacts, history, etc.) without knowing socket paths.
485
+ */
486
+ server.on("adapter_call", async (req) => {
487
+ const { adapter, method, params } = req.params;
488
+ if (!adapter)
489
+ return { ok: false, error: "adapter is required" };
490
+ if (!method)
491
+ return { ok: false, error: "method is required" };
492
+ const desc = registry.get(adapter);
493
+ if (!desc) {
494
+ return { ok: false, error: `Adapter '${adapter}' not registered. Is the ${adapter} daemon running?` };
495
+ }
496
+ try {
497
+ const client = new WatcherClient(desc.socketPath);
498
+ const forwardParams = { ...(params ?? {}), sessionId: req.sessionId };
499
+ if (req.itermSessionId)
500
+ forwardParams.itermSessionId = req.itermSessionId;
501
+ const result = await client.call_raw(method, forwardParams);
502
+ return { ok: true, result };
503
+ }
504
+ catch (e) {
505
+ const msg = e instanceof Error ? e.message : String(e);
506
+ return { ok: false, error: `adapter_call to ${adapter}.${method} failed: ${msg}` };
507
+ }
508
+ });
509
+ /**
510
+ * pailot_send — Send text or voice to PAILot app clients via WS gateway.
511
+ */
512
+ server.on("pailot_send", async (req) => {
513
+ const { text, voice, voiceName, sessionId: callerSessionId } = req.params;
514
+ if (!text)
515
+ return { ok: false, error: "text is required" };
516
+ // MCP server may not have ITERM_SESSION_ID — fall back to daemon's active session
517
+ const sessionId = callerSessionId || activeItermSessionId || undefined;
518
+ try {
519
+ if (voice) {
520
+ const { textToVoiceNote } = await import("../adapters/kokoro/tts.js");
521
+ const resolvedVoice = voiceName ?? voiceConfig.defaultVoice;
522
+ const plainText = stripMarkdown(text);
523
+ const chunks = splitIntoChunks(plainText);
524
+ for (let i = 0; i < chunks.length; i++) {
525
+ if (i > 0)
526
+ await new Promise((r) => setTimeout(r, 1000));
527
+ const audioBuffer = await textToVoiceNote(chunks[i], resolvedVoice);
528
+ // First chunk gets the full original text as transcript;
529
+ // subsequent chunks get empty transcript (audio only)
530
+ const transcript = i === 0 ? plainText : "";
531
+ await broadcastVoice(audioBuffer, transcript, sessionId);
532
+ }
533
+ return { ok: true, result: { sent: true, chunks: chunks.length } };
534
+ }
535
+ else {
536
+ broadcastText(text, sessionId);
537
+ }
538
+ return { ok: true, result: { sent: true } };
539
+ }
540
+ catch (e) {
541
+ return { ok: false, error: `pailot_send failed: ${e instanceof Error ? e.message : String(e)}` };
542
+ }
543
+ });
544
+ /**
545
+ * pailot_receive — Drain the PAILot message queue.
546
+ * Currently proxied to whazaa adapter's receive with from='pailot'.
547
+ */
548
+ server.on("pailot_receive", async (req) => {
549
+ const adapterName = registry.get("whazaa") ? "whazaa" : "telex";
550
+ const desc = registry.get(adapterName);
551
+ if (!desc)
552
+ return { ok: true, result: { messages: [] } };
553
+ try {
554
+ const client = new WatcherClient(desc.socketPath);
555
+ const result = await client.call_raw("receive", {
556
+ from: "pailot",
557
+ sessionId: req.sessionId,
558
+ });
559
+ return { ok: true, result };
560
+ }
561
+ catch {
562
+ return { ok: true, result: { messages: [] } };
563
+ }
564
+ });
565
+ /**
566
+ * rename — Rename session in hub + forward to all adapters.
567
+ */
568
+ server.on("rename", async (req) => {
569
+ const { name } = req.params;
570
+ if (!name)
571
+ return { ok: false, error: "name is required" };
572
+ // Update in hub's session manager
573
+ const session = manager.activeSession;
574
+ if (session)
575
+ manager.updateName(session.id, name);
576
+ // Forward to all adapters (best effort)
577
+ for (const adapter of registry.list()) {
578
+ try {
579
+ const client = new WatcherClient(adapter.socketPath);
580
+ await client.call_raw("rename", { name, sessionId: req.sessionId });
581
+ }
582
+ catch { /* best effort */ }
583
+ }
584
+ return { ok: true, result: { success: true, name } };
585
+ });
586
+ /**
587
+ * discover — Proxy to first available adapter for iTerm2 session scan.
588
+ */
589
+ server.on("discover", async (req) => {
590
+ const adapters = registry.list();
591
+ if (adapters.length === 0)
592
+ return { ok: false, error: "No adapters registered" };
593
+ try {
594
+ const client = new WatcherClient(adapters[0].socketPath);
595
+ const result = await client.call_raw("discover", { sessionId: req.sessionId });
596
+ return { ok: true, result };
597
+ }
598
+ catch (e) {
599
+ return { ok: false, error: `discover failed: ${e instanceof Error ? e.message : String(e)}` };
600
+ }
601
+ });
602
+ /**
603
+ * command — Execute a slash command through the hub command handler.
604
+ */
605
+ server.on("command", async (req) => {
606
+ const { text } = req.params;
607
+ if (!text)
608
+ return { ok: false, error: "text is required" };
609
+ // Try the hub's command handler first
610
+ const adapters = registry.list();
611
+ if (adapters.length > 0) {
612
+ try {
613
+ const client = new WatcherClient(adapters[0].socketPath);
614
+ const result = await client.call_raw("command", { text, sessionId: req.sessionId });
615
+ return { ok: true, result };
616
+ }
617
+ catch { /* fall through */ }
618
+ }
619
+ return { ok: true, result: { executed: true, command: text } };
620
+ });
621
+ // ── Phase 2: Message Routing ──
622
+ /**
623
+ * route_message — Adapters send messages to the hub for routing.
624
+ *
625
+ * The hub inspects the BrokerMessage target and type, then delivers
626
+ * to the appropriate adapter via its IPC socket ("deliver" method).
627
+ */
628
+ server.on("route_message", async (req) => {
629
+ const { message } = req.params;
630
+ if (!message || !message.source || !message.type) {
631
+ return { ok: false, error: "Invalid BrokerMessage: source and type are required" };
632
+ }
633
+ const result = await registry.route(message);
634
+ if (result.ok) {
635
+ return { ok: true, result: result };
636
+ }
637
+ return { ok: false, error: result.error ?? "Routing failed" };
638
+ });
639
+ }
640
+ //# sourceMappingURL=core-handlers.js.map