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,632 @@
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, } from "../core/state.js";
13
+ import { getSessionList, setItermSessionVar, setItermTabName, 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 { deliverViaApi } from "../core/transport.js";
17
+ import { hybridManager } from "../core/hybrid.js";
18
+ import { handleScreenshot } from "./screenshot.js";
19
+ /**
20
+ * Detect natural language image generation requests.
21
+ * Returns the extracted prompt if matched, null otherwise.
22
+ */
23
+ const IMAGE_REQUEST_PATTERNS = [
24
+ /^(?:send|show|give|create|make|draw|paint|render|generate)\s+(?:me\s+)?(?:an?\s+)?(?:image|picture|photo|illustration|drawing|painting)\s+(?:of\s+)?(.+)/i,
25
+ /^(?:schick|zeig|mach|erstell|generier|mal)\s+(?:mir\s+)?(?:ein(?:e|en)?\s+)?(?:bild|foto|zeichnung|illustration)\s+(?:von\s+)?(.+)/i,
26
+ ];
27
+ function detectImageRequest(text) {
28
+ for (const pattern of IMAGE_REQUEST_PATTERNS) {
29
+ const match = text.match(pattern);
30
+ if (match?.[1])
31
+ return match[1].trim();
32
+ }
33
+ return null;
34
+ }
35
+ /**
36
+ * Create the hub command handler.
37
+ *
38
+ * Returns a function that processes incoming messages (slash commands or
39
+ * plain text) and routes them to iTerm2 sessions or API backends.
40
+ *
41
+ * The `ctx` parameter is provided per-message by the hub routing engine,
42
+ * so the command handler knows how to reply to the originating adapter.
43
+ */
44
+ export function createHubCommandHandler() {
45
+ // ── Session resolution ──
46
+ function ensureActiveSession() {
47
+ const allSessions = getSessionList();
48
+ const allSessionIds = new Set(allSessions.map((s) => s.id));
49
+ for (const [sid, entry] of sessionRegistry) {
50
+ if (entry.itermSessionId && !allSessionIds.has(entry.itermSessionId)) {
51
+ sessionRegistry.delete(sid);
52
+ clientQueues.delete(sid);
53
+ if (activeClientId === sid) {
54
+ const remaining = [...sessionRegistry.values()].sort((a, b) => b.registeredAt - a.registeredAt);
55
+ setActiveClientId(remaining.length > 0 ? remaining[0].sessionId : null);
56
+ }
57
+ }
58
+ }
59
+ if (activeItermSessionId && !allSessionIds.has(activeItermSessionId)) {
60
+ log(`ensureActiveSession: ${activeItermSessionId.slice(0, 8)}… not in live sessions — clearing`);
61
+ setActiveItermSessionId("");
62
+ }
63
+ if (!activeItermSessionId && allSessions.length > 0) {
64
+ const busy = allSessions.find((s) => s.type === "claude" && !s.atPrompt);
65
+ const anyClaudeSession = allSessions.find((s) => s.type === "claude");
66
+ const pick = busy ?? anyClaudeSession ?? allSessions[0];
67
+ if (pick) {
68
+ setActiveItermSessionId(pick.id);
69
+ log(`ensureActiveSession: auto-selected ${pick.name} (${pick.id.slice(0, 8)}…)`);
70
+ }
71
+ }
72
+ setCachedSessionList(allSessions, Date.now());
73
+ return activeItermSessionId;
74
+ }
75
+ // ── Message delivery to iTerm2 ──
76
+ function deliverMessage(text) {
77
+ const bareSessionId = stripItermPrefix(activeItermSessionId) ?? activeItermSessionId;
78
+ if (bareSessionId && managedSessions.has(bareSessionId)) {
79
+ if (typeIntoSession(bareSessionId, text))
80
+ return true;
81
+ managedSessions.delete(bareSessionId);
82
+ }
83
+ if (activeItermSessionId) {
84
+ if (typeIntoSession(activeItermSessionId, text))
85
+ return true;
86
+ }
87
+ if (isScreenLocked()) {
88
+ const targetId = bareSessionId || activeItermSessionId;
89
+ let ttyPath = targetId ? sessionTtyCache.get(targetId) : undefined;
90
+ if (!ttyPath) {
91
+ log("Screen locked — TTY cache miss, attempting live snapshot refresh");
92
+ const fresh = snapshotAllSessions();
93
+ updateSessionTtyCache(fresh);
94
+ ttyPath = targetId ? sessionTtyCache.get(targetId) : undefined;
95
+ if (!ttyPath && fresh.length > 0) {
96
+ ttyPath = fresh[0].tty;
97
+ log(`Screen locked — falling back to first available TTY: ${ttyPath}`);
98
+ }
99
+ }
100
+ if (ttyPath) {
101
+ log(`Screen locked — PTY write fallback to ${ttyPath}`);
102
+ if (writeToTty(ttyPath, text))
103
+ return true;
104
+ log(`PTY write fallback failed for ${ttyPath}`);
105
+ }
106
+ else {
107
+ log("Screen locked — no TTY available for PTY fallback");
108
+ }
109
+ return false;
110
+ }
111
+ log(`${activeItermSessionId ? `Session ${activeItermSessionId} is not running Claude.` : "No cached session."} Searching for another...`);
112
+ const found = findClaudeSession();
113
+ if (found && isClaudeRunningInSession(found)) {
114
+ setActiveItermSessionId(found);
115
+ if (typeIntoSession(found, text))
116
+ return true;
117
+ }
118
+ log("No running Claude session found. Starting new one...");
119
+ const created = createClaudeSession();
120
+ if (created) {
121
+ setActiveItermSessionId(created);
122
+ if (typeIntoSession(created, text))
123
+ return true;
124
+ }
125
+ log("Failed to deliver message");
126
+ return false;
127
+ }
128
+ // ── Terminal tab handling ──
129
+ function handleTerminal(command) {
130
+ const newId = createTerminalTab(command ?? undefined);
131
+ if (newId) {
132
+ managedSessions.set(newId, { name: command ?? "terminal", createdAt: Date.now() });
133
+ setActiveItermSessionId(newId);
134
+ log(`/t: created terminal tab ${newId}`);
135
+ }
136
+ }
137
+ // ── End session (close iTerm2 tab + cleanup) ──
138
+ async function handleEndSessionVisual(session) {
139
+ killSession(session.id);
140
+ // Cleanup registry entries pointing to this iTerm session
141
+ for (const [sid, entry] of sessionRegistry) {
142
+ if (entry.itermSessionId === session.id) {
143
+ sessionRegistry.delete(sid);
144
+ clientQueues.delete(sid);
145
+ if (activeClientId === sid)
146
+ setActiveClientId(null);
147
+ }
148
+ }
149
+ managedSessions.delete(session.id);
150
+ if (activeItermSessionId === session.id)
151
+ setActiveItermSessionId("");
152
+ log(`Ended session ${session.id} ("${session.paiName ?? session.name}")`);
153
+ }
154
+ // ── Relocate (new visual session) ──
155
+ function handleRelocate(targetPath) {
156
+ const command = `claude --dangerously-skip-permissions`;
157
+ const newId = createClaudeSession(`cd ${targetPath} && ${command}`);
158
+ return newId;
159
+ }
160
+ // ── Main command handler ──
161
+ return function handleMessage(text, timestamp, ctx) {
162
+ const trimmedText = text.trim();
163
+ // --- /h, /help ---
164
+ if (trimmedText === "/h" || trimmedText === "/help") {
165
+ const help = [
166
+ "*Commands*",
167
+ "",
168
+ "*Sessions*",
169
+ "/s — List sessions",
170
+ "/N — Switch to session N",
171
+ "/N name — Switch & rename",
172
+ "/n path — New visual session (iTerm2)",
173
+ "/nh path — New headless session",
174
+ "/t [cmd] — Open terminal tab",
175
+ "/r N — Restart Claude in session N",
176
+ "/e N — End session (close tab)",
177
+ "",
178
+ "*Session control*",
179
+ "/c — Send /clear + go to Claude",
180
+ "/p — Send \"pause session\" to Claude",
181
+ "/ss — Screenshot",
182
+ "",
183
+ "*Media*",
184
+ "/image <prompt> — Generate an image",
185
+ "",
186
+ "*Watcher*",
187
+ "/restart — Restart the adapter",
188
+ "",
189
+ "*Keys*",
190
+ "/cc — Ctrl+C",
191
+ "/esc — Escape",
192
+ "/enter — Enter",
193
+ "/tab — Tab",
194
+ "/up /down /left /right — Arrows",
195
+ "/pick N [text] — Menu select",
196
+ ].join("\n");
197
+ ctx.reply(help).catch(() => { });
198
+ return;
199
+ }
200
+ // --- /nh <path> — new headless (API) session ---
201
+ const nhMatch = trimmedText.match(/^\/nh\s+(.+)$/);
202
+ if (nhMatch) {
203
+ const targetPath = nhMatch[1].trim();
204
+ if (targetPath && hybridManager) {
205
+ const name = basename(targetPath);
206
+ const session = hybridManager.createApiSession(name, targetPath);
207
+ log(`/nh: created API session "${session.name}" (${session.id}) cwd=${session.cwd}`);
208
+ ctx.reply(`New headless session: *${session.name}* (${session.cwd})`).catch(() => { });
209
+ }
210
+ return;
211
+ }
212
+ // --- /n <path> (aliases: /nv, /new, /relocate) — new visual session ---
213
+ const relocateMatch = trimmedText.match(/^\/(?:n|nv|new|relocate)\s+(.+)$/);
214
+ if (relocateMatch) {
215
+ const targetPath = relocateMatch[1].trim();
216
+ if (targetPath) {
217
+ const newSessionId = handleRelocate(targetPath);
218
+ if (newSessionId) {
219
+ const name = basename(targetPath);
220
+ if (hybridManager) {
221
+ hybridManager.registerVisualSession(name, targetPath, newSessionId);
222
+ }
223
+ setActiveItermSessionId(newSessionId);
224
+ log(`/n: created visual session "${name}" (iTerm2=${newSessionId})`);
225
+ ctx.reply(`New visual session: *${name}* (${targetPath})`).catch(() => { });
226
+ }
227
+ return;
228
+ }
229
+ log("/n: no path provided");
230
+ return;
231
+ }
232
+ // --- /sessions (aliases: /s) — list sessions ---
233
+ if (trimmedText === "/sessions" || trimmedText === "/s") {
234
+ if (hybridManager) {
235
+ // Prune dead visual sessions before listing
236
+ const liveSnapshots = snapshotAllSessions();
237
+ const liveIds = new Set(liveSnapshots.map(s => s.id));
238
+ hybridManager.pruneDeadVisualSessions(liveIds);
239
+ const list = hybridManager.formatSessionList();
240
+ ctx.reply(list).catch(() => { });
241
+ return;
242
+ }
243
+ ensureActiveSession();
244
+ const allSessions = cachedSessionList ?? getSessionList();
245
+ if (allSessions.length === 0 && sessionRegistry.size === 0) {
246
+ ctx.reply("No sessions found.").catch(() => { });
247
+ return;
248
+ }
249
+ const lines = allSessions.map((s, i) => {
250
+ const regEntry = [...sessionRegistry.values()].find((e) => e.itermSessionId === s.id);
251
+ const label = s.paiName
252
+ ?? (regEntry ? regEntry.name : null)
253
+ ?? (s.path ? basename(s.path) : null)
254
+ ?? s.name;
255
+ const typeTag = s.type === "terminal" ? " [terminal]" : "";
256
+ const isActive = activeItermSessionId
257
+ ? s.id === activeItermSessionId
258
+ : regEntry ? activeClientId === regEntry.sessionId : false;
259
+ return `${i + 1}. ${label}${typeTag}${isActive ? " \u2190 active" : ""}`;
260
+ });
261
+ ctx.reply(lines.join("\n")).catch(() => { });
262
+ return;
263
+ }
264
+ // --- /N [name] — switch to session N, optionally rename ---
265
+ const sessionSwitchMatch = trimmedText.match(/^\/(\d+)\s*(.*)?$/);
266
+ if (sessionSwitchMatch) {
267
+ const num = parseInt(sessionSwitchMatch[1], 10);
268
+ const newName = sessionSwitchMatch[2]?.trim() || null;
269
+ if (hybridManager) {
270
+ const session = hybridManager.switchToIndex(num);
271
+ if (!session) {
272
+ const count = hybridManager.listSessions().length;
273
+ ctx.reply(`Invalid session number. Use /s to list (1-${count}).`).catch(() => { });
274
+ return;
275
+ }
276
+ if (session.kind === "visual") {
277
+ setActiveItermSessionId(session.backendSessionId);
278
+ }
279
+ log(`/N: switched to ${session.kind} session "${session.name}" (${session.id})`);
280
+ const tag = session.kind === "api" ? " [api]" : " [visual]";
281
+ ctx.reply(`Switched to *${session.name}*${tag}`).catch(() => { });
282
+ return;
283
+ }
284
+ // Legacy fallback
285
+ const CACHE_TTL_MS = 60_000;
286
+ const sessions = cachedSessionList && (Date.now() - cachedSessionListTime < CACHE_TTL_MS)
287
+ ? cachedSessionList
288
+ : getSessionList();
289
+ if (sessions.length === 0) {
290
+ ctx.reply("No sessions found.").catch(() => { });
291
+ return;
292
+ }
293
+ if (num < 1 || num > sessions.length) {
294
+ ctx.reply(`Invalid session number. Use /s to list (1-${sessions.length}).`).catch(() => { });
295
+ return;
296
+ }
297
+ const chosen = sessions[num - 1];
298
+ const escapedSessionId = chosen.id.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
299
+ const focusScript = `
300
+ tell application "iTerm2"
301
+ repeat with aWindow in windows
302
+ repeat with aTab in tabs of aWindow
303
+ repeat with aSession in sessions of aTab
304
+ if id of aSession is "${escapedSessionId}" then
305
+ select aSession
306
+ return "focused"
307
+ end if
308
+ end repeat
309
+ end repeat
310
+ end repeat
311
+ return "not_found"
312
+ end tell`;
313
+ const focusResult = runAppleScript(focusScript);
314
+ if (focusResult === "focused") {
315
+ setActiveItermSessionId(chosen.id);
316
+ const regEntry = [...sessionRegistry.values()].find((e) => e.itermSessionId === chosen.id);
317
+ if (regEntry) {
318
+ setActiveClientId(regEntry.sessionId);
319
+ }
320
+ else {
321
+ setActiveClientId(null);
322
+ }
323
+ if (newName) {
324
+ setItermSessionVar(chosen.id, newName);
325
+ setItermTabName(chosen.id, newName);
326
+ if (regEntry)
327
+ regEntry.name = newName;
328
+ }
329
+ const displayName = newName
330
+ ?? chosen.paiName
331
+ ?? (regEntry ? regEntry.name : null)
332
+ ?? (chosen.path ? basename(chosen.path) : chosen.name);
333
+ ctx.reply(`Switched to *${displayName}*`).catch(() => { });
334
+ }
335
+ else {
336
+ ctx.reply("Session not found — it may have closed.").catch(() => { });
337
+ }
338
+ return;
339
+ }
340
+ // --- /t [command] — open a raw terminal tab ---
341
+ if (trimmedText === "/t" || trimmedText === "/terminal") {
342
+ handleTerminal(null);
343
+ return;
344
+ }
345
+ const terminalMatch = trimmedText.match(/^\/(?:t|terminal)\s+(.+)$/);
346
+ if (terminalMatch) {
347
+ handleTerminal(terminalMatch[1].trim());
348
+ return;
349
+ }
350
+ // --- /restart — restart the adapter (adapter handles this locally) ---
351
+ // This is forwarded back to the originating adapter for local restart.
352
+ // The hub cannot restart adapters directly.
353
+ if (trimmedText === "/restart") {
354
+ log("/restart: forwarded to adapter");
355
+ ctx.reply("Restart command — handled by adapter.").catch(() => { });
356
+ return;
357
+ }
358
+ // --- /image, /img <prompt> — generate an image ---
359
+ const imageMatch = trimmedText.match(/^\/(?:image|img)\s+(.+)$/s);
360
+ if (imageMatch) {
361
+ const prompt = imageMatch[1].trim();
362
+ ctx.reply("On it... generating your image.").catch(() => { });
363
+ (async () => {
364
+ try {
365
+ const { generateImage } = await import("./image-gen.js");
366
+ const result = await generateImage({ prompt });
367
+ if (result.images.length > 0) {
368
+ await ctx.replyImage(result.images[0], prompt.slice(0, 200));
369
+ }
370
+ }
371
+ catch (err) {
372
+ const errMsg = err instanceof Error ? err.message : String(err);
373
+ ctx.reply(`Image generation failed: ${errMsg}`).catch(() => { });
374
+ log(`/image: error — ${errMsg}`);
375
+ }
376
+ })().catch((err) => log(`/image: unhandled error — ${err}`));
377
+ return;
378
+ }
379
+ // --- /ss, /screenshot ---
380
+ if (trimmedText === "/ss" || trimmedText === "/screenshot") {
381
+ if (hybridManager) {
382
+ const status = hybridManager.formatActiveStatus();
383
+ if (status !== null) {
384
+ ctx.reply(status).catch(() => { });
385
+ }
386
+ else {
387
+ handleScreenshot(ctx).catch((err) => log(`/ss: unhandled error — ${err}`));
388
+ }
389
+ return;
390
+ }
391
+ handleScreenshot(ctx).catch((err) => log(`/ss: unhandled error — ${err}`));
392
+ return;
393
+ }
394
+ // --- /c — clear active session context ---
395
+ if (trimmedText === "/c") {
396
+ if (hybridManager) {
397
+ const active = hybridManager.activeSession;
398
+ if (active?.kind === "api") {
399
+ hybridManager.clearActiveSession();
400
+ log("/c: cleared API session conversation history");
401
+ ctx.reply("Session cleared.").catch(() => { });
402
+ return;
403
+ }
404
+ }
405
+ ensureActiveSession();
406
+ if (!activeItermSessionId) {
407
+ ctx.reply("No active session.").catch(() => { });
408
+ return;
409
+ }
410
+ ctx.reply("Clearing in ~10s…").catch(() => { });
411
+ (async () => {
412
+ const sid = activeItermSessionId;
413
+ await new Promise((r) => setTimeout(r, 10000));
414
+ typeIntoSession(sid, "/clear");
415
+ await new Promise((r) => setTimeout(r, 8000));
416
+ pasteTextIntoSession(sid, "go");
417
+ await new Promise((r) => setTimeout(r, 500));
418
+ sendKeystrokeToSession(sid, 13);
419
+ ctx.reply("Sent /clear + go").catch(() => { });
420
+ })().catch((err) => log(`/c: error — ${err}`));
421
+ return;
422
+ }
423
+ // --- /p — send "pause session" to active Claude session ---
424
+ if (trimmedText === "/p") {
425
+ ensureActiveSession();
426
+ if (!activeItermSessionId) {
427
+ ctx.reply("No active session.").catch(() => { });
428
+ return;
429
+ }
430
+ typeIntoSession(activeItermSessionId, "pause session");
431
+ ctx.reply("Sent \"pause session\"").catch(() => { });
432
+ return;
433
+ }
434
+ // --- Keyboard control commands ---
435
+ if (trimmedText === "/cc" ||
436
+ trimmedText === "/esc" ||
437
+ trimmedText === "/enter" ||
438
+ trimmedText === "/tab" ||
439
+ trimmedText === "/up" ||
440
+ trimmedText === "/down" ||
441
+ trimmedText === "/left" ||
442
+ trimmedText === "/right" ||
443
+ /^\/pick\s+(\d+)/.test(trimmedText)) {
444
+ if (hybridManager?.activeSession?.kind === "api") {
445
+ ctx.reply("Keyboard commands need a visual session. Use /nv to create one.").catch(() => { });
446
+ return;
447
+ }
448
+ ensureActiveSession();
449
+ if (!activeItermSessionId) {
450
+ ctx.reply("No active session.").catch(() => { });
451
+ return;
452
+ }
453
+ if (trimmedText === "/cc") {
454
+ sendKeystrokeToSession(activeItermSessionId, 3);
455
+ ctx.reply("Ctrl+C sent").catch(() => { });
456
+ return;
457
+ }
458
+ if (trimmedText === "/esc") {
459
+ sendKeystrokeToSession(activeItermSessionId, 27);
460
+ ctx.reply("Esc sent").catch(() => { });
461
+ return;
462
+ }
463
+ if (trimmedText === "/enter") {
464
+ sendKeystrokeToSession(activeItermSessionId, 13);
465
+ ctx.reply("Enter sent").catch(() => { });
466
+ return;
467
+ }
468
+ if (trimmedText === "/tab") {
469
+ sendKeystrokeToSession(activeItermSessionId, 9);
470
+ ctx.reply("Tab sent").catch(() => { });
471
+ return;
472
+ }
473
+ if (trimmedText === "/up") {
474
+ sendEscapeSequenceToSession(activeItermSessionId, "A");
475
+ ctx.reply("\u2191").catch(() => { });
476
+ return;
477
+ }
478
+ if (trimmedText === "/down") {
479
+ sendEscapeSequenceToSession(activeItermSessionId, "B");
480
+ ctx.reply("\u2193").catch(() => { });
481
+ return;
482
+ }
483
+ if (trimmedText === "/left") {
484
+ sendEscapeSequenceToSession(activeItermSessionId, "D");
485
+ ctx.reply("\u2190").catch(() => { });
486
+ return;
487
+ }
488
+ if (trimmedText === "/right") {
489
+ sendEscapeSequenceToSession(activeItermSessionId, "C");
490
+ ctx.reply("\u2192").catch(() => { });
491
+ return;
492
+ }
493
+ const pickMatch = trimmedText.match(/^\/pick\s+(\d+)(?:\s+(.+))?$/);
494
+ if (pickMatch) {
495
+ const pickNum = parseInt(pickMatch[1], 10);
496
+ const pickText = pickMatch[2] || null;
497
+ if (pickNum < 1) {
498
+ ctx.reply("Pick number must be at least 1.").catch(() => { });
499
+ return;
500
+ }
501
+ const sessionId = activeItermSessionId;
502
+ (async () => {
503
+ for (let i = 0; i < pickNum - 1; i++) {
504
+ sendEscapeSequenceToSession(sessionId, "B");
505
+ await new Promise((r) => setTimeout(r, 50));
506
+ }
507
+ sendKeystrokeToSession(sessionId, 13);
508
+ if (pickText) {
509
+ await new Promise((r) => setTimeout(r, 200));
510
+ typeIntoSession(sessionId, pickText);
511
+ }
512
+ const msgText = pickText ? `Picked option ${pickNum}: ${pickText}` : `Picked option ${pickNum}`;
513
+ ctx.reply(msgText).catch(() => { });
514
+ })().catch((err) => log(`/pick: error — ${err}`));
515
+ return;
516
+ }
517
+ }
518
+ // --- /r N — restart Claude in session N ---
519
+ const restartMatch = trimmedText.match(/^\/(?:restart|r)\s+(\d+)$/);
520
+ if (restartMatch) {
521
+ const num = parseInt(restartMatch[1], 10);
522
+ const sessions = getSessionList();
523
+ if (sessions.length === 0) {
524
+ ctx.reply("No sessions found.").catch(() => { });
525
+ return;
526
+ }
527
+ if (num < 1 || num > sessions.length) {
528
+ ctx.reply(`Invalid session number. Use /s to list (1-${sessions.length}).`).catch(() => { });
529
+ return;
530
+ }
531
+ const target = sessions[num - 1];
532
+ if (target.type === "terminal") {
533
+ ctx.reply("Use /e to end terminal sessions.").catch(() => { });
534
+ }
535
+ else {
536
+ restartSession(target.id).catch((err) => log(`/r: error — ${err}`));
537
+ ctx.reply(`Restarting session ${num}…`).catch(() => { });
538
+ }
539
+ return;
540
+ }
541
+ // --- /e N — end session (close tab + remove from registry) ---
542
+ const endMatch = trimmedText.match(/^\/(?:end|e)\s+(\d+)$/);
543
+ if (endMatch) {
544
+ const num = parseInt(endMatch[1], 10);
545
+ if (hybridManager) {
546
+ const session = hybridManager.getByIndex(num);
547
+ if (!session) {
548
+ const count = hybridManager.listSessions().length;
549
+ ctx.reply(`Invalid session number. Use /s to list (1-${count}).`).catch(() => { });
550
+ return;
551
+ }
552
+ if (session.kind === "visual") {
553
+ const itermSessions = getSessionList();
554
+ const target = itermSessions.find(s => s.id === session.backendSessionId);
555
+ if (target) {
556
+ handleEndSessionVisual(target).catch((err) => log(`/e: error — ${err}`));
557
+ }
558
+ }
559
+ hybridManager.removeByIndex(num);
560
+ log(`/e: ended ${session.kind} session "${session.name}" (${session.id})`);
561
+ ctx.reply(`Ended session *${session.name}*.`).catch(() => { });
562
+ return;
563
+ }
564
+ const sessions = getSessionList();
565
+ if (sessions.length === 0) {
566
+ ctx.reply("No sessions found.").catch(() => { });
567
+ return;
568
+ }
569
+ if (num < 1 || num > sessions.length) {
570
+ ctx.reply(`Invalid session number. Use /s to list (1-${sessions.length}).`).catch(() => { });
571
+ return;
572
+ }
573
+ const target = sessions[num - 1];
574
+ handleEndSessionVisual(target).catch((err) => log(`/e: error — ${err}`));
575
+ ctx.reply(`Ended session *${target.paiName ?? target.name}*.`).catch(() => { });
576
+ return;
577
+ }
578
+ // --- Natural language image generation detection ---
579
+ const imageNlMatch = detectImageRequest(trimmedText);
580
+ if (imageNlMatch) {
581
+ ctx.reply("On it... generating your image.").catch(() => { });
582
+ (async () => {
583
+ try {
584
+ const { generateImage } = await import("./image-gen.js");
585
+ const result = await generateImage({ prompt: imageNlMatch });
586
+ if (result.images.length > 0) {
587
+ await ctx.replyImage(result.images[0], imageNlMatch.slice(0, 200));
588
+ }
589
+ }
590
+ catch (err) {
591
+ const errMsg = err instanceof Error ? err.message : String(err);
592
+ ctx.reply(`Image generation failed: ${errMsg}`).catch(() => { });
593
+ log(`image-gen: error — ${errMsg}`);
594
+ }
595
+ })().catch((err) => log(`image-gen: unhandled error — ${err}`));
596
+ return;
597
+ }
598
+ // --- Plain text / unrecognized commands → dispatch to iTerm2 ---
599
+ dispatchIncomingMessage(text, timestamp);
600
+ // Prefix with adapter source tag so Claude knows where to reply
601
+ const tag = ctx.source === "pailot"
602
+ ? "PAILot"
603
+ : ctx.source === "telex"
604
+ ? "Telex"
605
+ : "Whazaa";
606
+ let textToDeliver;
607
+ if (trimmedText.startsWith("!")) {
608
+ textToDeliver = text.replace(/^!/, "");
609
+ }
610
+ else if (trimmedText.startsWith("/")) {
611
+ textToDeliver = text;
612
+ }
613
+ else if (/^\[(?:Voice note|Audio)\]:/.test(trimmedText)) {
614
+ textToDeliver = `[${tag}:voice] ${text}`;
615
+ }
616
+ else {
617
+ textToDeliver = `[${tag}] ${text}`;
618
+ }
619
+ // Route based on active session kind
620
+ const activeHybrid = hybridManager?.activeSession;
621
+ if (activeHybrid?.kind === "api") {
622
+ deliverViaApi(hybridManager.apiBackend, textToDeliver, activeHybrid.backendSessionId, {
623
+ sendText: (replyText) => ctx.reply(replyText),
624
+ sendVoice: (buffer, transcript) => ctx.replyVoice(buffer, transcript ?? ""),
625
+ });
626
+ return;
627
+ }
628
+ // Visual session — deliver to iTerm2
629
+ deliverMessage(textToDeliver);
630
+ };
631
+ }
632
+ //# sourceMappingURL=commands.js.map