aibroker 0.5.1 → 0.6.2

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 (133) hide show
  1. package/README.md +263 -104
  2. package/dist/adapters/iterm/iterm2-api.d.ts +20 -0
  3. package/dist/adapters/iterm/iterm2-api.d.ts.map +1 -0
  4. package/dist/adapters/iterm/iterm2-api.js +244 -0
  5. package/dist/adapters/iterm/iterm2-api.js.map +1 -0
  6. package/dist/adapters/iterm/sessions.d.ts +1 -0
  7. package/dist/adapters/iterm/sessions.d.ts.map +1 -1
  8. package/dist/adapters/iterm/sessions.js +26 -2
  9. package/dist/adapters/iterm/sessions.js.map +1 -1
  10. package/dist/adapters/kokoro/media.js +2 -2
  11. package/dist/adapters/kokoro/media.js.map +1 -1
  12. package/dist/adapters/pailot/gateway.d.ts +5 -6
  13. package/dist/adapters/pailot/gateway.d.ts.map +1 -1
  14. package/dist/adapters/pailot/gateway.js +575 -34
  15. package/dist/adapters/pailot/gateway.js.map +1 -1
  16. package/dist/aibp/bridge.d.ts +123 -0
  17. package/dist/aibp/bridge.d.ts.map +1 -0
  18. package/dist/aibp/bridge.js +363 -0
  19. package/dist/aibp/bridge.js.map +1 -0
  20. package/dist/aibp/envelope.d.ts +26 -0
  21. package/dist/aibp/envelope.d.ts.map +1 -0
  22. package/dist/aibp/envelope.js +101 -0
  23. package/dist/aibp/envelope.js.map +1 -0
  24. package/dist/aibp/index.d.ts +11 -0
  25. package/dist/aibp/index.d.ts.map +1 -0
  26. package/dist/aibp/index.js +10 -0
  27. package/dist/aibp/index.js.map +1 -0
  28. package/dist/aibp/registry.d.ts +71 -0
  29. package/dist/aibp/registry.d.ts.map +1 -0
  30. package/dist/aibp/registry.js +408 -0
  31. package/dist/aibp/registry.js.map +1 -0
  32. package/dist/aibp/types.d.ts +91 -0
  33. package/dist/aibp/types.d.ts.map +1 -0
  34. package/dist/aibp/types.js +8 -0
  35. package/dist/aibp/types.js.map +1 -0
  36. package/dist/core/hybrid.d.ts +2 -0
  37. package/dist/core/hybrid.d.ts.map +1 -1
  38. package/dist/core/hybrid.js +8 -0
  39. package/dist/core/hybrid.js.map +1 -1
  40. package/dist/core/state.d.ts +12 -0
  41. package/dist/core/state.d.ts.map +1 -1
  42. package/dist/core/state.js +34 -0
  43. package/dist/core/state.js.map +1 -1
  44. package/dist/core/status-cache.d.ts +51 -0
  45. package/dist/core/status-cache.d.ts.map +1 -0
  46. package/dist/core/status-cache.js +62 -0
  47. package/dist/core/status-cache.js.map +1 -0
  48. package/dist/daemon/adapter-registry.d.ts +5 -0
  49. package/dist/daemon/adapter-registry.d.ts.map +1 -1
  50. package/dist/daemon/adapter-registry.js +94 -4
  51. package/dist/daemon/adapter-registry.js.map +1 -1
  52. package/dist/daemon/cli.d.ts +1 -0
  53. package/dist/daemon/cli.d.ts.map +1 -1
  54. package/dist/daemon/cli.js +95 -3
  55. package/dist/daemon/cli.js.map +1 -1
  56. package/dist/daemon/command-context.d.ts +28 -0
  57. package/dist/daemon/command-context.d.ts.map +1 -0
  58. package/dist/daemon/command-context.js +13 -0
  59. package/dist/daemon/command-context.js.map +1 -0
  60. package/dist/daemon/commands.d.ts +22 -0
  61. package/dist/daemon/commands.d.ts.map +1 -0
  62. package/dist/daemon/commands.js +849 -0
  63. package/dist/daemon/commands.js.map +1 -0
  64. package/dist/daemon/core-handlers.d.ts.map +1 -1
  65. package/dist/daemon/core-handlers.js +758 -3
  66. package/dist/daemon/core-handlers.js.map +1 -1
  67. package/dist/daemon/create-adapter.js +2 -1
  68. package/dist/daemon/create-adapter.js.map +1 -1
  69. package/dist/daemon/image-context.d.ts +56 -0
  70. package/dist/daemon/image-context.d.ts.map +1 -0
  71. package/dist/daemon/image-context.js +116 -0
  72. package/dist/daemon/image-context.js.map +1 -0
  73. package/dist/daemon/image-gen/index.d.ts +22 -0
  74. package/dist/daemon/image-gen/index.d.ts.map +1 -0
  75. package/dist/daemon/image-gen/index.js +129 -0
  76. package/dist/daemon/image-gen/index.js.map +1 -0
  77. package/dist/daemon/image-gen/providers/cloudflare.d.ts +13 -0
  78. package/dist/daemon/image-gen/providers/cloudflare.d.ts.map +1 -0
  79. package/dist/daemon/image-gen/providers/cloudflare.js +63 -0
  80. package/dist/daemon/image-gen/providers/cloudflare.js.map +1 -0
  81. package/dist/daemon/image-gen/providers/huggingface.d.ts +12 -0
  82. package/dist/daemon/image-gen/providers/huggingface.d.ts.map +1 -0
  83. package/dist/daemon/image-gen/providers/huggingface.js +58 -0
  84. package/dist/daemon/image-gen/providers/huggingface.js.map +1 -0
  85. package/dist/daemon/image-gen/providers/pollinations.d.ts +11 -0
  86. package/dist/daemon/image-gen/providers/pollinations.d.ts.map +1 -0
  87. package/dist/daemon/image-gen/providers/pollinations.js +39 -0
  88. package/dist/daemon/image-gen/providers/pollinations.js.map +1 -0
  89. package/dist/daemon/image-gen/providers/replicate.d.ts +9 -0
  90. package/dist/daemon/image-gen/providers/replicate.d.ts.map +1 -0
  91. package/dist/daemon/image-gen/providers/replicate.js +158 -0
  92. package/dist/daemon/image-gen/providers/replicate.js.map +1 -0
  93. package/dist/daemon/image-gen/types.d.ts +41 -0
  94. package/dist/daemon/image-gen/types.d.ts.map +1 -0
  95. package/dist/daemon/image-gen/types.js +5 -0
  96. package/dist/daemon/image-gen/types.js.map +1 -0
  97. package/dist/daemon/index.d.ts.map +1 -1
  98. package/dist/daemon/index.js +260 -6
  99. package/dist/daemon/index.js.map +1 -1
  100. package/dist/daemon/screenshot.d.ts +12 -0
  101. package/dist/daemon/screenshot.d.ts.map +1 -0
  102. package/dist/daemon/screenshot.js +252 -0
  103. package/dist/daemon/screenshot.js.map +1 -0
  104. package/dist/daemon/session-content.d.ts +27 -0
  105. package/dist/daemon/session-content.d.ts.map +1 -0
  106. package/dist/daemon/session-content.js +76 -0
  107. package/dist/daemon/session-content.js.map +1 -0
  108. package/dist/daemon/vision.d.ts +46 -0
  109. package/dist/daemon/vision.d.ts.map +1 -0
  110. package/dist/daemon/vision.js +176 -0
  111. package/dist/daemon/vision.js.map +1 -0
  112. package/dist/index.d.ts +6 -1
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +4 -1
  115. package/dist/index.js.map +1 -1
  116. package/dist/ipc/validate.d.ts +52 -0
  117. package/dist/ipc/validate.d.ts.map +1 -0
  118. package/dist/ipc/validate.js +129 -0
  119. package/dist/ipc/validate.js.map +1 -0
  120. package/dist/mcp/index.d.ts +23 -0
  121. package/dist/mcp/index.d.ts.map +1 -0
  122. package/dist/mcp/index.js +787 -0
  123. package/dist/mcp/index.js.map +1 -0
  124. package/dist/types/broker.d.ts +3 -1
  125. package/dist/types/broker.d.ts.map +1 -1
  126. package/dist/types/broker.js.map +1 -1
  127. package/package.json +5 -2
  128. package/templates/adapter/ONBOARDING_PROMPT.md +51 -29
  129. package/templates/adapter/README.md.tmpl +14 -31
  130. package/templates/adapter/package.json.tmpl +1 -1
  131. package/templates/adapter/src/watcher/commands.ts.tmpl +24 -126
  132. package/templates/adapter/src/watcher/index.ts.tmpl +112 -88
  133. package/templates/adapter/src/watcher/ipc-server.ts.tmpl +27 -3
@@ -0,0 +1,849 @@
1
+ /**
2
+ * daemon/commands.ts — Unified slash-command router for the AIBroker hub.
3
+ *
4
+ * Handles all slash commands (/s, /ss, /c, /cc, /n, etc.) and message
5
+ * delivery to iTerm2 sessions. Transport-agnostic — uses CommandContext
6
+ * for all replies instead of calling adapter-specific send functions.
7
+ *
8
+ * This module was extracted from Whazaa's commands.ts to make it the
9
+ * single command handler shared by all adapters.
10
+ */
11
+ import { basename } from "node:path";
12
+ import { sessionRegistry, activeClientId, setActiveClientId, activeItermSessionId, setActiveItermSessionId, cachedSessionList, cachedSessionListTime, setCachedSessionList, clientQueues, managedSessions, dispatchIncomingMessage, sessionTtyCache, updateSessionTtyCache, getAibpBridge, } from "../core/state.js";
13
+ import { getSessionList, setItermSessionVar, setItermTabName, setItermBadge, createClaudeSession, createTerminalTab, killSession, restartSession, } from "../adapters/iterm/sessions.js";
14
+ import { runAppleScript, findClaudeSession, isClaudeRunningInSession, isScreenLocked, typeIntoSession, pasteTextIntoSession, sendKeystrokeToSession, sendEscapeSequenceToSession, stripItermPrefix, writeToTty, snapshotAllSessions, } from "../adapters/iterm/core.js";
15
+ import { log } from "../core/log.js";
16
+ import { statusCache } from "../core/status-cache.js";
17
+ import { deliverViaApi } from "../core/transport.js";
18
+ import { hybridManager } from "../core/hybrid.js";
19
+ import { handleScreenshot } from "./screenshot.js";
20
+ import { buildImageContextKey, getImageContext, setImageContext, clearImageContext, classifyImageRequest, buildChainedPrompt, appendPromptToContext, } from "./image-context.js";
21
+ /**
22
+ * Detect natural language image generation requests.
23
+ * Returns the extracted prompt if matched, null otherwise.
24
+ */
25
+ const IMAGE_REQUEST_PATTERNS = [
26
+ /^(?:send|show|give|create|make|draw|paint|render|generate)\s+(?:me\s+)?(?:an?\s+)?(?:image|picture|photo|illustration|drawing|painting)\s+(?:of\s+)?(.+)/i,
27
+ /^(?:schick|zeig|mach|erstell|generier|mal)\s+(?:mir\s+)?(?:ein(?:e|en)?\s+)?(?:bild|foto|zeichnung|illustration)\s+(?:von\s+)?(.+)/i,
28
+ ];
29
+ function detectImageRequest(text) {
30
+ for (const pattern of IMAGE_REQUEST_PATTERNS) {
31
+ const match = text.match(pattern);
32
+ if (match?.[1])
33
+ return match[1].trim();
34
+ }
35
+ return null;
36
+ }
37
+ /**
38
+ * Create the hub command handler.
39
+ *
40
+ * Returns a function that processes incoming messages (slash commands or
41
+ * plain text) and routes them to iTerm2 sessions or API backends.
42
+ *
43
+ * The `ctx` parameter is provided per-message by the hub routing engine,
44
+ * so the command handler knows how to reply to the originating adapter.
45
+ */
46
+ export function createHubCommandHandler() {
47
+ // ── Session resolution ──
48
+ function ensureActiveSession() {
49
+ const allSessions = getSessionList();
50
+ const allSessionIds = new Set(allSessions.map((s) => s.id));
51
+ for (const [sid, entry] of sessionRegistry) {
52
+ if (entry.itermSessionId && !allSessionIds.has(entry.itermSessionId)) {
53
+ sessionRegistry.delete(sid);
54
+ clientQueues.delete(sid);
55
+ if (activeClientId === sid) {
56
+ const remaining = [...sessionRegistry.values()].sort((a, b) => b.registeredAt - a.registeredAt);
57
+ setActiveClientId(remaining.length > 0 ? remaining[0].sessionId : null);
58
+ }
59
+ }
60
+ }
61
+ if (activeItermSessionId && !allSessionIds.has(activeItermSessionId)) {
62
+ log(`ensureActiveSession: ${activeItermSessionId.slice(0, 8)}… not in live sessions — clearing`);
63
+ setActiveItermSessionId("");
64
+ }
65
+ if (!activeItermSessionId && allSessions.length > 0) {
66
+ const busy = allSessions.find((s) => s.type === "claude" && !s.atPrompt);
67
+ const anyClaudeSession = allSessions.find((s) => s.type === "claude");
68
+ const pick = busy ?? anyClaudeSession ?? allSessions[0];
69
+ if (pick) {
70
+ setActiveItermSessionId(pick.id);
71
+ log(`ensureActiveSession: auto-selected ${pick.name} (${pick.id.slice(0, 8)}…)`);
72
+ }
73
+ }
74
+ setCachedSessionList(allSessions, Date.now());
75
+ return activeItermSessionId;
76
+ }
77
+ // ── Message delivery to iTerm2 ──
78
+ function deliverMessage(text) {
79
+ const bareSessionId = stripItermPrefix(activeItermSessionId) ?? activeItermSessionId;
80
+ if (bareSessionId && managedSessions.has(bareSessionId)) {
81
+ if (typeIntoSession(bareSessionId, text))
82
+ return true;
83
+ managedSessions.delete(bareSessionId);
84
+ }
85
+ if (activeItermSessionId) {
86
+ if (typeIntoSession(activeItermSessionId, text))
87
+ return true;
88
+ }
89
+ if (isScreenLocked()) {
90
+ const targetId = bareSessionId || activeItermSessionId;
91
+ let ttyPath = targetId ? sessionTtyCache.get(targetId) : undefined;
92
+ if (!ttyPath) {
93
+ log("Screen locked — TTY cache miss, attempting live snapshot refresh");
94
+ const fresh = snapshotAllSessions();
95
+ updateSessionTtyCache(fresh);
96
+ ttyPath = targetId ? sessionTtyCache.get(targetId) : undefined;
97
+ if (!ttyPath && fresh.length > 0) {
98
+ ttyPath = fresh[0].tty;
99
+ log(`Screen locked — falling back to first available TTY: ${ttyPath}`);
100
+ }
101
+ }
102
+ if (ttyPath) {
103
+ log(`Screen locked — PTY write fallback to ${ttyPath}`);
104
+ if (writeToTty(ttyPath, text))
105
+ return true;
106
+ log(`PTY write fallback failed for ${ttyPath}`);
107
+ }
108
+ else {
109
+ log("Screen locked — no TTY available for PTY fallback");
110
+ }
111
+ return false;
112
+ }
113
+ log(`${activeItermSessionId ? `Session ${activeItermSessionId} is not running Claude.` : "No cached session."} Searching for another...`);
114
+ const found = findClaudeSession();
115
+ if (found && isClaudeRunningInSession(found)) {
116
+ setActiveItermSessionId(found);
117
+ if (typeIntoSession(found, text))
118
+ return true;
119
+ }
120
+ log("No running Claude session found. Starting new one...");
121
+ const created = createClaudeSession();
122
+ if (created) {
123
+ setActiveItermSessionId(created);
124
+ if (typeIntoSession(created, text))
125
+ return true;
126
+ }
127
+ log("Failed to deliver message");
128
+ return false;
129
+ }
130
+ // ── Terminal tab handling ──
131
+ function handleTerminal(command) {
132
+ const newId = createTerminalTab(command ?? undefined);
133
+ if (newId) {
134
+ managedSessions.set(newId, { name: command ?? "terminal", createdAt: Date.now() });
135
+ setActiveItermSessionId(newId);
136
+ log(`/t: created terminal tab ${newId}`);
137
+ }
138
+ }
139
+ // ── End session (close iTerm2 tab + cleanup) ──
140
+ async function handleEndSessionVisual(session) {
141
+ killSession(session.id);
142
+ // Cleanup registry entries pointing to this iTerm session
143
+ for (const [sid, entry] of sessionRegistry) {
144
+ if (entry.itermSessionId === session.id) {
145
+ sessionRegistry.delete(sid);
146
+ clientQueues.delete(sid);
147
+ if (activeClientId === sid)
148
+ setActiveClientId(null);
149
+ }
150
+ }
151
+ managedSessions.delete(session.id);
152
+ if (activeItermSessionId === session.id)
153
+ setActiveItermSessionId("");
154
+ log(`Ended session ${session.id} ("${session.paiName ?? session.name}")`);
155
+ }
156
+ // ── Relocate (new visual session) ──
157
+ function handleRelocate(targetPath) {
158
+ const command = `claude --dangerously-skip-permissions`;
159
+ const newId = createClaudeSession(`cd ${targetPath} && ${command}`);
160
+ return newId;
161
+ }
162
+ // ── Main command handler ──
163
+ return function handleMessage(text, timestamp, ctx) {
164
+ const trimmedText = text.trim();
165
+ // --- /h, /help ---
166
+ if (trimmedText === "/h" || trimmedText === "/help") {
167
+ const help = [
168
+ "*Commands*",
169
+ "",
170
+ "*Sessions*",
171
+ "/s — List sessions",
172
+ "/N — Switch to session N",
173
+ "/N name — Switch & rename",
174
+ "/n path — New visual session (iTerm2)",
175
+ "/nh path — New headless session",
176
+ "/t [cmd] — Open terminal tab",
177
+ "/r N — Restart Claude in session N",
178
+ "/e N — End session (close tab)",
179
+ "",
180
+ "*Session control*",
181
+ "/c — Send /clear + go to Claude",
182
+ "/p — Send \"pause session\" to Claude",
183
+ "/ss — Screenshot",
184
+ "/st — Session status (busy/idle)",
185
+ "",
186
+ "*Media*",
187
+ "/image <prompt> — Generate an image",
188
+ "",
189
+ "*Watcher*",
190
+ "/restart — Restart the adapter",
191
+ "",
192
+ "*Keys*",
193
+ "/cc — Ctrl+C",
194
+ "/esc — Escape",
195
+ "/enter — Enter",
196
+ "/tab — Tab",
197
+ "/up /down /left /right — Arrows",
198
+ "/pick N [text] — Menu select",
199
+ ].join("\n");
200
+ ctx.reply(help).catch(() => { });
201
+ return;
202
+ }
203
+ // --- /nh <path> — new headless (API) session ---
204
+ const nhMatch = trimmedText.match(/^\/nh\s+(.+)$/);
205
+ if (nhMatch) {
206
+ const targetPath = nhMatch[1].trim();
207
+ if (targetPath && hybridManager) {
208
+ const name = basename(targetPath);
209
+ const session = hybridManager.createApiSession(name, targetPath);
210
+ log(`/nh: created API session "${session.name}" (${session.id}) cwd=${session.cwd}`);
211
+ ctx.reply(`New headless session: *${session.name}* (${session.cwd})`).catch(() => { });
212
+ }
213
+ return;
214
+ }
215
+ // --- /n <path> (aliases: /nv, /new, /relocate) — new visual session ---
216
+ const relocateMatch = trimmedText.match(/^\/(?:n|nv|new|relocate)\s+(.+)$/);
217
+ if (relocateMatch) {
218
+ const targetPath = relocateMatch[1].trim();
219
+ if (targetPath) {
220
+ const newSessionId = handleRelocate(targetPath);
221
+ if (newSessionId) {
222
+ const name = basename(targetPath);
223
+ if (hybridManager) {
224
+ hybridManager.registerVisualSession(name, targetPath, newSessionId);
225
+ }
226
+ setActiveItermSessionId(newSessionId);
227
+ log(`/n: created visual session "${name}" (iTerm2=${newSessionId})`);
228
+ ctx.reply(`New visual session: *${name}* (${targetPath})`).catch(() => { });
229
+ }
230
+ return;
231
+ }
232
+ log("/n: no path provided");
233
+ return;
234
+ }
235
+ // --- /sessions (aliases: /s) — list sessions ---
236
+ if (trimmedText === "/sessions" || trimmedText === "/s") {
237
+ if (hybridManager) {
238
+ // Prune dead visual sessions before listing
239
+ const liveSnapshots = snapshotAllSessions();
240
+ const liveIds = new Set(liveSnapshots.map(s => s.id));
241
+ hybridManager.pruneDeadVisualSessions(liveIds);
242
+ const list = hybridManager.formatSessionList();
243
+ ctx.reply(list).catch(() => { });
244
+ return;
245
+ }
246
+ ensureActiveSession();
247
+ const allSessions = cachedSessionList ?? getSessionList();
248
+ if (allSessions.length === 0 && sessionRegistry.size === 0) {
249
+ ctx.reply("No sessions found.").catch(() => { });
250
+ return;
251
+ }
252
+ const lines = allSessions.map((s, i) => {
253
+ const regEntry = [...sessionRegistry.values()].find((e) => e.itermSessionId === s.id);
254
+ const label = s.paiName
255
+ ?? (regEntry ? regEntry.name : null)
256
+ ?? (s.path ? basename(s.path) : null)
257
+ ?? s.name;
258
+ const typeTag = s.type === "terminal" ? " [terminal]" : "";
259
+ const isActive = activeItermSessionId
260
+ ? s.id === activeItermSessionId
261
+ : regEntry ? activeClientId === regEntry.sessionId : false;
262
+ return `${i + 1}. ${label}${typeTag}${isActive ? " \u2190 active" : ""}`;
263
+ });
264
+ ctx.reply(lines.join("\n")).catch(() => { });
265
+ return;
266
+ }
267
+ // --- /N [name] — switch to session N, optionally rename ---
268
+ const sessionSwitchMatch = trimmedText.match(/^\/(\d+)\s*(.*)?$/);
269
+ if (sessionSwitchMatch) {
270
+ const num = parseInt(sessionSwitchMatch[1], 10);
271
+ const newName = sessionSwitchMatch[2]?.trim() || null;
272
+ if (hybridManager) {
273
+ const session = hybridManager.switchToIndex(num);
274
+ if (!session) {
275
+ const count = hybridManager.listSessions().length;
276
+ ctx.reply(`Invalid session number. Use /s to list (1-${count}).`).catch(() => { });
277
+ return;
278
+ }
279
+ if (session.kind === "visual") {
280
+ setActiveItermSessionId(session.backendSessionId);
281
+ }
282
+ log(`/N: switched to ${session.kind} session "${session.name}" (${session.id})`);
283
+ const tag = session.kind === "api" ? " [api]" : " [visual]";
284
+ ctx.reply(`Switched to *${session.name}*${tag}`).catch(() => { });
285
+ return;
286
+ }
287
+ // Legacy fallback
288
+ const CACHE_TTL_MS = 60_000;
289
+ const sessions = cachedSessionList && (Date.now() - cachedSessionListTime < CACHE_TTL_MS)
290
+ ? cachedSessionList
291
+ : getSessionList();
292
+ if (sessions.length === 0) {
293
+ ctx.reply("No sessions found.").catch(() => { });
294
+ return;
295
+ }
296
+ if (num < 1 || num > sessions.length) {
297
+ ctx.reply(`Invalid session number. Use /s to list (1-${sessions.length}).`).catch(() => { });
298
+ return;
299
+ }
300
+ const chosen = sessions[num - 1];
301
+ const escapedSessionId = chosen.id.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
302
+ const focusScript = `
303
+ tell application "iTerm2"
304
+ repeat with aWindow in windows
305
+ repeat with aTab in tabs of aWindow
306
+ repeat with aSession in sessions of aTab
307
+ if id of aSession is "${escapedSessionId}" then
308
+ select aSession
309
+ return "focused"
310
+ end if
311
+ end repeat
312
+ end repeat
313
+ end repeat
314
+ return "not_found"
315
+ end tell`;
316
+ const focusResult = runAppleScript(focusScript);
317
+ if (focusResult === "focused") {
318
+ setActiveItermSessionId(chosen.id);
319
+ const regEntry = [...sessionRegistry.values()].find((e) => e.itermSessionId === chosen.id);
320
+ if (regEntry) {
321
+ setActiveClientId(regEntry.sessionId);
322
+ }
323
+ else {
324
+ setActiveClientId(null);
325
+ }
326
+ if (newName) {
327
+ setItermSessionVar(chosen.id, newName);
328
+ setItermTabName(chosen.id, newName);
329
+ setItermBadge(chosen.id, newName);
330
+ if (regEntry)
331
+ regEntry.name = newName;
332
+ }
333
+ const displayName = newName
334
+ ?? chosen.paiName
335
+ ?? (regEntry ? regEntry.name : null)
336
+ ?? (chosen.path ? basename(chosen.path) : chosen.name);
337
+ ctx.reply(`Switched to *${displayName}*`).catch(() => { });
338
+ }
339
+ else {
340
+ ctx.reply("Session not found — it may have closed.").catch(() => { });
341
+ }
342
+ return;
343
+ }
344
+ // --- /t [command] — open a raw terminal tab ---
345
+ if (trimmedText === "/t" || trimmedText === "/terminal") {
346
+ handleTerminal(null);
347
+ return;
348
+ }
349
+ const terminalMatch = trimmedText.match(/^\/(?:t|terminal)\s+(.+)$/);
350
+ if (terminalMatch) {
351
+ handleTerminal(terminalMatch[1].trim());
352
+ return;
353
+ }
354
+ // --- /restart — restart the adapter (adapter handles this locally) ---
355
+ // This is forwarded back to the originating adapter for local restart.
356
+ // The hub cannot restart adapters directly.
357
+ if (trimmedText === "/restart") {
358
+ log("/restart: forwarded to adapter");
359
+ ctx.reply("Restart command — handled by adapter.").catch(() => { });
360
+ return;
361
+ }
362
+ // --- /image, /img <prompt> — generate an image (always fresh, clears context) ---
363
+ const imageMatch = trimmedText.match(/^\/(?:image|img)\s+(.+)$/s);
364
+ if (imageMatch) {
365
+ const prompt = imageMatch[1].trim();
366
+ const imgCtxKey = buildImageContextKey(ctx.source, ctx.recipient, ctx.sessionId);
367
+ // Explicit /image command always starts fresh
368
+ clearImageContext(imgCtxKey);
369
+ ctx.typing(true);
370
+ ctx.reply("On it... generating your image.").catch(() => { });
371
+ (async () => {
372
+ try {
373
+ const { generateImage } = await import("./image-gen/index.js");
374
+ const result = await generateImage({ prompt });
375
+ ctx.typing(false);
376
+ if (result.images.length > 0) {
377
+ const imgBuf = result.images[0];
378
+ await ctx.replyImage(imgBuf, prompt.slice(0, 200));
379
+ // Store prompt + image in context for future refinements
380
+ setImageContext(imgCtxKey, appendPromptToContext(undefined, prompt, imgBuf, "image/png"));
381
+ }
382
+ }
383
+ catch (err) {
384
+ ctx.typing(false);
385
+ const errMsg = err instanceof Error ? err.message : String(err);
386
+ ctx.reply(`Image generation failed: ${errMsg}`).catch(() => { });
387
+ log(`/image: error — ${errMsg}`);
388
+ }
389
+ })().catch((err) => { ctx.typing(false); log(`/image: unhandled error — ${err}`); });
390
+ return;
391
+ }
392
+ // --- /ss, /screenshot ---
393
+ if (trimmedText === "/ss" || trimmedText === "/screenshot") {
394
+ if (hybridManager) {
395
+ const status = hybridManager.formatActiveStatus();
396
+ if (status !== null) {
397
+ ctx.reply(status).catch(() => { });
398
+ }
399
+ else {
400
+ handleScreenshot(ctx).catch((err) => log(`/ss: unhandled error — ${err}`));
401
+ }
402
+ return;
403
+ }
404
+ handleScreenshot(ctx).catch((err) => log(`/ss: unhandled error — ${err}`));
405
+ return;
406
+ }
407
+ // --- /c — clear active session context ---
408
+ if (trimmedText === "/c") {
409
+ if (hybridManager) {
410
+ const active = hybridManager.activeSession;
411
+ if (active?.kind === "api") {
412
+ hybridManager.clearActiveSession();
413
+ log("/c: cleared API session conversation history");
414
+ ctx.reply("Session cleared.").catch(() => { });
415
+ return;
416
+ }
417
+ }
418
+ ensureActiveSession();
419
+ if (!activeItermSessionId) {
420
+ ctx.reply("No active session.").catch(() => { });
421
+ return;
422
+ }
423
+ ctx.reply("Clearing in ~10s…").catch(() => { });
424
+ (async () => {
425
+ const sid = activeItermSessionId;
426
+ await new Promise((r) => setTimeout(r, 10000));
427
+ typeIntoSession(sid, "/clear");
428
+ await new Promise((r) => setTimeout(r, 8000));
429
+ pasteTextIntoSession(sid, "go");
430
+ await new Promise((r) => setTimeout(r, 500));
431
+ sendKeystrokeToSession(sid, 13);
432
+ ctx.reply("Sent /clear + go").catch(() => { });
433
+ })().catch((err) => log(`/c: error — ${err}`));
434
+ return;
435
+ }
436
+ // --- /p — send "pause session" to active Claude session ---
437
+ if (trimmedText === "/p") {
438
+ ensureActiveSession();
439
+ if (!activeItermSessionId) {
440
+ ctx.reply("No active session.").catch(() => { });
441
+ return;
442
+ }
443
+ typeIntoSession(activeItermSessionId, "pause session");
444
+ ctx.reply("Sent \"pause session\"").catch(() => { });
445
+ return;
446
+ }
447
+ // --- Keyboard control commands ---
448
+ if (trimmedText === "/cc" ||
449
+ trimmedText === "/esc" ||
450
+ trimmedText === "/enter" ||
451
+ trimmedText === "/tab" ||
452
+ trimmedText === "/up" ||
453
+ trimmedText === "/down" ||
454
+ trimmedText === "/left" ||
455
+ trimmedText === "/right" ||
456
+ /^\/pick\s+(\d+)/.test(trimmedText)) {
457
+ if (hybridManager?.activeSession?.kind === "api") {
458
+ ctx.reply("Keyboard commands need a visual session. Use /nv to create one.").catch(() => { });
459
+ return;
460
+ }
461
+ ensureActiveSession();
462
+ if (!activeItermSessionId) {
463
+ ctx.reply("No active session.").catch(() => { });
464
+ return;
465
+ }
466
+ if (trimmedText === "/cc") {
467
+ sendKeystrokeToSession(activeItermSessionId, 3);
468
+ ctx.reply("Ctrl+C sent").catch(() => { });
469
+ return;
470
+ }
471
+ if (trimmedText === "/esc") {
472
+ sendKeystrokeToSession(activeItermSessionId, 27);
473
+ ctx.reply("Esc sent").catch(() => { });
474
+ return;
475
+ }
476
+ if (trimmedText === "/enter") {
477
+ sendKeystrokeToSession(activeItermSessionId, 13);
478
+ ctx.reply("Enter sent").catch(() => { });
479
+ return;
480
+ }
481
+ if (trimmedText === "/tab") {
482
+ sendKeystrokeToSession(activeItermSessionId, 9);
483
+ ctx.reply("Tab sent").catch(() => { });
484
+ return;
485
+ }
486
+ if (trimmedText === "/up") {
487
+ sendEscapeSequenceToSession(activeItermSessionId, "A");
488
+ ctx.reply("\u2191").catch(() => { });
489
+ return;
490
+ }
491
+ if (trimmedText === "/down") {
492
+ sendEscapeSequenceToSession(activeItermSessionId, "B");
493
+ ctx.reply("\u2193").catch(() => { });
494
+ return;
495
+ }
496
+ if (trimmedText === "/left") {
497
+ sendEscapeSequenceToSession(activeItermSessionId, "D");
498
+ ctx.reply("\u2190").catch(() => { });
499
+ return;
500
+ }
501
+ if (trimmedText === "/right") {
502
+ sendEscapeSequenceToSession(activeItermSessionId, "C");
503
+ ctx.reply("\u2192").catch(() => { });
504
+ return;
505
+ }
506
+ const pickMatch = trimmedText.match(/^\/pick\s+(\d+)(?:\s+(.+))?$/);
507
+ if (pickMatch) {
508
+ const pickNum = parseInt(pickMatch[1], 10);
509
+ const pickText = pickMatch[2] || null;
510
+ if (pickNum < 1) {
511
+ ctx.reply("Pick number must be at least 1.").catch(() => { });
512
+ return;
513
+ }
514
+ const sessionId = activeItermSessionId;
515
+ (async () => {
516
+ for (let i = 0; i < pickNum - 1; i++) {
517
+ sendEscapeSequenceToSession(sessionId, "B");
518
+ await new Promise((r) => setTimeout(r, 50));
519
+ }
520
+ sendKeystrokeToSession(sessionId, 13);
521
+ if (pickText) {
522
+ await new Promise((r) => setTimeout(r, 200));
523
+ typeIntoSession(sessionId, pickText);
524
+ }
525
+ const msgText = pickText ? `Picked option ${pickNum}: ${pickText}` : `Picked option ${pickNum}`;
526
+ ctx.reply(msgText).catch(() => { });
527
+ })().catch((err) => log(`/pick: error — ${err}`));
528
+ return;
529
+ }
530
+ }
531
+ // --- /r N — restart Claude in session N ---
532
+ const restartMatch = trimmedText.match(/^\/(?:restart|r)\s+(\d+)$/);
533
+ if (restartMatch) {
534
+ const num = parseInt(restartMatch[1], 10);
535
+ const sessions = getSessionList();
536
+ if (sessions.length === 0) {
537
+ ctx.reply("No sessions found.").catch(() => { });
538
+ return;
539
+ }
540
+ if (num < 1 || num > sessions.length) {
541
+ ctx.reply(`Invalid session number. Use /s to list (1-${sessions.length}).`).catch(() => { });
542
+ return;
543
+ }
544
+ const target = sessions[num - 1];
545
+ if (target.type === "terminal") {
546
+ ctx.reply("Use /e to end terminal sessions.").catch(() => { });
547
+ }
548
+ else {
549
+ restartSession(target.id).catch((err) => log(`/r: error — ${err}`));
550
+ ctx.reply(`Restarting session ${num}…`).catch(() => { });
551
+ }
552
+ return;
553
+ }
554
+ // --- /e N — end session (close tab + remove from registry) ---
555
+ const endMatch = trimmedText.match(/^\/(?:end|e)\s+(\d+)$/);
556
+ if (endMatch) {
557
+ const num = parseInt(endMatch[1], 10);
558
+ if (hybridManager) {
559
+ const session = hybridManager.getByIndex(num);
560
+ if (!session) {
561
+ const count = hybridManager.listSessions().length;
562
+ ctx.reply(`Invalid session number. Use /s to list (1-${count}).`).catch(() => { });
563
+ return;
564
+ }
565
+ if (session.kind === "visual") {
566
+ const itermSessions = getSessionList();
567
+ const target = itermSessions.find(s => s.id === session.backendSessionId);
568
+ if (target) {
569
+ handleEndSessionVisual(target).catch((err) => log(`/e: error — ${err}`));
570
+ }
571
+ }
572
+ hybridManager.removeByIndex(num);
573
+ log(`/e: ended ${session.kind} session "${session.name}" (${session.id})`);
574
+ ctx.reply(`Ended session *${session.name}*.`).catch(() => { });
575
+ return;
576
+ }
577
+ const sessions = getSessionList();
578
+ if (sessions.length === 0) {
579
+ ctx.reply("No sessions found.").catch(() => { });
580
+ return;
581
+ }
582
+ if (num < 1 || num > sessions.length) {
583
+ ctx.reply(`Invalid session number. Use /s to list (1-${sessions.length}).`).catch(() => { });
584
+ return;
585
+ }
586
+ const target = sessions[num - 1];
587
+ handleEndSessionVisual(target).catch((err) => log(`/e: error — ${err}`));
588
+ ctx.reply(`Ended session *${target.paiName ?? target.name}*.`).catch(() => { });
589
+ return;
590
+ }
591
+ // --- /aibp <subcommand> — AIBP protocol inspection ---
592
+ if (trimmedText === "/aibp" || trimmedText.startsWith("/aibp ")) {
593
+ const sub = trimmedText.slice(5).trim() || "status";
594
+ const bridge = getAibpBridge();
595
+ if (!bridge) {
596
+ ctx.reply("AIBP bridge not initialized.").catch(() => { });
597
+ return;
598
+ }
599
+ switch (sub) {
600
+ case "status": {
601
+ // Combined overview: sessions + plugins + adapters
602
+ const snapshots = snapshotAllSessions();
603
+ const lines = ["*AIBP Status*", ""];
604
+ // Sessions
605
+ lines.push(`*Sessions (${snapshots.length})*`);
606
+ for (let i = 0; i < snapshots.length; i++) {
607
+ const snap = snapshots[i];
608
+ const label = snap.paiName ?? snap.tabTitle ?? snap.name;
609
+ const isActive = snap.id === activeItermSessionId;
610
+ const icon = snap.atPrompt ? "🟢" : "🟡";
611
+ const tag = isActive ? " ← active" : "";
612
+ lines.push(`${i + 1}. ${icon} *${label}*${tag}`);
613
+ const cached = statusCache.get(snap.id);
614
+ if (cached?.summary && Date.now() - cached.timestamp < 5 * 60 * 1000) {
615
+ lines.push(` _${cached.summary}_`);
616
+ }
617
+ }
618
+ lines.push("");
619
+ // Plugins
620
+ const plugins = bridge.registry.listPlugins();
621
+ lines.push(`*Plugins (${plugins.length})*`);
622
+ for (const p of plugins) {
623
+ const chCount = p.joinedChannels.size;
624
+ lines.push(`• *${p.spec.name}* [${p.spec.type}] — ${p.status}${chCount > 0 ? ` (${chCount} ch)` : ""}`);
625
+ }
626
+ lines.push("");
627
+ // Channels with activity
628
+ const channels = bridge.registry.listChannels();
629
+ const activeChannels = channels.filter(ch => ch.members.size > 0);
630
+ if (activeChannels.length > 0) {
631
+ lines.push(`*Channels (${activeChannels.length})*`);
632
+ for (const ch of activeChannels) {
633
+ const age = ch.lastMessageTs ? `${Math.round((Date.now() - ch.lastMessageTs) / 1000)}s ago` : "no activity";
634
+ const outbox = ch.outbox.length > 0 ? `, ${ch.outbox.length} buffered` : "";
635
+ lines.push(`• ${ch.channel} — ${ch.members.size} member(s), ${age}${outbox}`);
636
+ }
637
+ lines.push("");
638
+ }
639
+ // Peers (mesh)
640
+ const peers = bridge.listPeers();
641
+ if (peers.length > 0) {
642
+ lines.push(`*Peers (${peers.length})*`);
643
+ for (const p of peers)
644
+ lines.push(`• ${p}`);
645
+ }
646
+ ctx.reply(lines.join("\n")).catch(() => { });
647
+ return;
648
+ }
649
+ case "plugins": {
650
+ const plugins = bridge.registry.listPlugins();
651
+ const lines = [`*AIBP Plugins (${plugins.length})*`, ""];
652
+ for (const p of plugins) {
653
+ lines.push(`*${p.address}*`);
654
+ lines.push(` Type: ${p.spec.type} | Status: ${p.status}`);
655
+ lines.push(` Caps: ${p.spec.capabilities.join(", ")}`);
656
+ if (p.joinedChannels.size > 0) {
657
+ lines.push(` Channels: ${Array.from(p.joinedChannels).join(", ")}`);
658
+ }
659
+ if (p.spec.commands?.length) {
660
+ lines.push(` Commands: ${p.spec.commands.map(c => c.name).join(", ")}`);
661
+ }
662
+ lines.push("");
663
+ }
664
+ ctx.reply(lines.join("\n")).catch(() => { });
665
+ return;
666
+ }
667
+ case "channels": {
668
+ const channels = bridge.registry.listChannels();
669
+ const lines = [`*AIBP Channels (${channels.length})*`, ""];
670
+ for (const ch of channels) {
671
+ const members = Array.from(ch.members).join(", ") || "(empty)";
672
+ const age = ch.lastMessageTs ? `last: ${Math.round((Date.now() - ch.lastMessageTs) / 1000)}s ago` : "no activity";
673
+ lines.push(`*${ch.channel}*`);
674
+ lines.push(` Members: ${members}`);
675
+ lines.push(` ${age}${ch.outbox.length > 0 ? ` | outbox: ${ch.outbox.length}` : ""}`);
676
+ lines.push("");
677
+ }
678
+ ctx.reply(lines.join("\n")).catch(() => { });
679
+ return;
680
+ }
681
+ case "commands": {
682
+ const commands = bridge.listCommands();
683
+ const lines = [`*AIBP Commands (${commands.length})*`, ""];
684
+ // Group by owner
685
+ const byOwner = new Map();
686
+ for (const cmd of commands) {
687
+ const group = byOwner.get(cmd.owner) ?? [];
688
+ group.push(cmd);
689
+ byOwner.set(cmd.owner, group);
690
+ }
691
+ for (const [owner, cmds] of byOwner) {
692
+ lines.push(`*${owner}*`);
693
+ for (const c of cmds) {
694
+ const desc = c.spec?.description ? ` — ${c.spec.description}` : "";
695
+ const args = c.spec?.args ? ` ${c.spec.args}` : "";
696
+ lines.push(` /${c.name}${args}${desc}`);
697
+ }
698
+ lines.push("");
699
+ }
700
+ ctx.reply(lines.join("\n")).catch(() => { });
701
+ return;
702
+ }
703
+ case "peers": {
704
+ const peers = bridge.listPeers();
705
+ if (peers.length === 0) {
706
+ ctx.reply("No mesh peers connected.").catch(() => { });
707
+ }
708
+ else {
709
+ const lines = [`*AIBP Mesh Peers (${peers.length})*`, ""];
710
+ for (const p of peers)
711
+ lines.push(`• ${p}`);
712
+ ctx.reply(lines.join("\n")).catch(() => { });
713
+ }
714
+ return;
715
+ }
716
+ case "help": {
717
+ ctx.reply([
718
+ "*AIBP Commands*",
719
+ "",
720
+ "/aibp status — Overview (sessions, plugins, channels)",
721
+ "/aibp plugins — Registered plugins with capabilities",
722
+ "/aibp channels — Active channels with members",
723
+ "/aibp commands — All registered commands by owner",
724
+ "/aibp peers — Mesh network peers",
725
+ "/aibp help — This help",
726
+ ].join("\n")).catch(() => { });
727
+ return;
728
+ }
729
+ default:
730
+ ctx.reply(`Unknown subcommand: ${sub}. Try /aibp help`).catch(() => { });
731
+ return;
732
+ }
733
+ }
734
+ // --- /status, /st — alias for /aibp status ---
735
+ if (trimmedText === "/status" || trimmedText === "/st") {
736
+ return handleMessage("/aibp status", timestamp, ctx);
737
+ }
738
+ // --- Image refinement pre-check ---
739
+ // If the user has an active image context, short messages like "with a tie"
740
+ // or "make it watercolor" should be caught as refinements even if they don't
741
+ // match IMAGE_REQUEST_PATTERNS (which require "send/show/make me an image...").
742
+ const imgCtxKey = buildImageContextKey(ctx.source, ctx.recipient, ctx.sessionId);
743
+ const existingImageCtx = getImageContext(imgCtxKey);
744
+ const imageNlMatch = detectImageRequest(trimmedText);
745
+ // Refinement shortcut: if image context exists and no NL match, try classifying
746
+ // the raw message. If it classifies as "refine", intercept it.
747
+ let forceRefinement = false;
748
+ if (!imageNlMatch && existingImageCtx && existingImageCtx.promptHistory.length > 0) {
749
+ const preCheck = classifyImageRequest(trimmedText, existingImageCtx);
750
+ if (preCheck.type === "refine") {
751
+ forceRefinement = true;
752
+ }
753
+ }
754
+ if (imageNlMatch || forceRefinement) {
755
+ const imageCtx = existingImageCtx;
756
+ const classification = imageNlMatch
757
+ ? classifyImageRequest(imageNlMatch, imageCtx)
758
+ : classifyImageRequest(trimmedText, imageCtx);
759
+ // Only intercept if the classifier found an actual image signal.
760
+ // "unrelated" falls through to normal routing (Claude, slash commands, etc.).
761
+ if (classification.type !== "unrelated") {
762
+ if (classification.type === "new") {
763
+ clearImageContext(imgCtxKey);
764
+ }
765
+ ctx.typing(true);
766
+ ctx.reply("On it... generating your image.").catch(() => { });
767
+ (async () => {
768
+ try {
769
+ const { generateImage, getConfiguredProvider } = await import("./image-gen/index.js");
770
+ let imgBuf;
771
+ let effectivePrompt;
772
+ if (classification.type === "refine" && imageCtx) {
773
+ const updatedHistory = [...imageCtx.promptHistory, classification.prompt];
774
+ effectivePrompt = buildChainedPrompt(updatedHistory);
775
+ // Prefer native img2img if the provider supports it and we have a previous image
776
+ const provider = getConfiguredProvider();
777
+ if (provider.refine && imageCtx.lastImage) {
778
+ log(`image-gen: using native img2img (refine) — provider=${provider.name}`);
779
+ const result = await provider.refine({
780
+ prompt: classification.prompt,
781
+ sourceImage: imageCtx.lastImage,
782
+ sourceMime: imageCtx.lastImageMime,
783
+ strength: 0.7,
784
+ });
785
+ imgBuf = result.images[0];
786
+ }
787
+ else {
788
+ log(`image-gen: refining via prompt chaining — "${effectivePrompt.slice(0, 80)}"`);
789
+ const result = await generateImage({ prompt: effectivePrompt });
790
+ imgBuf = result.images[0];
791
+ }
792
+ await ctx.replyImage(imgBuf, classification.prompt.slice(0, 200));
793
+ setImageContext(imgCtxKey, appendPromptToContext(imageCtx, classification.prompt, imgBuf, "image/png"));
794
+ }
795
+ else {
796
+ // New image
797
+ effectivePrompt = classification.prompt;
798
+ const result = await generateImage({ prompt: effectivePrompt });
799
+ imgBuf = result.images[0];
800
+ await ctx.replyImage(imgBuf, effectivePrompt.slice(0, 200));
801
+ setImageContext(imgCtxKey, appendPromptToContext(undefined, effectivePrompt, imgBuf, "image/png"));
802
+ }
803
+ ctx.typing(false);
804
+ }
805
+ catch (err) {
806
+ ctx.typing(false);
807
+ const errMsg = err instanceof Error ? err.message : String(err);
808
+ ctx.reply(`Image generation failed: ${errMsg}`).catch(() => { });
809
+ log(`image-gen: error — ${errMsg}`);
810
+ }
811
+ })().catch((err) => { ctx.typing(false); log(`image-gen: unhandled error — ${err}`); });
812
+ return;
813
+ }
814
+ }
815
+ // --- Plain text / unrecognized commands → dispatch to iTerm2 ---
816
+ dispatchIncomingMessage(text, timestamp);
817
+ // Prefix with adapter source tag so Claude knows where to reply
818
+ const tag = ctx.source === "pailot"
819
+ ? "PAILot"
820
+ : ctx.source === "telex"
821
+ ? "Telex"
822
+ : "Whazaa";
823
+ let textToDeliver;
824
+ if (trimmedText.startsWith("!")) {
825
+ textToDeliver = text.replace(/^!/, "");
826
+ }
827
+ else if (trimmedText.startsWith("/")) {
828
+ textToDeliver = text;
829
+ }
830
+ else if (/^\[(?:Voice note|Audio)\]:/.test(trimmedText)) {
831
+ textToDeliver = `[${tag}:voice] ${text}`;
832
+ }
833
+ else {
834
+ textToDeliver = `[${tag}] ${text}`;
835
+ }
836
+ // Route based on active session kind
837
+ const activeHybrid = hybridManager?.activeSession;
838
+ if (activeHybrid?.kind === "api") {
839
+ deliverViaApi(hybridManager.apiBackend, textToDeliver, activeHybrid.backendSessionId, {
840
+ sendText: (replyText) => ctx.reply(replyText),
841
+ sendVoice: (buffer, transcript) => ctx.replyVoice(buffer, transcript ?? ""),
842
+ });
843
+ return;
844
+ }
845
+ // Visual session — deliver to iTerm2
846
+ deliverMessage(textToDeliver);
847
+ };
848
+ }
849
+ //# sourceMappingURL=commands.js.map